TLS Hardening skill for Codex
TLS 1.2+ with AEAD only, proven by a scan. Codex finds where TLS actually terminates, baselines it with testssl.sh, then applies a Mozilla intermediate or modern profile with ECDHE and AEAD suites, a complete chain and staged HSTS. It checks access logs for legacy clients before removing protocols, keeps legacy devices on a separate endpoint, and rescans to show no medium or worse findings.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- tls-hardening Installs to ~/.agents/skills/tls-hardening/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install tls-hardening The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project tls-hardening Installs to .agents/skills/tls-hardening/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/tls-hardening.zipunzip -o tls-hardening.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) tls-hardening PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $tls-hardening, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Hardens server-side TLS on nginx, Apache, HAProxy, Caddy, load balancers and CDNs - protocol versions, cipher suites, key exchange groups, certificate chains, HSTS and session resumption - and proves the result with testssl.sh and openssl s_client. Use when a scanner flags weak ciphers or TLS 1.0/1.1, when choosing a Mozilla modern or intermediate profile, before a compliance review (PCI DSS, SOC 2), or when clients fail handshakes after a config change.
Use it when
- A scanner or auditor flags old protocols, CBC suites or a broken chain
- You are setting TLS for a new nginx, HAProxy, ALB or CDN endpoint
- Clients started failing handshakes after a TLS change
- You want to roll out HSTS without locking out subdomains
Use something else when
- You need to issue or auto-renew certificates with ACME: use tls-certificates
- You are setting response headers beyond HSTS: use security-headers
How to check the result
openssl s_client fails with -tls1_1 and succeeds with -tls1_3, and a testssl.sh rerun saved as JSON shows no MEDIUM or higher findings and a trusted chain.
The mistake it prevents
Hardening the origin server while a CDN or load balancer in front still negotiates TLS 1.0 with every real client.
Done well means: TLS 1.2 and 1.3 only, forward-secret AEAD suites only, a complete chain that every target client validates, HSTS where the whole domain is HTTPS, and a saved testssl.sh run before and after that shows nothing rated MEDIUM or worse. Compatibility is decided from real client data, not from a guess.
Before changing anything
- Read
AGENTS.mdand find where TLS is terminated. It is often not the app: checknginx.conf/sites-enabled,haproxy.cfg,Caddyfile, ingress annotations, Terraform for ALB/CloudFront listeners, or the CDN dashboard. Hardening the origin does nothing if a load balancer in front negotiates TLS with clients. - Record the current state with a baseline scan (needs network, and testssl.sh or Docker installed; ask for approval in the Codex sandbox). Only scan hosts the user owns or is authorized to test.
- Ask who the clients are: browsers only, mobile apps with old OS floors, IoT devices, payment terminals, partner integrations. Old Java (7), Android 4.x and Windows 7 era clients are what break when TLS 1.0/1.1 or CBC suites go.
# testssl.sh 3.2+; --jsonfile keeps a machine-readable baseline
./testssl.sh --quiet --warnings batch --jsonfile baseline.json example.com:443
docker run --rm -t ghcr.io/testssl/testssl.sh --quiet example.com # container alternative
./testssl.sh -p -P -S -U example.com # protocols, server preference, cert, vulns only
./testssl.sh --starttls smtp mail.example.com:25 # non-HTTPS services
Choose a profile
| Clients | Profile | Protocols | Notes |
|---|---|---|---|
| Modern browsers, your own apps (iOS 12.2+, Android 10+) | Mozilla modern | TLS 1.3 only | Simplest; nothing to tune |
| Public website, APIs with unknown clients (default) | Mozilla intermediate | TLS 1.2 + 1.3 | ECDHE + AEAD only for 1.2 |
| Known legacy device fleet | Separate legacy endpoint | 1.2 with extra suites | Isolate on its own hostname/IP; never weaken the main one |
Generate the starting config with the Mozilla SSL Configuration Generator for the exact server and OpenSSL version, then review it against this file. Do not hand-type cipher lists from memory.
Reference configuration (nginx, intermediate)
ssl_protocols TLSv1.2 TLSv1.3;
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;
ssl_prefer_server_ciphers off; # all suites are strong; let clients pick (ChaCha on mobile)
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_session_cache shared:TLS:10m;
ssl_session_timeout 1d;
ssl_session_tickets off; # unless ticket keys are rotated across the fleet
ssl_certificate /etc/ssl/example/fullchain.pem; # leaf + intermediates, no root
ssl_certificate_key /etc/ssl/example/privkey.pem;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
ssl_ciphersonly governs TLS 1.2 and below. TLS 1.3 suites are all AEAD and fine at their defaults; change them only throughssl_conf_command Ciphersuites ...if a policy demands it.- Hybrid post-quantum key exchange: with OpenSSL 3.5+ (or BoringSSL-based proxies) you can
put
X25519MLKEM768first in the groups list. Checkopenssl versionon the actual host first; an unknown group name stops nginx from starting. - Apache equivalents:
SSLProtocol -all +TLSv1.2 +TLSv1.3,SSLCipherSuite,SSLHonorCipherOrder off,SSLSessionTickets off. HAProxy:ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-ticketsandssl-default-bind-ciphersinglobal. - Caddy already defaults to TLS 1.2+ with strong suites; prefer leaving its defaults alone.
- Managed endpoints use named policies: AWS ALB/NLB
ELBSecurityPolicy-TLS13-1-2-2021-06(or the newer TLS13 policies with post-quantum groups), CloudFrontTLSv1.2_2021, Cloudflare “Minimum TLS Version” 1.2 plus TLS 1.3 on. Change them in Terraform, not the console, so the change is reviewable.
Certificates and chain
- Serve leaf plus intermediates. A missing intermediate works in desktop Chrome (AIA
fetching) and fails in curl, Java, Python and Android. Test with a client that does not
fetch:
openssl s_client -connect host:443 -servername host </dev/null. - ECDSA P-256 leaf keys are the default for new certificates; keep an RSA-2048+ cert in
parallel only if a legacy client needs it (nginx accepts two
ssl_certificatepairs). - Certificate lifetimes are shrinking under CA/Browser Forum rules (200 days maximum from March 2026, dropping further in 2027 and 2029). Automate issuance and renewal with ACME; any manual renewal step is now an outage waiting to happen.
- Let’s Encrypt ended OCSP in 2025. Do not add
ssl_staplingfor those certificates; it only produces warnings. Stapling is still reasonable for CAs that publish OCSP. - Issuance, ACME clients and renewal automation belong to
$tls-certificates.
HSTS rollout
- Confirm every subdomain serves valid HTTPS (internal hosts too, if
includeSubDomains). - Start with
max-age=300, then a week, then63072000(two years). - Add
preloadand submit to hstspreload.org only after explicit user confirmation: removal from the preload list takes months and cannot be rushed.
Gotchas
- TLS 1.0/1.1 disabled on the origin but still offered by the CDN edge: scan the public hostname and the origin separately.
- SNI-less clients get the default server block. Make sure the default
serverhas the same hardened settings, or scanners see a weaker config on the bare IP. - Session tickets with a static key that never rotates undermine forward secrecy. Rotate keys
(
ssl_session_ticket_keywith several files, rotated by a job) or turn tickets off. - Removing CBC or RSA key exchange breaks old Java and embedded clients silently: their
failures appear in client logs, not server logs. Check access logs for
$ssl_protocoland$ssl_cipherbefore removing anything (add them to the nginxlog_formatfor a week). nginx -tpassing does not mean the running process reloaded; runnginx -s reload(or the service manager equivalent) and rescan.- mTLS between services is a different job: verify
ssl_verify_clientand the CA bundle, and never acceptoptional_no_cain production.
Verification
openssl s_client -connect example.com:443 -servername example.com -tls1_1 </dev/null # must fail
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null # must succeed
curl -sI https://example.com | grep -i strict-transport
./testssl.sh --quiet --jsonfile after.json --severity MEDIUM example.com
- testssl.sh shows no finding at MEDIUM or above; protocols list shows only 1.2 and 1.3.
- Chain: “Chain of trust: Ok”, no “incomplete chain” warning.
- Real clients from the inventory (mobile app build, partner test, monitoring probe) connect.
- If a client fails, restore the previous config from version control, add the client’s suite to a separate legacy endpoint, and rescan; never re-enable 1.0/1.1 globally.
Report template
**Endpoint:** example.com:443 (terminated at nginx 1.26 on web-01..04, OpenSSL 3.0.13)
**Before:** TLS 1.0/1.1 offered; 3DES and CBC suites; no HSTS; chain missing R11 intermediate
**Change:** Mozilla intermediate; ECDHE+AEAD only; X25519/P-256; tickets off; full chain; HSTS 2y (no preload)
**After:** testssl.sh 3.2: no MEDIUM+ findings (after.json attached)
**Client check:** iOS app 5.4, Android app min SDK 26, partner SFTP gateway: all connect
**Follow-up:** preload decision pending owner confirmation; legacy POS endpoint scheduled for removal What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "TLS Hardening"
short_description: "TLS 1.2+ with AEAD only, proven by a scan"
default_prompt: "$tls-hardening Our scan says we still support TLS 1.0 and weak ciphers - fix the nginx config" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.