Why this happens (most common)
- Hard-coded
http://URLs in HTML/CSS/JS (scripts, images, beacons, fonts, analytics). - Reverse proxy/ELB misconfig — app is behind TLS, but your app thinks the request is HTTP because
X-Forwarded-Proto/“trust proxy” isn’t set, so it refuses it. - Health checks/cron/crawlers hitting
http://…. - Mixed-content auto-upgrades missing (no CSP
upgrade-insecure-requests).
Quick triage
- In DevTools → Network, see which request returned 403; check Initiator to find the caller.
- Repo sweep for stray HTTP:
- macOS/Linux:
rg -n "http://(?!localhost)" -g '!node_modules' - Windows:
git grep -n "http://(?!localhost)"
- macOS/Linux:
- Edge logs: look for 403s and the
Host/User-Agentto spot health checks or bots. - At the proxy: confirm requests carry
X-Forwarded-Proto: https.
Fixes that stick
1) Redirect and forward correctly
Nginx
server { listen 80; server_name _; return 301 https://$host$request_uri; }
# when proxying to app:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Apache
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
CloudFront/Cloudflare
- Viewer Protocol Policy: Redirect HTTP to HTTPS
- Origin Protocol Policy: HTTPS only
- Enable HSTS (see below).
Kubernetes NGINX Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
2) Make your app “proxy-aware”
Express
app.enable('trust proxy');
app.use((req,res,next)=> req.secure ? next()
: res.redirect(301, 'https://' + req.headers.host + req.originalUrl));
Django
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Rails
config.force_ssl = true
3) Stop emitting HTTP from the codebase
- Replace all
http://withhttps://(analytics, fonts, images, API calls). - Add CSP to auto-upgrade stragglers:
Content-Security-Policy: upgrade-insecure-requests
- Add a CI guard so this never regresses:
- Fail build if
http://(non-localhost) appears in web assets. - ESLint rule: avoid http URLs in strings (or a custom grep step).
- Fail build if
4) Security headers (enforce contract)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Referrer-Policy: no-referrer-when-downgrade
HSTS ensures browsers won’t even try HTTP next time.
5) Clean up non-browser callers
- Update health checks and any internal jobs to HTTPS.
- If a third-party beacon/analytics script was
http://, change tohttps://. (Many gov/analytics endpoints deliberately 403 plain HTTP beacons.)
Bottom line: treat “HTTPS only” as a hard contract. Redirect at the edge, forward proto headers correctly, make the app proxy-aware, purge http:// from the codebase, and lock it with HSTS + CI. If you want, paste the 403 request URL + initiator and I’ll point to the exact culprit.