Sécurité HTTP : automatiser l'audit des headers dans votre pipeline CI/CD
Inspiré de l'article Why I don't trust my own deployments publié sur DEV.to par Oleksii Antoniuk.
Un backend Laravel bien configuré, un certificat TLS en place, et pourtant... une simple requête curl -I sur votre domaine de production peut révéler des failles béantes. Les security headers HTTP sont le dernier kilomètre de la chaîne de sécurité, et ils sont trop souvent oubliés après un déploiement.
Dans cet article, on ne se contente pas de lister les headers à activer. On construit un garde-fou automatique : un job CI/CD qui teste votre baseline de sécurité et bloque le pipeline si un header critique est absent ou mal configuré.
Pourquoi les security headers sont critiques
Beaucoup de développeurs pensent que HTTPS suffit. Ce n'est pas le cas. Sans les bons headers, votre application reste exposée à :
- Clickjacking : un attaquant embarque votre site dans une
<iframe>invisible pour piéger vos utilisateurs. Paré parX-Frame-OptionsouContent-Security-Policy: frame-ancestors. - MIME sniffing : le navigateur devine le type d'un fichier et exécute un script déguisé en image. Bloqué par
X-Content-Type-Options: nosniff. - XSS : des scripts injectés qui s'exécutent dans le contexte de votre domaine. Atténué par un
Content-Security-Policystrict. - Downgrade attacks : forcer un utilisateur vers HTTP. Évité par
Strict-Transport-Security(HSTS).
La baseline minimale que tout projet MulerTech doit respecter :
| Header | Valeur recommandée |
|---|---|
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
X-Frame-Options |
DENY ou SAMEORIGIN |
X-Content-Type-Options |
nosniff |
Referrer-Policy |
strict-origin-when-cross-origin |
Content-Security-Policy |
Selon votre application |
Middleware Laravel pour appliquer les headers en production
Première étape : s'assurer que Laravel envoie ces headers sur chaque réponse HTTP. Créez un middleware dédié.
php artisan make:middleware SecurityHeaders
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SecurityHeaders
{
private array $headers = [
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains; preload',
'X-Frame-Options' => 'DENY',
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()',
'Content-Security-Policy' => "default-src 'self'; script-src 'self'; object-src 'none'",
];
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
foreach ($this->headers as $header => $value) {
$response->headers->set($header, $value);
}
return $response;
}
}
Enregistrez-le dans bootstrap/app.php (Laravel 11) :
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
})
⚠️ Si votre Nginx ajoute déjà ces headers (voir section suivante), évitez la duplication. Privilégiez un seul point de vérité.
Configuration Nginx
Pour les projets où Nginx est le point d'entrée (reverse proxy devant PHP-FPM ou Docker), ajoutez les headers directement dans le bloc server :
server {
listen 443 ssl;
server_name example.com;
# ... ssl_certificate, ssl_certificate_key, etc.
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'" always;
location / {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Le mot-clé always est essentiel : sans lui, Nginx n'ajoute pas les headers sur les réponses d'erreur (4xx, 5xx).
Job CI/CD : bloquer le pipeline si la baseline n'est pas respectée
C'est là que ça devient vraiment utile. Un script de vérification lancé automatiquement après chaque déploiement, qui échoue proprement si un header manque.
GitHub Actions
# .github/workflows/security-headers.yml
name: Security Headers Audit
on:
deployment_status:
jobs:
audit-headers:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
env:
TARGET_URL: ${{ github.event.deployment_status.target_url }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Audit Security Headers
run: |
chmod +x ./scripts/check-headers.sh
./scripts/check-headers.sh "$TARGET_URL"
GitLab CI
# .gitlab-ci.yml
security-headers-audit:
stage: post-deploy
image: alpine/curl
script:
- apk add --no-cache bash
- chmod +x ./scripts/check-headers.sh
- ./scripts/check-headers.sh "$CI_ENVIRONMENT_URL"
environment:
name: production
only:
- main
Le script check-headers.sh
#!/usr/bin/env bash
set -euo pipefail
URL="${1:-}"
if [[ -z "$URL" ]]; then
echo "Usage: $0 <url>"
exit 1
fi
echo "→ Audit des security headers pour : $URL"
HEADERS=$(curl -sI --max-time 10 "$URL")
FAILED=0
check_header() {
local name="$1"
local pattern="$2"
if echo "$HEADERS" | grep -qi "^${name}:"; then
local value
value=$(echo "$HEADERS" | grep -i "^${name}:" | head -1)
if echo "$value" | grep -qi "$pattern"; then
echo "✅ $name : OK"
else
echo "⚠️ $name : présent mais valeur incorrecte → $value"
FAILED=1
fi
else
echo "❌ $name : ABSENT"
FAILED=1
fi
}
check_header "Strict-Transport-Security" "max-age"
check_header "X-Frame-Options" "DENY\|SAMEORIGIN"
check_header "X-Content-Type-Options" "nosniff"
check_header "Referrer-Policy" "."
check_header "Content-Security-Policy" "."
if [[ "$FAILED" -eq 1 ]]; then
echo ""
echo "Pipeline bloqué : un ou plusieurs security headers sont absents ou incorrects."
exit 1
fi
echo ""
echo "Audit terminé : tous les headers requis sont en place."
exit 0
Si un header manque, le job retourne un code de sortie 1 — GitHub Actions et GitLab CI interprètent cela comme un échec et bloquent le merge ou le déploiement suivant.
Conclusion
Les security headers ne sont pas une option. Ils font partie de la définition de done de chaque déploiement. En les automatisant dans votre pipeline CI/CD, vous transformez une vérification manuelle oubliable en une contrainte technique non négociable.
La stack Laravel + middleware + Nginx couvre l'envoi des headers. Le script check-headers.sh intégré à GitHub Actions ou GitLab CI garantit que la production ne régresse jamais silencieusement.
Prochaine étape : affiner votre Content-Security-Policy avec un mode report-only pour détecter les violations sans casser l'application — mais ça, c'est pour un prochain article.