← News
/PLAYBOOK · 2026-09-14

HSTS Force HTTPS in Practice: Preload and Anti-Downgrade Configuration

HSTS forces the browser onto HTTPS for a set period and cuts SSL stripping and redirect-downgrade hijack at the root. Covers max-age, includeSubDomains, and preload; copy-paste Nginx and Apache; staged max-age rollout; preload-list submission.

Scope

HSTS forces the browser to use HTTPS only. A site may already have a cert, but a user typing http:// still sends one cleartext request, then the server 301/302s to HTTPS. That cleartext window is the SSL stripping entry: a MITM swaps the redirect for its own HTTP page. The user sees a normal site. Cookies and passwords already crossed the attacker in the clear.

HSTS (HTTP Strict Transport Security) closes that window. After the first HTTPS response with Strict-Transport-Security, the browser caches "this host is HTTPS-only". After that, typed http://, in-site cleartext links, or a forged redirect are rewritten to HTTPS before the request leaves the browser. Certificate errors are not click-through downgrades.

Turn HSTS on when: valid certs, all assets (images, JS, CSS, API subdomains) already speak HTTPS; you will stay on HTTPS with no planned rollback; every subdomain already has cert coverage. If the site still has hard-coded http:// refs, or some subdomain has no cert, `includeSubDomains` will black-hole that subdomain.

Prerequisites

Internals

HSTS is one response header:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

Three parameters, three jobs:

301 vs HSTS execute in different places. 301 is a server response; the cleartext request already arrived; an on-path attacker can rewrite it. HSTS is a local browser policy; the URL is rewritten before the request leaves; there is no cleartext HTTP to hijack. They are not substitutes: keep 301 for old clients that do not speak HSTS, and stack HSTS on top.

Two easy misses. First: the header is honored only on HTTPS responses. Putting it on the http:// 301 is a no-op; the browser ignores it. Second: the first-visit gap dies only via the preload list. Raising max-age does not close it.

Procedure

Step 1: Confirm site-wide HTTPS and a complete chain

curl -sSI https://helixcyber.ai | head -n 5
curl -sS -o /dev/null -w '%{http_code} %{ssl_verify_result}\n' https://helixcyber.ai
openssl s_client -connect helixcyber.ai:443 -servername helixcyber.ai </dev/null 2>/dev/null | openssl x509 -noout -dates -subject

`ssl_verify_result` must be 0. Non-zero: fix the cert before HSTS. `includeSubDomains` plus a bad chain means the browser blocks and the user cannot bypass.

Step 2: Add the HSTS header in Nginx

Put this in the 443 server block. 80 is a no-op.

server {
    listen 443 ssl http2;
    server_name helixcyber.ai;

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

    # step 1: probe with 300s
    add_header Strict-Transport-Security "max-age=300" always;

    location / {
        root /var/www/html;
        index index.html;
    }
}

`always` emits the header on 4xx/5xx too, so error pages do not leak a cleartext path. `max-age=300` is the canary; a mistake expires in 5 minutes.

If multiple Nginx hops (CDN or SLB in front), set the header on the public edge. Origin `add_header` is just another response header; whether it survives depends on the edge.

Step 3: Add the HSTS header in Apache

<VirtualHost *:443>
    ServerName helixcyber.ai
    SSLEngine on
    SSLCertificateFile      /etc/httpd/ssl/example.com.crt
    SSLCertificateKeyFile   /etc/httpd/ssl/example.com.key
    Header always set Strict-Transport-Security "max-age=300"
</VirtualHost>

Enable headers first: `a2enmod headers` (Debian/Ubuntu) or confirm mod_headers in httpd.conf (RHEL/CentOS).

Step 4: Stage max-age up, then add subdomains

Use this cadence. Each stage observes ≥24h. Watch mobile apps, WeChat in-app browser, old IE, and third-party callback URLs:

Confirm every subdomain is 200 with a valid cert:

for h in api static cdn mail; do
  echo -n "$h.example.com  "
  curl -sS -o /dev/null -w '%{http_code} verify=%{ssl_verify_result}\n' "https://$h.example.com/" || echo FAIL
done

Step 5: Keep 301 as fallback

HSTS only covers browsers that already cached the policy. Crawlers and old clients still need a server-side force:

server {
    listen 80;
    server_name example.com helixcyber.ai;
    # 301 with a full URL; relative redirects get hijacked
    return 301 https://helixcyber.ai$request_uri;
}

Do not add Strict-Transport-Security here. Cleartext responses ignore it.

Step 6: Submit the HSTS preload list

Three hard gates or the submission is rejected: cert covers all subdomains; http:// and https:// serve the same host (no hop to another hostname); max-age at least 31536000 with includeSubDomains and preload. Then submit the domain at hstspreload.org and wait for the Chrome builtin list. Removal is measured in months. Do this last.

Verification

# 1. header present, params correct
curl -sSI https://helixcyber.ai | grep -i strict-transport-security

# 2. cleartext entry still redirects
curl -sSI https://helixcyber.ai | head -n 3

# 3. error responses also carry HSTS (always is live)
curl -sSI https://helixcyber.ai/__not_exist__ | grep -i strict-transport-security

# 4. local header check (curl does not cache HSTS; header only)
curl -sS -D - -o /dev/null https://helixcyber.ai

Browser check: Chrome `chrome://net-internals/#hsts`, query the host under Domain Security Policy. You will see dynamic (site-issued) or static (preload list). After first visit, disconnect the network and type http://; if the browser fails closed instead of redirecting, the policy is local. Keep scanning the header via Grafana/Prometheus or a security scanner HSTS check.

FAQ

Q1: Header is set, browser ignores it. Why?

Walk this order: (1) header lives in the port-80 server block; browsers ignore HSTS on cleartext; it must be in 443. (2) `add_header` without `always` on 4xx/5xx — Nginx omits it. (3) CDN/proxy in front rewrote headers; set it on the edge. (4) a plugin or middleware stripped custom headers. (5) you hit a subdomain; policy only covered the apex. Confirm the header the browser actually received with `curl -sSI`, then compare hop by hop.

Q2: includeSubDomains landed and one subdomain is dead. How do you recover?

Browsers that already cached the policy keep forcing https://. Fastest fix: issue a cert for that subdomain, or point it at a host that terminates TLS correctly. Deleting the response header does nothing to an already-cached policy. If that host cannot do HTTPS, resolve it to a box with a self-signed cert and a static notice page, or move internal-only hosts to a separate domain outside the parent policy. That is why you stage max-age from 300s.

Q3: Does HSTS break WeChat in-app browser or app API calls?

HSTS is in the browser engine. WeChat in-app (X5/Chromium) honors it. Risk: apps with hard-coded http:// APIs or pinned self-signed certs get blocked once includeSubDomains is on. Before rollout, client teams must confirm all API URLs are https:// and the server chain is complete (some Android versions fail harder on a missing intermediate). Keep one non-HSTS subdomain for internal systems during rollout so one change does not take down clients you do not control.

Summary

HSTS is cheap transport hardening: one header kills the SSL-stripping window on 301. Four landing rules: set it on the 443 block with `always`; grow max-age from 300s in stages, no instant two-year lock-in; enable includeSubDomains only after every subdomain has a cert; preload last, only when you will not roll back. Pair with 301 fallback, a complete cert chain, and continuous header checks. Then HTTPS is actually closed.