Skip to content
CloudOps
Newsletter
All guides
AI for NGINX By James Joyner IV · · 10 min read

Hardening NGINX TLS/SSL With AI Without Shipping Hallucinated Ciphers

Use AI to draft NGINX TLS config—ssl_protocols, ssl_ciphers, HSTS, OCSP stapling—then verify every cipher against Mozilla's generator before reload.

  • #nginx
  • #ai
  • #tls
  • #ssl
  • #security

Last quarter I pasted an AI-generated ssl_ciphers line straight into a staging config and ran nginx -t. It failed. The model had confidently handed me a cipher named TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA512—which reads like a real OpenSSL cipher but isn’t one OpenSSL recognizes in that string format. NGINX rejected the whole block. That five-second failure is the entire reason I wrote this guide: AI is genuinely good at drafting and explaining TLS config, but cipher names live in a namespace where one wrong token silently looks correct and hard-fails at load. You stay in control by validating against an authoritative source and nginx -t, every time.

This is specifically about the TLS layer—protocols, ciphers, OCSP, HSTS, session resumption. If you want a broader sweep of your server blocks, request limits, and exposed methods, that’s a different job; see reviewing NGINX security configuration with AI. And if you’re chasing response-header hardening, the HTTP security headers and CSP guide covers that ground. Here we stay on the wire.

The one rule: ciphers come from Mozilla, not the model

The Mozilla SSL Configuration Generator is the authoritative source for which protocols and ciphers to enable. It encodes the current “modern” and “intermediate” profiles, tracks OpenSSL version compatibility, and—critically—emits cipher strings that actually parse. Treat its output as the source of truth. Use AI to explain what each directive does and to adapt the surrounding config to your environment, not to invent the cipher list.

Here’s a prompt that keeps the model in its lane:

You are reviewing an NGINX TLS config. I will paste a ssl_ciphers line generated by the Mozilla SSL Config Generator (intermediate profile, OpenSSL 3.0). Do NOT rewrite or “improve” the cipher list—explain what each cipher family does and confirm whether the directive ordering interacts correctly with ssl_prefer_server_ciphers. Flag anything that would fail nginx -t.

That framing matters. When you ask AI to generate ciphers, you invite hallucination. When you ask it to explain a list you control, you get the upside—clear reasoning about ECDHE forward secrecy, GCM versus CBC, why TLS 1.3 ciphers aren’t configured the same way—without the failure mode.

A modern intermediate TLS block

This is the shape I run for general-purpose sites that still need to support slightly older clients. The cipher line here is the intermediate profile; copy yours fresh from the generator for your exact OpenSSL version rather than trusting any blog’s snapshot, including this one.

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/example.com/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com/privkey.pem;

    # Protocols: drop everything below TLS 1.2.
    ssl_protocols TLSv1.2 TLSv1.3;

    # Cipher list: paste from Mozilla generator for your OpenSSL version.
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    # For TLS 1.2, let the server's ordered list win.
    ssl_prefer_server_ciphers on;

    # TLS 1.3 ciphers are negotiated by the protocol itself and are
    # not controlled by ssl_ciphers; ssl_prefer_server_ciphers has no
    # effect on 1.3. Don't let AI tell you to "add" 1.3 ciphers here.

    # Session resumption: cut full-handshake overhead on repeat visits.
    ssl_session_cache   shared:MozSSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # DH params for the DHE cipher families above.
    ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    # OCSP stapling.
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/example.com/chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # HSTS: tell browsers to stay on HTTPS. Start conservative.
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
}

A few things AI tends to get subtly wrong that are worth internalizing. First, the TLS 1.3 point above: those cipher suites are mandatory-to-implement and not configured through ssl_ciphers in the normal NGINX build. If a model tells you to append TLS_AES_256_GCM_SHA384 to your ssl_ciphers line, it’s confusing OpenSSL’s two separate cipher APIs—ignore it. Second, ssl_prefer_server_ciphers only influences TLS 1.2 negotiation; it does nothing for 1.3, where the client’s preference is honored by design.

Generate the DH params yourself

ssl_dhparam needs a real file. Don’t let AI hand you a pre-baked PEM—generate your own so it’s genuinely random and matches a bit length you trust.

# 2048 is the practical floor; 4096 is slower to generate but fine.
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

If you’ve gone fully ECDHE/TLS 1.3 and dropped the DHE cipher families from your list, you don’t need ssl_dhparam at all—remove both the DHE ciphers and the directive together. AI is helpful here for explaining the tradeoff: DHE buys you broader client compatibility at a CPU cost, ECDHE is faster and universally supported by anything modern.

HSTS: think before you preload

Strict-Transport-Security is a commitment. Once a browser sees it with a long max-age, it refuses plain HTTP to your domain for that duration. That’s the point, but it bites if you have a subdomain still on HTTP. Ask AI to walk you through the implications before you add includeSubDomains, and absolutely before you add preload:

I’m adding HSTS to example.com. I have api.example.com and a legacy status.example.com that is HTTP-only. Explain exactly what includeSubDomains and preload will break, and what order I should roll them out in.

A good answer flags that includeSubDomains will immediately break status.example.com for any visitor who has already hit the apex over HTTPS, and that preload is effectively irreversible on a useful timescale. Roll out without includeSubDomains first, confirm every subdomain is HTTPS, then tighten. The deeper response-header discussion lives in the security headers guide.

Verify before you reload

This is the non-negotiable step. Config that parses is necessary but not sufficient—you also want to confirm the live handshake matches intent.

# 1. Does the config even parse? Catches hallucinated cipher names.
nginx -t

# 2. Reload only if -t passed.
nginx -s reload

Then confirm the actual negotiated protocol and cipher against the running server:

# Confirm TLS 1.3 is offered and stapling is active.
openssl s_client -connect example.com:443 -tls1_3 -status </dev/null 2>/dev/null \
  | grep -E "Protocol|Cipher|OCSP"

# Confirm a TLS 1.1 handshake is REJECTED (it should fail).
openssl s_client -connect example.com:443 -tls1_1 </dev/null 2>&1 \
  | grep -E "no protocols available|handshake failure|Protocol"

The first command should show your negotiated cipher and an OCSP Response Status: successful line if stapling is wired correctly. The second should fail to connect—if it succeeds, your ssl_protocols line isn’t doing its job. This empirical check is what catches the gap between what AI told you the config does and what it actually does. The cost of being wrong on TLS is high enough that “the model said so” is never the stopping point.

Where AI actually earns its keep

The leverage isn’t generation—it’s explanation and review at the speed of conversation. Paste a config block and ask “what’s the security impact of ssl_session_tickets on here, and why might I want it off?” and you’ll get a genuinely useful answer about ticket key rotation and forward secrecy that would otherwise be twenty minutes of reading. Ask it to diff your block against the Mozilla intermediate profile and tell you what you’re missing. Use it to draft the unfamiliar parts—resolver tuning, stapling caveats—then verify.

I keep a small library of these review prompts; if you want a starting set, the prompts collection has TLS and NGINX entries you can adapt. And the rest of the NGINX category covers the surrounding config—rate limiting, proxying, log analysis—where the same draft-then-verify loop applies.

The discipline is simple and it’s the whole game: let AI explain and draft, pull ciphers and protocols from Mozilla, and prove it with nginx -t plus a live openssl s_client handshake before anything reaches production. Stay in control of the cryptographic decisions and AI becomes a fast, knowledgeable pair—skip the verification and it becomes the fastest way to ship a broken TLS config you’ve ever seen.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.