Image de couverture : Sécurité HTTP : automatiser l'audit des headers dans votre pipeline CI/CD
tech

Sécurité HTTP : automatiser l'audit des headers dans votre pipeline CI/CD

12 April 2026
2 min de lecture
43 vues
Sébastien Muler

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é par X-Frame-Options ou Content-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-Policy strict.
  • 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.

Partager cet article