nginxsslreverse-proxynginx-config

Nginix catch-all domain configuration file serves the default SSL certificate


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?


Solution

  • 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.