I am building a domain name management app that handles multiple domain names and serves different landing pages based on the domain name.
I have set up a DNS system that works using PowerDNS, any domain with my NS servers resolves to my server.
I have the following Nginx configuration:
# Define custom log format for domain monitoring
log_format domain_logger '{"time":"$time_local","host":"$host","remote_addr":"$remote_addr","request":"$request"}';
# Define debug log format to include SSL certificate information
log_format ssl_debug '{"time":"$time_local","host":"$host","http_host":"$http_host","ssl_certificate":"$ssl_certificate_by_domain","ssl_key":"$ssl_certificate_key_by_domain","has_cert":"$has_valid_certificate"}';
# Include dynamically generated certificate mappings
include /etc/nginx/conf.d/cert-mappings.conf;
# Include domains with certificates map
include /etc/nginx/conf.d/domains-with-certs.conf;
# Portfolio domains with valid certificates - SSL server (Catch-all for SSL)
server {
listen 443 ssl default_server; # Make this the default SSL server
listen [::]:443 ssl default_server; # IPv6
# This server block will handle all SSL connections
# We use dynamic certificate paths from the map
ssl_certificate $ssl_certificate_by_domain;
ssl_certificate_key $ssl_certificate_key_by_domain;
# Fallback for SNI (Server Name Indication) when no matching server_name is found
# This helps in serving a default certificate when SNI is not provided or
# when the domain doesn't match an explicit server_name.
# ssl_reject_handshake off; # Keep this off here, as we are serving a certificate
# Add debug headers to see which certificate is being used
add_header X-SSL-Certificate-Path $ssl_certificate_by_domain;
add_header X-SSL-Key-Path $ssl_certificate_key_by_domain;
add_header X-Host $host;
add_header X-HTTP-Host $http_host;
add_header X-Has-Certificate $has_valid_certificate;
# Log domain access for certificate generation
access_log /etc/nginx/logs/domains.log domain_logger;
# Add SSL debug logging
access_log /etc/nginx/logs/ssl_debug.log ssl_debug;
location / {
proxy_pass http://app:5555;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host; # Use $http_host for consistency
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
}
# Handle all domains on HTTP - used for certificate challenges and for domains without certificates
server {
listen 80 default_server; # Catch-all for HTTP
listen [::]:80 default_server; # IPv6
# Log domain access for certificate generation
access_log /etc/nginx/logs/domains.log domain_logger;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
allow all; # Ensure certbot can access this
}
# For domains with certificates, redirect to HTTPS
if ($has_valid_certificate = 1) {
return 301 https://$host$request_uri;
}
# For domains without certificates, show a message or forward to app
location / {
# Option 1: Show a custom message that HTTPS is not yet available
add_header Content-Type text/html;
return 200 '<html><body><h1>Secure connection not yet available</h1><p>Please try again in a few minutes while we set up SSL for this domain.</p></body></html>';
# Option 2: Forward to app (uncomment if you prefer this)
# proxy_pass http://app:5555;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header Host $http_host;
# proxy_set_header X-Forwarded-Proto $scheme;
# proxy_redirect off;
}
}
The problem I encountered is that Nginix serves the default self-signed certificate even though the domain is available in the mapping:
map $http_host $ssl_certificate_by_domain {
default /etc/nginx/ssl/default.crt;
domainandco.com /etc/letsencrypt/live/domainandco.com/fullchain.pem;
www.domainandco.com /etc/letsencrypt/live/domainandco.com/fullchain.pem;
isa7.com /etc/letsencrypt/live/isa7.com/fullchain.pem;
www.isa7.com /etc/letsencrypt/live/isa7.com/fullchain.pem;
}
map $http_host $ssl_certificate_key_by_domain {
default /etc/nginx/ssl/default.key;
domainandco.com /etc/letsencrypt/live/domainandco.com/privkey.pem;
www.domainandco.com /etc/letsencrypt/live/domainandco.com/privkey.pem;
isa7.com /etc/letsencrypt/live/isa7.com/privkey.pem;
www.isa7.com /etc/letsencrypt/live/isa7.com/privkey.pem;
}
Why does Nginx serve the default certificate instead of the right one for the domain?
You can't choose the server certificate based on $http_host
, since the value of $http_host
is the value of the Host
header in the request and is only known after the TLS handshake. But the certificate is already needed within the TLS handshake, i.e. earlier than you provide it.
What is available early is $ssl_server_name
, which is the server_name
information from the TLS ClientHello. Try this instead.
Note though that dynamically setting a certificate can have a performance impact. To cite from the documentation:
Note that using variables implies that a certificate will be loaded for each SSL handshake, and this may have a negative impact on performance.
Benchmarks done here show about 40% less performance.