Enforcing HTTPS
Chapter 2 — Enforcing HTTPS
Installing a certificate (Chapter 1) makes HTTPS available — but it doesn't stop users arriving over plain HTTP. A visitor who types osztromok.com without the https://, follows an old bookmark, or clicks a link from an older site will make their first request over HTTP. That first request is unencrypted, and could include cookies or credentials. Enforcing HTTPS means redirecting HTTP traffic to HTTPS before any content is served — and then using HSTS to tell browsers to never attempt HTTP again.
Why a Certificate Alone Is Not Enough
The redirect is a 301 Permanent Redirect, not a 302 Temporary. The permanent status tells browsers and search engines that HTTP is never the right choice for this domain. Search engines consolidate SEO signals to the HTTPS version, and browsers cache the redirect so repeat visitors skip the HTTP round-trip entirely.
Three Ways to Redirect HTTP to HTTPS in Apache
- Uses mod_alias (built in)
- Redirects all URLs under
/ - Preserves path and query string
- Best for: most cases — this is the recommended approach
- Requires mod_rewrite (usually already enabled)
- Conditional redirects possible
- Handles X-Forwarded-Proto checks
- Best for: complex redirect logic or Cloudflare Flexible SSL mode
- Regex-based URL matching
- Good for partial enforcement
- Less common in practice
- Best for: migrating specific paths to HTTPS before full enforcement
- Cloudflare dashboard → SSL/TLS → Edge Certificates
- Eliminates redirect loop risk entirely
- Works in Flexible, Full, or Full Strict mode
- Best for: simplest setup when all traffic goes through Cloudflare
Method 1 — Redirect directive
Method 2 — mod_rewrite (for Flexible SSL or conditional logic)
Redirect permanent works without any loop risk. If you're on Flexible SSL mode (Cloudflare sends HTTP to Apache), use Method 2 with the X-Forwarded-Proto check, or better yet — use Cloudflare's "Always Use HTTPS" edge setting instead.
Cloudflare "Always Use HTTPS"
This is the cleanest solution if all traffic goes through Cloudflare. The redirect happens at Cloudflare's edge — the HTTP request never touches your server. Enable it in the Cloudflare dashboard:
SSL/TLS → Edge Certificates → Always Use HTTPS → toggle On
When enabled, a visitor who requests http://osztromok.com gets a 301 redirect to https://osztromok.com from Cloudflare's servers directly. Your Apache config doesn't need a redirect at all — the *:80 VirtualHost can simply return a 404 or be left as-is.
The Redirect Loop Trap
This is the most common mistake when adding HTTPS redirects behind Cloudflare, and it produces the browser error ERR_TOO_MANY_REDIRECTS.
HSTS — HTTP Strict Transport Security
A redirect upgrades the current HTTP request to HTTPS. HSTS goes further: it tells the browser to never attempt HTTP for this domain again — for a specified duration. The browser enforces HTTPS locally, before making any network request, so there's no HTTP exposure even on the first visit after the cached instruction is set.
This eliminates the "HTTPS downgrade attack" window — the brief moment during the first HTTP request before the redirect fires, when an attacker on the same network could intercept or modify the traffic.
HSTS Header Parameters
max-age=86400 = 1 day) while testing, then increase to a year once you're confident HTTPS is solid. Once set to a long duration, reducing it requires waiting for all cached instructions to expire.blog.osztromok.com, api.osztromok.com, etc. Only add this when every subdomain has a valid HTTPS certificate. If any subdomain has no cert, adding this will make that subdomain inaccessible in browsers that have cached the HSTS instruction.Configuring HSTS in Apache
HSTS is set via the Strict-Transport-Security response header, added by mod_headers. It must be in the *:443 VirtualHost only — browsers ignore this header on plain HTTP responses (rightly so, since an attacker on HTTP could strip it).
Progressively hardening HSTS
Don't jump straight to a 1-year HSTS with preload — if anything breaks, you're locked in for a very long time. Work through this progression over weeks:
| Stage | Header value | When to move to next stage |
|---|---|---|
| Testing | max-age=86400 (1 day) | HTTPS stable, no cert issues after 1 week |
| Moderate | max-age=604800 (1 week) | All subdomains have certs; no issues after 2 weeks |
| Production | max-age=31536000; includeSubDomains | Confident everything will stay on HTTPS long-term |
| Preload-ready | max-age=31536000; includeSubDomains; preload | Submit to hstspreload.org — permanent commitment |
HSTS Preload List
The HSTS preload list is a database maintained by Google (and used by Chrome, Firefox, Safari, Edge) that hardcodes domains as HTTPS-only directly in the browser binary. Even a brand new browser that has never visited your site will refuse to make HTTP requests to a preloaded domain.
- Removal takes 6–12 months minimum — browsers don't update daily, and cached binaries persist.
- Every subdomain of osztromok.com must serve HTTPS — no exceptions. Any subdomain without a valid cert becomes completely inaccessible in browsers that have cached the preload entry.
- You cannot run anything over plain HTTP on the domain — ever. Not even temporary test pages, admin panels, or internal tools.
- If your cert expires and renewal fails, your entire domain goes dark until it's fixed — browsers won't even show a "proceed anyway" option.
Complete Setup Walkthrough
/etc/apache2/sites-available/osztromok.com.conf. Replace the *:80 block content with:
<VirtualHost *:443> block, add:
includeSubDomains when all your subdomains have valid HTTPS certificates. Reload Apache after the change.
chrome://net-internals/#hsts (Chrome) and delete the entry manually. This is by design.X-Forwarded-Proto — Reading the Real Protocol
When traffic passes through a proxy (Cloudflare, a load balancer, another reverse proxy), the proxy adds an X-Forwarded-Proto header to tell the backend what protocol the original client used — even if the proxy changed it en route.
This is why the mod_rewrite method (Method 2) works correctly with Flexible SSL — it checks X-Forwarded-Proto before deciding whether to redirect. The simple Redirect permanent doesn't do this check, which is why it causes loops in Flexible mode.
X-Forwarded-Proto: https header to bypass your redirect if Apache accepts it from arbitrary sources. Behind Cloudflare, this is mitigated because Cloudflare strips or overwrites the header before forwarding. In general, only rely on proxy headers from known, trusted proxies.
Troubleshooting
Redirect permanent sends it back to HTTPS — which Cloudflare again sends as HTTP to Apache. Fix: switch to Full (Strict) SSL mode (Chapter 1) or use mod_rewrite with X-Forwarded-Proto check. (2) Is "Always Use HTTPS" enabled in Cloudflare AND you have an Apache redirect? Disable one. (3) Test without Cloudflare: curl -I -H "Host: osztromok.com" http://localhost — if this also loops, the issue is in Apache alone (check for duplicate redirect rules).
sudo apache2ctl -M | grep headers. If missing, run sudo a2enmod headers && sudo systemctl restart apache2. (2) Header is in the *:80 block instead of *:443 — browsers ignore HSTS on HTTP responses. (3) Apache was reloaded (not restarted) after enabling a module — modules require restart, not just reload. Check: curl -sk https://localhost -D - -o /dev/null | grep -i strict.
Redirect permanent / directive preserves the path — http://osztromok.com/blog/post-1 redirects to https://osztromok.com/blog/post-1. If the path is being dropped, check for a secondary redirect somewhere (Cloudflare Page Rules, a .htaccess RewriteRule, or a duplicate Redirect directive pointing at a specific path). Run curl -sIL http://osztromok.com/blog/post-1 and examine each redirect in the chain.
http:// URLs. The page itself is encrypted but those resources are not, so the browser downgrades the security indicator. Fix: update all asset URLs to use https:// or protocol-relative //. Apache can help with mod_substitute or a blanket Content-Security-Policy upgrade (covered in Chapter 6).
includeSubDomains before the subdomain had a valid HTTPS certificate. The browser is enforcing HTTPS on the subdomain and the cert is missing or invalid. Fix: immediately get a certificate for the subdomain (certbot with -d subdomain.osztromok.com) and configure Apache for HTTPS on that subdomain. To test the fix, clear HSTS for the domain: in Chrome, visit chrome://net-internals/#hsts, enter the subdomain under "Delete domain security policies," then retest.
Quick Reference — Chapter 2
| Command / Check | Purpose |
|---|---|
| curl -I -H "Host: osztromok.com" http://localhost | Test redirect locally — should return 301 with Location: https:// |
| curl -sIL http://osztromok.com | grep -E "HTTP|Location|Strict" | Follow the full redirect chain and check for HSTS in the final response |
| curl -sk https://localhost -D - -o /dev/null | grep -i strict | Verify HSTS header is in the HTTPS response from Apache directly |
| sudo apache2ctl -M | grep headers | Confirm mod_headers is loaded (required for HSTS) |
| sudo a2enmod headers && sudo systemctl restart apache2 | Enable mod_headers — restart required (not just reload) |
| chrome://net-internals/#hsts | Inspect or delete HSTS entries in Chrome — useful for testing |
| HSTS value | Meaning | When to use |
|---|---|---|
| max-age=86400 | Enforce HTTPS for 1 day | Testing phase — easy to recover |
| max-age=604800 | Enforce HTTPS for 1 week | Early production — gaining confidence |
| max-age=31536000 | Enforce HTTPS for 1 year | Stable production — HTTPS confirmed solid |
| ; includeSubDomains | Apply rule to all subdomains | Only when all subdomains have valid certs |
| ; preload | Request browser preload inclusion | After submitting to hstspreload.org — permanent |
| Approach | Best for | Redirect loop risk? |
|---|---|---|
| Cloudflare "Always Use HTTPS" | All traffic through Cloudflare | None — handled at edge before Apache |
| Apache Redirect permanent (Method 1) | Full (Strict) SSL mode | None — Apache receives genuine HTTPS |
| mod_rewrite + X-Forwarded-Proto (Method 2) | Flexible SSL mode or complex logic | None — checks the original protocol first |
| Apache Redirect + Flexible SSL (wrong combo) | Avoid | Yes — ERR_TOO_MANY_REDIRECTS |