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.

  • Security
  • $tls-hardening
  • 140 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- tls-hardening

Installs to ~/.agents/skills/tls-hardening/ for every project on this machine.

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

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.

~/.agents/skills/tls-hardening/SKILL.md

--- name: tls-hardening description: Hardens server-side TLS on nginx, Apache, HAProxy, Caddy, load balancers and CDNs - protocol versions, cipher suites, key exchange groups,… license: MIT ---

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

  1. Read AGENTS.md and find where TLS is terminated. It is often not the app: check nginx.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.
  2. 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.
  3. 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

ClientsProfileProtocolsNotes
Modern browsers, your own apps (iOS 12.2+, Android 10+)Mozilla modernTLS 1.3 onlySimplest; nothing to tune
Public website, APIs with unknown clients (default)Mozilla intermediateTLS 1.2 + 1.3ECDHE + AEAD only for 1.2
Known legacy device fleetSeparate legacy endpoint1.2 with extra suitesIsolate 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_ciphers only governs TLS 1.2 and below. TLS 1.3 suites are all AEAD and fine at their defaults; change them only through ssl_conf_command Ciphersuites ... if a policy demands it.
  • Hybrid post-quantum key exchange: with OpenSSL 3.5+ (or BoringSSL-based proxies) you can put X25519MLKEM768 first in the groups list. Check openssl version on 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-tickets and ssl-default-bind-ciphers in global.
  • 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), CloudFront TLSv1.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_certificate pairs).
  • 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_stapling for 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

  1. Confirm every subdomain serves valid HTTPS (internal hosts too, if includeSubDomains).
  2. Start with max-age=300, then a week, then 63072000 (two years).
  3. Add preload and 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 server has 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_key with 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_protocol and $ssl_cipher before removing anything (add them to the nginx log_format for a week).
  • nginx -t passing does not mean the running process reloaded; run nginx -s reload (or the service manager equivalent) and rescan.
  • mTLS between services is a different job: verify ssl_verify_client and the CA bundle, and never accept optional_no_ca in 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.yamlgenerated from this page

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.

esc