How to Block Non-Domain Requests in Nginx on AWS Elastic Beanstalk to Prevent Bot Scans


10 views

Many Django developers on AWS Elastic Beanstalk encounter this frustrating scenario:

DisallowedHost at //www/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
Invalid HTTP_HOST header: 'xx.xxx.xx.xx'. You may need to add 'xx.xxx.xxx.xx' to ALLOWED_HOSTS.

These occur when bots scan your server's IP address directly rather than using your domain name, triggering Django's security checks.

In an AWS Elastic Beanstalk environment with Nginx:

  1. Requests first hit the load balancer
  2. Nginx acts as a reverse proxy
  3. Your Django application runs on localhost

The default Nginx configuration allows direct IP access, which we need to modify.

The most effective approach is to configure Nginx with:

  1. A default server block that drops non-domain requests
  2. A named server block that handles your legitimate domain traffic

Implementation Steps

Create a new configuration file in your .platform/nginx/conf.d/ directory (for Elastic Beanstalk):

# .platform/nginx/conf.d/domain_filter.conf
server {
    listen 80 default_server;
    server_name _;
    return 444;
}

server {
    listen 80;
    server_name example.com *.example.com;
    
    # Rest of your existing configuration
    include conf.d/elasticbeanstalk/*.conf;
}

For dynamic subdomains, use these patterns:

server_name ~^(www\.)?example\.com$ ~^(.+)\.example\.com$;

This regex pattern will match:

  • example.com
  • www.example.com
  • any-subdomain.example.com

1. Health Checks: Ensure AWS health checks still work:

location /health {
    access_log off;
    return 200;
}

2. SSL Termination: If using HTTPS, update both server blocks:

server {
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate /path/to/cert;
    ssl_certificate_key /path/to/key;
    return 444;
}

Here's a full example combining all elements:

# .platform/nginx/conf.d/secure_domain.conf
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    return 444;
}

server {
    listen 80;
    server_name ~^(www\.)?example\.com$ ~^(.+)\.example\.com$;
    
    # Redirect HTTP to HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name ~^(www\.)?example\.com$ ~^(.+)\.example\.com$;
    
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # Your existing proxy configuration
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    location /health {
        access_log off;
        return 200;
    }
}

After deployment:

  1. Verify domain access works normally
  2. Test that direct IP access returns no response (connection closed)
  3. Check health check endpoints still function
  4. Monitor error logs for any legitimate traffic being blocked

When running Django applications on AWS Elastic Beanstalk, you'll often encounter bots scanning for vulnerabilities by making direct requests to your server's IP address. This generates numerous errors like:

DisallowedHost at //www/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
Invalid HTTP_HOST header: 'xx.xxx.xx.xx'. You may need to add 'xx.xxx.xxx.xx' to ALLOWED_HOSTS.

AWS Elastic Beanstalk provides a default Nginx configuration that needs modification to properly handle domain-based filtering. The key files are:

/etc/nginx/nginx.conf
.platform/nginx/conf.d/elasticbeanstalk/00_application.conf

We'll implement a solution that:

  • Blocks all requests not using your domain
  • Works with wildcard subdomains
  • Maintains ELB health check functionality
# Add this to your .platform/nginx/conf.d/elasticbeanstalk/00_application.conf
server {
    listen 80 default_server;
    server_name _;
    return 444;
}

server {
    listen 80;
    server_name mydomain.com *.mydomain.com;
    
    # Rest of your existing configuration
    location / {
        # Your existing location block
    }
    
    location = /health-check.html {
        # Your existing health check configuration
    }
}

The solution works by:

  1. Creating a default server block that drops connections (444)
  2. Creating a specific server block for your domains that processes legitimate traffic

Key points to note:

  • The default server block uses return 444 which immediately closes the connection
  • The domain-specific block matches both your root domain and all subdomains
  • Health checks continue to work as they match the specific location block

After deploying, test with:

curl -I http://yourdomain.com
curl -I http://[your-server-ip]

The first should return normal headers, while the second should fail to connect.

For enhanced security, consider adding:

# Block requests with invalid Host headers
if ($host !~* ^(mydomain.com|www.mydomain.com|.*\.mydomain.com)$ ) {
    return 444;
}