Network exposure

Terminate TLS deliberately

Use a maintained reverse proxy or platform endpoint for HTTPS and keep upstream connections, redirects, certificates, and headers explicit.

HTTPS protects traffic to the TLS endpoint. Know exactly where decryption happens and what path follows it.

On a single VPS the usual answer is: Nginx terminates TLS on port 443 and forwards plain HTTP to the application on loopback. That is a fine design — as long as you chose it, and can state it.

One host, one clear boundary

server {
    listen 443 ssl;
    server_name blog.flaviocopes.com;

    ssl_certificate     /etc/letsencrypt/live/blog.flaviocopes.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.flaviocopes.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

Bind the application to loopback when only the reverse proxy should reach it. Nginx may terminate TLS while forwarding plain HTTP to an app on loopback. That is reasonable on one host, but the same hop across a private network needs its own protection decision.

Redirect HTTP deliberately instead of leaving port 80 serving content:

server {
    listen 80;
    server_name blog.flaviocopes.com;
    return 301 https://$host$request_uri;
}

Keep renewal automated and proven

Let’s Encrypt certificates expire after 90 days, so automate certificate renewal and rehearse it:

sudo certbot renew --dry-run
# Congratulations, all simulated renewals succeeded

Test certificate expiry and renewal alerts. The classic failure is silent: renewal broke months ago, nothing complained, and you learn about it from a browser error on launch day. An external monitor that checks days-to-expiry catches this while it is still boring.

Guard the identity headers

Pass trusted proxy headers through a controlled boundary. Proxy identity headers need the same boundary. Remove caller-supplied copies at the edge, then add trusted values so the application never confuses internet input with proxy evidence. In the snippet above, proxy_set_header overwrites whatever X-Forwarded-Proto a client tried to smuggle in.

Record the protocol, certificate owner, and trust boundary for every browser-to-database hop. Force HTTP, an expired test certificate, and a direct app-port request, then prove each fails or redirects as designed.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →