What is it? #
HTTPS in production is mostly about keeping it working rather than setting it up. Certificates expire, configuration drifts, and a wrong setting can be hard to reverse.
Three things need attention: renewal must be automated and monitored, the protocol configuration should exclude outdated versions and ciphers, and HSTS should be enabled once you are confident.
HSTS in particular deserves care. It instructs browsers to refuse plain HTTP for your domain for a defined period, and it cannot be withdrawn quickly.
Behind a CDN or load balancer, TLS often terminates at the edge, which changes where renewal happens and what the origin sees.
Think of it like this #
A door lock that must be re-certified quarterly and whose certificate is checked by everyone entering.
Letting it lapse locks out every visitor at once, and the fix is quick only if someone noticed before the queue formed.
Simple example #
A certificate renewal fails silently for six weeks because a deploy hook was removed. The expiry monitor fires at fourteen days, and it is fixed calmly rather than during an outage.
Code #
# Modern TLS configuration
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.0 and 1.1 are obsolete
ssl_prefer_server_ciphers off; # modern advice: let the client choose
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305;
ssl_session_cache shared:SSL:10m; # resumption: faster repeat visits
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_stapling on; # OCSP stapling: faster validation
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
HSTS, and why to be careful
Strict-Transport-Security: max-age=31536000; includeSubDomains
The browser will refuse plain HTTP for this domain for a year, and will
not let users click through a certificate warning.
Roll it out gradually:
max-age=300 five minutes, to verify nothing breaks
max-age=86400 one day
max-age=31536000 one year, once confident
includeSubDomains covers every subdomain — make sure they all have
valid certificates first, or they become unreachable.
preload submits your domain to a browser-shipped list. It is very
difficult to reverse. Do not enable it casually.
# Monitoring: automation fails silently
for DOMAIN in example.com api.example.com admin.example.com; do
END=<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>o</mi><mi>p</mi><mi>e</mi><mi>n</mi><mi>s</mi><mi>s</mi><mi>l</mi><msub><mi>s</mi><mi>c</mi></msub><mi>l</mi><mi>i</mi><mi>e</mi><mi>n</mi><mi>t</mi><mo>−</mo><mi>c</mi><mi>o</mi><mi>n</mi><mi>n</mi><mi>e</mi><mi>c</mi><mi>t</mi><mi mathvariant="normal">"</mi></mrow><annotation encoding="application/x-tex">(openssl s_client -connect "</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal">o</span><span class="mord mathnormal">p</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">ss</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord"><span class="mord mathnormal">s</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.1514em;"><span style="top:-2.55em;margin-left:0em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="katex-sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight">c</span></span></span></span><span class="vlist-s"></span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">i</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:0.6944em;"></span><span class="mord mathnormal">co</span><span class="mord mathnormal">nn</span><span class="mord mathnormal">ec</span><span class="mord mathnormal">t</span><span class="mord">"</span></span></span></span>DOMAIN":443 -servername "$DOMAIN" </dev/null 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
DAYS=<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mo stretchy="false">(</mo><mo stretchy="false">(</mo></mrow><annotation encoding="application/x-tex">(( (</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(((</span></span></span></span>(date -d "<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>E</mi><mi>N</mi><mi>D</mi><mi mathvariant="normal">"</mi><mo>+</mo></mrow><annotation encoding="application/x-tex">END" +%s) -</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal" style="margin-right:0.0576em;">E</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord">"</span><span class="mord">+</span></span></span></span>(date +%s) ) / 86400 ))
echo "<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>D</mi><mi>O</mi><mi>M</mi><mi>A</mi><mi>I</mi><mi>N</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">DOMAIN:</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.6833em;"></span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span><span class="mord mathnormal" style="margin-right:0.0278em;">O</span><span class="mord mathnormal" style="margin-right:0.109em;">M</span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.0785em;">I</span><span class="mord mathnormal" style="margin-right:0.109em;">N</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>DAYS days"
[ "<span class="katex-error" title="ParseError: KaTeX parse error: Expected 'EOF', got '&' at position 16: DAYS" -lt 14 ] &̲& alert "" style="color:#cc0000">DAYS" -lt 14 ] && alert "</span>DOMAIN certificate expires in $DAYS days"
done
What commonly goes wrong
expired certificate renewal broke and nobody was alerted
missing intermediates serving cert.pem instead of fullchain.pem;
browsers often work, API clients fail
mixed content an HTTPS page loading an HTTP resource: blocked
wrong name the certificate does not cover www, or a subdomain
HSTS too early enabled before all subdomains had certificates
renewed but not reloaded the new certificate is on disk, the old one served
How it works #
Restricting to TLS 1.2 and 1.3 removes protocol versions with known weaknesses. Modern clients all support at least 1.2, and the compatibility cost is negligible.
Letting the client choose the cipher is current advice, because modern clients know which cipher performs best on their hardware.
Session resumption avoids repeating the full handshake for returning visitors, which is a measurable latency improvement on sites with many short connections.
OCSP stapling has the server fetch the certificate's revocation status and include it in the handshake, saving the client a separate request and improving privacy.
HSTS is powerful and awkward to reverse. The gradual rollout exists because a mistake — a subdomain without a certificate, combined with includeSubDomains — makes that subdomain unreachable for the full max-age period, with no way to override it from the server side.
Preloading is stronger still: the setting ships inside browsers, so removal takes months regardless of what your server sends.
Monitoring every domain separately matters because renewal usually breaks for one specific certificate, often a subdomain nobody thinks about.
The missing-intermediates problem is particularly deceptive, because browsers frequently cache intermediates from other sites and succeed while mobile apps and API clients fail.
Real-world use #
Certificate expiry still causes outages at organisations of every size. The cause is invariably an automation failure nobody was alerted to.
The renewal-but-no-reload failure is common. The certificate on disk is current, the running server still holds the old one in memory, and the site fails at the original expiry date despite a successful renewal.
Behind a CDN, TLS usually terminates at the edge. Renewal then happens there, and the origin may use its own certificate or plain HTTP over a private network — knowing which applies avoids a confusing debugging session.
Mixed content appears during migrations, when an HTTPS page still references an HTTP asset. Browsers block it, and the symptom is a broken page rather than a security warning.
Testing with an external analyser after any TLS change is worthwhile: it catches missing intermediates, weak configuration and protocol issues in seconds.
Common mistakes #
- No expiry monitoring, so a silent renewal failure becomes an outage.
- Renewing without reloading the server, so the old certificate is still served.
- Serving cert.pem instead of fullchain.pem, breaking non-browser clients.
- Enabling HSTS with includeSubDomains before every subdomain has a certificate.
- Enabling HSTS preload casually — it takes months to reverse.
Practice #
Audit the TLS configuration of a site you run: check the protocol versions, whether the chain is complete, and the expiry date of every domain and subdomain. Add an expiry monitor covering all of them, then plan an HSTS rollout starting at a five-minute max-age.