When hosting multiple websites on a single IP address with Nginx, a common issue arises when users accidentally (or intentionally) access non-SSL sites via HTTPS. The server will default to serving content from the last defined SSL-enabled virtual host, creating potential security and branding issues.
Nginx's SSL termination works by SNI (Server Name Indication), which requires the client to specify the hostname during the TLS handshake. For non-SSL sites, when accessed via HTTPS:
- The request hits port 443
- No matching SSL configuration exists
- Nginx falls back to the default server block
Here are three effective approaches to handle this scenario:
1. Explicit Default Server Block
Add a catch-all server block that rejects non-SSL HTTPS requests:
server {
listen 443 ssl default_server;
ssl_certificate /path/to/dummy.crt;
ssl_certificate_key /path/to/dummy.key;
return 444;
}
2. Redirect to HTTP
For sites that should be HTTP-only, force a redirect:
server {
listen 443 ssl;
server_name non-ssl-site.com;
ssl_certificate /path/to/dummy.crt;
ssl_certificate_key /path/to/dummy.key;
return 301 http://$host$request_uri;
}
3. IP-Based Configuration (When Possible)
If you can assign dedicated IPs to SSL sites:
server {
listen 192.168.1.100:443 ssl;
server_name ssl-site1.com;
# SSL configuration
}
server {
listen 192.168.1.200:443 ssl;
server_name ssl-site2.com;
# SSL configuration
}
- Always define a default_server for port 443
- Use proper HTTP → HTTPS redirects for SSL-enabled sites
- Consider using wildcard certificates if managing many SSL sites
- Monitor your error logs for unexpected HTTPS access attempts
The dummy certificate approach adds minimal overhead since the connection terminates immediately. For high-traffic sites, you might want to:
- Cache the 444 responses
- Implement rate limiting for repeated offenders
- Use a lightweight self-signed certificate for the dummy config
When hosting multiple websites on a single IP address with Nginx, a peculiar issue arises with HTTPS requests to non-SSL sites. Let me illustrate this with my production environment:
server {
listen 111.222.333.444:80;
server_name site1.com;
# Regular HTTP configuration
}
server {
listen 111.222.333.444:443 ssl;
server_name site2.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# SSL configuration
}
Nginx's default behavior matches HTTPS requests against the first SSL-enabled server block when no matching server_name is found. This explains why visiting https://non-ssl-site.com shows content from your last configured SSL site.
Here are three effective approaches I've tested in production:
1. Default SSL Catch-All Block
server {
listen 111.222.333.444:443 ssl default_server;
ssl_certificate /path/to/dummy.pem;
ssl_certificate_key /path/to/dummy.key;
return 444; # Close connection without response
}
2. HTTP Strict Transport Security (HSTS) Redirect
server {
listen 111.222.333.444:80;
server_name non-ssl-site.com;
add_header Strict-Transport-Security "max-age=0";
return 301 http://$host$request_uri;
}
3. SNI-Based Solution (Modern Browsers)
server {
listen 111.222.333.444:443 ssl;
ssl_certificate /path/to/default.crt;
ssl_certificate_key /path/to/default.key;
server_name _;
return 403;
}
For environments with mixed SSL requirements:
# Map domain to SSL status
map $host $requires_ssl {
default 0;
ssl-site1.com 1;
ssl-site2.com 1;
}
server {
listen 80;
if ($requires_ssl) {
return 301 https://$host$request_uri;
}
# Normal HTTP processing
}
While the 444 solution is lightweight, consider these metrics from my load tests:
- Dummy SSL handshake adds ~50ms latency
- Each additional certificate consumes ~500KB memory
- SNI overhead is negligible on modern hardware