Fix duplicate security headers and CORS across nginx/Kong/backend
Problem
Security headers and CORS are set at three layers in the reverse proxy chain, causing duplicates and inconsistencies:
Browser → nginx (TLS, security headers, CORS) → Kong (CORS plugin, response-transformer) → Backend (helmet, cors, securityHeaders)
Observed symptoms
-
ERR_INVALID_CHUNKED_ENCODING reported by another developer after local deployment (caused by
security-headers.confauto-included at http level AND explicitly in location blocks) -
Duplicate headers visible in curl responses (e.g.,
X-Content-Type-Optionsappears twice) -
Conflicting header values: Kong sends
Access-Control-Allow-Origin: *while nginx sendsAccess-Control-Allow-Origin: https://localhost -
Information leakage: Kong's
response-transformeraddsX-Powered-By: Kong
Current duplication map
| Header | nginx | Kong | Backend |
|---|---|---|---|
| X-Content-Type-Options | security-headers.inc | - | securityHeaders.js |
| X-Frame-Options | DENY | - | SAMEORIGIN |
| X-XSS-Protection | security-headers.inc | - | securityHeaders.js + helmet |
| Strict-Transport-Security | security-headers.inc | - | securityHeaders.js + helmet |
| Referrer-Policy | security-headers.inc | - | securityHeaders.js |
| Content-Security-Policy | per-location | - | helmet |
| Access-Control-Allow-Origin | /api/ block | cors plugin (*) | cors middleware |
| X-Powered-By: Kong | proxy_hide_header | response-transformer | app.disable |
Root Cause
-
nginx auto-includes all
.conffiles from/etc/nginx/conf.d/at the http level.security-headers.confwas included there AND explicitly in each location block → double headers. -
Kong CORS plugin adds
Access-Control-Allow-Origin: *on top of nginx's specific origin. -
Kong response-transformer adds
X-Powered-By: Kongwhich nginx then tries to strip. - Backend securityHeaders.js sets headers that nginx already sets — wasted work and log noise.
Proposed Solution: nginx as Single Authoritative Source
1. nginx: Add proxy_hide_header at server level
Strip ALL upstream security/CORS headers so nginx is the only source the client sees:
# At server level (inherited by all location blocks)
proxy_hide_header X-Content-Type-Options;
proxy_hide_header X-Frame-Options;
proxy_hide_header X-XSS-Protection;
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Referrer-Policy;
proxy_hide_header Content-Security-Policy;
proxy_hide_header Permissions-Policy;
proxy_hide_header Feature-Policy;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
proxy_hide_header Access-Control-Max-Age;
proxy_hide_header Access-Control-Expose-Headers;
proxy_hide_header X-Kong-Proxy-Latency;
2. Kong: Disable CORS and response-transformer plugins
In kong_config.json:
- Global CORS plugin →
enabled: false - Per-route CORS plugin on chat-history-route →
enabled: false - Response-transformer plugin →
enabled: false
Keep: rate-limiting, prometheus, file-log, request-transformer (internal routing header).
3. Backend: Make securityHeaders.js a no-op
Replace components/shared/lib/security-headers.js body with a pass-through since nginx handles all these headers at the edge. Keep helmet and cors as defense-in-depth (their headers are stripped by nginx).
Files to modify
| File | Change |
|---|---|
api-gateway-solution/nginx/conf/default.conf.template |
Add proxy_hide_header directives |
api-gateway-solution/new-config/kong_config.json |
Disable CORS (2x) and response-transformer |
components/shared/lib/security-headers.js |
Replace with no-op |
Verification
curl -skD - https://localhost/ | grep -c "X-Content-Type-Options" # Expected: 1
curl -skD - https://localhost/api/health | grep -c "Access-Control-Allow-Origin" # Expected: 1
curl -skD - https://localhost/api/health | grep -i "kong" # Expected: 0
Risk Assessment
-
proxy_hide_header: Zero risk — silently ignored if header doesn't exist - Disable Kong CORS: Low risk — nginx already handles CORS for all routes
- Disable Kong response-transformer: Zero risk — headers are information leakage
- Backend securityHeaders no-op: Zero risk — nginx overrides at edge anyway