TP 15 : CI/CD & Firewall-as-Code (FaC) avec FortiGate

Module : 5 – NetSecDevOps et Sécurité Automatisée Durée indicative : 2h30 Prérequis : Modules 1 et 2 (Git, FortiGate REST API), un dépôt GitHub, un runner CI accessible

Objectifs

  • Décrire des règles de pare-feu FortiGate sous forme de fichiers versionnés.
  • Construire un pipeline CI/CD : lint, tests de non-régression, déploiement.
  • Appliquer le principe de shift-left à la gestion d’un pare-feu.

Contexte

Toute nouvelle règle de pare-feu passe désormais par une Pull Request (Module 1), validée automatiquement par un pipeline avant d’être appliquée au FortiGate du TP 6.

Pipeline Firewall-as-Code de bout en bout Pipeline Firewall-as-Code de bout en bout De la branche feature au pare-feu : PR, CI (lint + tests), gate, merge, puis déploiement automatisé.

Étape 1 – Décrire les règles en YAML

Dans le dépôt du TP 6, créez firewall/rules.yaml :

rules:
  - name: allow-web-to-srv01
    srcintf: port2
    dstintf: port1
    srcaddr: LAN
    dstaddr: SRV-WEB-01
    service: HTTPS
    action: accept

Étape 2 – Script de lint

scripts/lint_rules.py : vérifie que chaque règle possède les champs obligatoires et qu’aucune règle n’utilise srcaddr: all avec service: ALL (règle « any-any » interdite) :

import sys, yaml

with open("firewall/rules.yaml") as f:
    rules = yaml.safe_load(f)["rules"]

errors = []
for r in rules:
    if r.get("srcaddr") == "all" and r.get("service") == "ALL":
        errors.append(f"Règle any-any interdite : {r['name']}")

if errors:
    print("\n".join(errors))
    sys.exit(1)
print("Lint OK")

Étape 3 – Test de non-régression

scripts/test_rules.py : à partir d’une liste de flux qui doivent rester bloqués (ex. LAN -> Internet:22), vérifie qu’aucune règle du fichier ne les autoriserait par erreur.

Étape 4 – Script de déploiement

scripts/deploy_rules.py : lit rules.yaml, compare aux règles existantes via l’API FortiOS (GET /firewall/policy), et crée/modifie uniquement les règles en écart (idempotence, comme au Module 4).

Étape 5 – Pipeline CI/CD (GitHub Actions)

.github/workflows/firewall.yml :

name: Firewall-as-Code
on:
  pull_request:
    paths: ["firewall/**"]
  push:
    branches: [main]
    paths: ["firewall/**"]

jobs:
  lint-and-test:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: pip install pyyaml requests
      - run: python scripts/lint_rules.py
      - run: python scripts/test_rules.py

  deploy:
    needs: lint-and-test
    if: github.ref == 'refs/heads/main'
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: pip install pyyaml requests
      - run: python scripts/deploy_rules.py
        env:
          FORTIGATE_TOKEN: ${{ secrets.FORTIGATE_TOKEN }}

Rappel Module 1 : protégez main avec required status checks sur le job lint-and-test, pour interdire la fusion d’une PR qui ne passe pas les contrôles.

Étape 6 – Test de bout en bout

  1. Ouvrez une PR ajoutant une règle valide → le job lint-and-test doit passer.
  2. Ouvrez une PR ajoutant une règle « any-any » → le job doit échouer et bloquer la fusion.
  3. Fusionnez la PR valide → le job deploy applique la règle sur le FortiGate (vérifiez dans l’interface).

Rendu attendu

  • Le dépôt avec firewall/rules.yaml, les scripts et le workflow CI/CD.
  • Deux captures de pipeline : un échec (règle any-any) et un succès (déploiement).
  • Une règle visible sur le FortiGate, créée uniquement via le pipeline.

Points de vérification

  • Une règle any-any est rejetée automatiquement par le lint.
  • Le déploiement n’agit que sur main, après succès des tests.
  • Le token FortiGate est stocké en secret CI, jamais dans le code.