Skip to content

fix: serve security headers from app nginx instead of ingress custom-headers#4186

Open
devthejo wants to merge 3 commits into
masterfrom
fix/frontend-security-headers-in-nginx
Open

fix: serve security headers from app nginx instead of ingress custom-headers#4186
devthejo wants to merge 3 commits into
masterfrom
fix/frontend-security-headers-in-nginx

Conversation

@devthejo

Copy link
Copy Markdown
Member

Problème

La preprod (domifa-preprod.ovh.fabrique.social.gouv.fr/manage) renvoyait 503 no available server alors que les pods étaient sains et le déploiement « passait ».

Cause : l'edge ingress du cluster legacy ovh-dev a été basculé de nginx-ingress vers Traefik. Le provider de compat kubernetesingressnginx de Traefik n'honore l'annotation nginx.ingress.kubernetes.io/custom-headers que pour les en-têtes présents dans son allowlist globalAllowedResponseHeaders (vide par défaut). Tout en-tête hors allowlist (CSP, Referrer-Policy, Cache-Control…) fait échouer la résolution de la route entière → 503, alors que frontend, portail-admins et portail-usagers posaient leurs en-têtes de sécu via cette annotation.

Changement

On déplace les en-têtes de sécurité dans la conf nginx de chaque app (l'image nginx4spa permet de surcharger /etc/nginx/nginx.conf) et on retire les blocs ingress.customHeaders du .kontinuous/values.yaml.

Résultat : les apps émettent elles-mêmes leurs en-têtes de sécu, indépendamment de l'ingress controller placé devant. Plus de couplage à un comportement spécifique de l'edge.

  • packages/{frontend,portail-admins,portail-usagers}/nginx.conf : conf basée sur celle de l'image nginx4spa:8.2.4 + en-têtes de sécu (CSP par app, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Referrer-Policy, Cache-Control, + X-Robots-Tag pour admins). En-têtes en always (envoyés aussi sur les réponses d'erreur).
  • Dockerfiles : COPY ./packages/<app>/nginx.conf /etc/nginx/nginx.conf.
  • .kontinuous/values.yaml : suppression des ingress.customHeaders.

Les en-têtes (noms et valeurs, CSP incluse) sont repris à l'identique de ce qui était défini dans values.yaml.

Validation

nginx -t OK sur les 3 confs contre l'image réelle ghcr.io/socialgouv/docker/nginx4spa:8.2.4.

Note plateforme : un correctif edge (ajout de globalAllowedResponseHeaders côté Traefik) est remonté en parallèle aux admins du cluster ; cette PR rend l'app résiliente quel que soit l'edge.

🤖 Generated with Claude Code

…headers

The frontend, portail-admins and portail-usagers apps relied on the
`nginx.ingress.kubernetes.io/custom-headers` annotation to set their
security headers (CSP, X-Frame-Options, Referrer-Policy, ...).

This couples the apps to the ingress controller honouring that annotation.
On the legacy cluster the edge controller was swapped to Traefik, whose
nginx-compat provider rejects custom-headers whose names are not in its
`globalAllowedResponseHeaders` allowlist — dropping the whole route and
returning `503 no available server` even though the pods were healthy.

Move the headers into each app's own nginx config (the nginx4spa image
supports overriding `/etc/nginx/nginx.conf`) and drop the ingress
customHeaders blocks. The apps now emit their security headers themselves,
independently of the ingress controller in front of them.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@revu-bot
revu-bot Bot requested a review from revu-bot June 25, 2026 08:08

@revu-bot revu-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR moves security headers from Kubernetes ingress customHeaders annotations into per-app nginx configurations, fixing a 503 regression caused by Traefik's kubernetesingressnginx compatibility provider not honouring arbitrary response headers. The approach is sound and the fix is correct.

Overall quality: Good. The configs are validated, headers are faithfully ported, and the always flag ensures headers are sent on error responses too. A few issues worth addressing:

File Lines Severity Issue
All 3 nginx.conf 28–35 IMPORTANT add_header in server block is silently overridden by any location-level add_header — security headers will be dropped on static assets
All 3 nginx.conf 14 MINOR error_log declared twice (http + top-level)
packages/frontend/nginx.conf 38 IMPORTANT Missing Permissions-Policy header — CSP alone doesn't restrict browser feature APIs
All 3 nginx.conf 19 MINOR keepalive_timeout 3000 is 50 minutes — almost certainly a copy-paste error (should be 65s)

The most impactful issue is the nginx add_header inheritance trap: because location / and location /live both exist without their own add_header directives, the server-block headers will be inherited — but if the upstream nginx4spa base image's included configs add any add_header inside a location block, all server-block headers are silently dropped for that location. This is a well-known nginx footgun. The safe fix is to move all add_header directives into every location block, or use proxy_hide_header + add_header at the http level (not applicable here), or — best for this use case — use a dedicated include snippet.

Comment thread packages/frontend/nginx.conf Outdated
Comment thread packages/nginx.conf
Comment thread packages/portail-admins/nginx.conf Outdated
Comment thread packages/portail-usagers/nginx.conf Outdated
Comment thread packages/nginx.conf
@codecov-commenter

codecov-commenter commented Jun 25, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.31%. Comparing base (ad802b2) to head (162a5f4).
⚠️ Report is 1 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##           master    #4186    +/-   ##
========================================
  Coverage   61.31%   61.31%            
========================================
  Files        1030     1030            
  Lines       20860    20860            
  Branches     4076     4076            
========================================
  Hits        12791    12791            
- Misses       7783     7986   +203     
+ Partials      286       83   -203     

see 77 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8a4c12d...162a5f4. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@devthejo
devthejo marked this pull request as draft June 25, 2026 08:14
nginx does not inherit `add_header` directives into a location block that
defines its own. To keep the security headers robust against future location
blocks (and any header set by an included/base-image snippet), extract them
into a per-app `security-headers.conf` and `include` it both at server level
and inside every location block, keeping a single source of truth per app.

Addresses review feedback on the add_header inheritance trap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devthejo

Copy link
Copy Markdown
Member Author

Vérification e2e sur env de review ✅

Env de review déployé depuis cette branche (cluster ovh-dev, qui présente exactement le bug : edge Traefik, aucun IngressRoute/Middleware, annotation custom-headers en échec). Les 3 apps répondent 200 et servent leurs en-têtes de sécu depuis leur propre nginx (server: nginx), sans dépendre de l'edge :

App URL review code en-têtes
frontend https://domifa-fix-frontend-security-headers-in-nginx-lo3hjcn0.ovh.fabrique.social.gouv.fr/ (+ /manage) 200 CSP, X-Frame, Referrer, Cache-Control, X-Content-Type, X-XSS:0
portail-admins https://admin-domifa-...lo3hjcn0... 200 CSP strict + X-Robots-Tag
portail-usagers https://mon-domifa-...lo3hjcn0... 200 CSP

Headers émis une seule fois (pas de doublon), validé aussi par nginx -t sur l'image réelle nginx4spa:8.2.4.

⚠️ Le job Deploy Review est rouge, mais uniquement à cause de backend-cron (CrashLoopBackOff : la migration d'init DB des PR-envs loadAndExecuteDump échoue sur une syntax error du dump SQL). C'est pré-existant et sans rapport avec ce changement (front/nginx). Les pods frontend/portail-admins/portail-usagers sont Running et servent correctement.

@devthejo
devthejo marked this pull request as ready for review June 25, 2026 09:08

@revu-bot revu-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR correctly solves a real production incident (503s caused by Traefik's incompatibility with nginx.ingress.kubernetes.io/custom-headers) by moving security headers into each app's nginx configuration. The approach is sound and the fix is well-motivated.

Key observations:

  • The three nginx.conf files are byte-for-byte identical — only the security-headers.conf files differ per app. This creates a maintainability burden.
  • The include at the server {} level is redundant given the per-location includes, and the comment explaining this is slightly misleading.
  • The Cache-Control: no-store header is applied to all routes including static assets (/live, /ready, /50x.html), which is overly broad but not a regression from the previous behaviour.
  • Missing Permissions-Policy header (not a regression, but worth noting).
  • The keepalive_timeout 3000 value (seconds, not ms) is very high — likely a copy from the upstream image, not introduced here.
File Lines Severity Issue
packages/*/nginx.conf (all 3) 1–76 IMPORTANT Identical files — DRY violation, future drift risk
packages/*/nginx.conf 31 MINOR Server-level include is redundant when all locations also include
packages/frontend/security-headers.conf 5 IMPORTANT Cache-Control: no-store applied to health/error endpoints

Comment thread packages/nginx.conf
Comment thread packages/nginx.conf
Comment thread packages/frontend/security-headers.conf
The frontend, portail-admins and portail-usagers nginx configs were
byte-for-byte identical (only the per-app security-headers.conf differs).
Collapse them into a single packages/nginx.conf copied by each Dockerfile,
keeping the per-app security-headers.conf, to avoid three-way drift.

Also clarify the server-level security-headers include is only a fallback
for requests matching no location.

Addresses review feedback (DRY across the three nginx.conf).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants