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.
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
- Ouvrez une PR ajoutant une règle valide → le job
lint-and-testdoit passer. - Ouvrez une PR ajoutant une règle « any-any » → le job doit échouer et bloquer la fusion.
- Fusionnez la PR valide → le job
deployapplique 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.