Exercice 05 : Capstone - Architecture DevSecOps Avancée
🎯 Objectifs
À la fin de cet exercice, vous serez capable de :
- ✅ Configurer Vault Agent Injector pour les secrets Kubernetes
- ✅ Écrire une politique OPA qui refuse les images non signées avec cosign
- ✅ Construire un pipeline end-to-end : build → sign → verify → deploy
- ✅ Réaliser un threat model STRIDE simplifié sur votre architecture
- ✅ Rédiger un runbook de réponse à incident pour une CVE critique
Durée estimée : 2h
Difficulté : ⭐⭐⭐⭐⭐ (Expert)
Prérequis : Exercices 01 à 04 complétés, cluster Kubernetes avec Gatekeeper
📖 Contexte
Vous êtes architecte DevSecOps. Une startup vous demande de concevoir et implémenter leur infrastructure de sécurité de zéro. L'objectif : "Secure by Default" - aucune configuration non sécurisée ne peut atteindre la production.
📋 Énoncé
Construisez une infrastructure DevSecOps complète en 5 modules interconnectés.
🧭 Déroulement de l'exercice
Module 1 : Vault comme source unique de vérité pour les secrets
Étape 1 : Configurer Vault pour Kubernetes
# Prérequis : Vault accessible depuis le cluster K8s
# Pour cet exercice, Vault tourne dans le cluster lui-même
# Installer Vault avec Helm
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault \
--namespace vault \
--create-namespace \
--set "server.dev.enabled=true" \
--set "injector.enabled=true"
kubectl wait --for=condition=ready pod/vault-0 -n vault --timeout=60sÉtape 2 : Configurer l'authentification Kubernetes
# Exec dans le pod Vault
kubectl exec -n vault vault-0 -- /bin/sh << 'EOF'
# Activer l'auth Kubernetes
vault auth enable kubernetes
# Configurer avec les infos du cluster
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
# Créer un secret de base de données
vault secrets enable -path=secret kv-v2
vault kv put secret/production/database \
username=app_user \
password="$(openssl rand -base64 24)"
# Créer une politique d'accès
vault policy write app-policy - << 'POLICY'
path "secret/data/production/*" {
capabilities = ["read"]
}
POLICY
# Créer un rôle Kubernetes
vault write auth/kubernetes/role/app-role \
bound_service_account_names=app-sa \
bound_service_account_namespaces=production \
policies=app-policy \
ttl=1h
EOFÉtape 3 : Déploiement avec injection automatique des secrets
# app-deployment.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: production
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: secure-app
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: secure-app
template:
metadata:
labels:
app: secure-app
annotations:
# Annotations Vault Agent Injector
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "app-role"
vault.hashicorp.com/agent-inject-secret-database: "secret/data/production/database"
vault.hashicorp.com/agent-inject-template-database: |
{{- with secret "secret/data/production/database" -}}
DB_USERNAME={{ .Data.data.username }}
DB_PASSWORD={{ .Data.data.password }}
{{- end }}
spec:
serviceAccountName: app-sa
containers:
- name: app
image: ghcr.io/votre-org/secure-app:latest
command: ["/bin/sh", "-c"]
args:
- |
# Vault injecte les secrets dans /vault/secrets/
source /vault/secrets/database
echo "Connecté en tant que: $DB_USERNAME"
# Lancer l'application réelle ici
sleep infinity
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mikubectl apply -f app-deployment.yaml
# Vérifier que les secrets sont injectés
kubectl exec -n production deployment/secure-app -c app -- cat /vault/secrets/databaseModule 2 : OPA - Interdire les images non signées
Créer une ConstraintTemplate qui vérifie les signatures cosign
cat > constraint-signed-images.yaml << 'EOF'
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiresignedimages
spec:
crd:
spec:
names:
kind: K8sRequireSignedImages
validation:
openAPIV3Schema:
properties:
allowedIssuers:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiresignedimages
# Note : La vérification cosign complète nécessite un webhook externe
# (ex: Connaisseur ou Sigstore Policy Controller)
# Cette règle vérifie le format de l'image (pas de :latest)
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
# Interdire l'utilisation du tag :latest
endswith(container.image, ":latest")
msg := sprintf(
"Container '%v' : l'image '%v' utilise le tag ':latest'. Utilisez un tag immuable (SHA ou version).",
[container.name, container.image]
)
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
# Vérifier qu'un digest SHA est utilisé OU un tag de version
not contains(container.image, "@sha256:")
not regex.match(`:.+\..+$|:v?\d+\.\d+`, container.image)
not endswith(container.image, ":latest") # Déjà géré ci-dessus
msg := sprintf(
"Container '%v' : l'image '%v' doit utiliser un digest SHA256 ou un tag de version sémantique.",
[container.name, container.image]
)
}
EOF
cat > constraint-apply-signed.yaml << 'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireSignedImages
metadata:
name: require-signed-images
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
namespaces: ["production"]
enforcementAction: deny
EOF
kubectl apply -f constraint-signed-images.yaml constraint-apply-signed.yamlModule 3 : Pipeline CI/CD avec signature et vérification
# .github/workflows/secure-pipeline.yml
name: Secure Build and Deploy
on:
push:
tags: ['v*']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-sign:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # Pour cosign keyless
outputs:
image-digest: ${{ steps.build.outputs.digest }}
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- uses: anchore/sbom-action/download-syft@v0
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Build avec tag immuable (SHA)
- name: Build et push
id: build
uses: docker/build-push-action@v6
with:
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
# Scanner AVANT de signer (ne pas signer une image vulnérable)
- name: Scanner l'image (bloquant)
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
exit-code: '1'
severity: 'CRITICAL'
ignore-unfixed: true
# Générer SBOM
- uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
output-file: sbom.spdx.json
# Signer l'image (keyless via GitHub OIDC)
- name: Signer l'image
run: cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
# Attacher SBOM
- name: Attacher SBOM
run: |
cosign attest --yes \
--type spdxjson \
--predicate sbom.spdx.json \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
deploy:
needs: build-and-sign
runs-on: ubuntu-latest
environment: production
steps:
- name: Vérifier la signature avant déploiement
run: |
cosign verify \
--certificate-identity-regexp "https://github.com/${{ github.repository }}/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
${{ needs.build-and-sign.outputs.image-ref }}
echo "✅ Signature vérifiée - déploiement autorisé"
- name: Déployer avec le digest immuable
run: |
# Utiliser le digest SHA, jamais un tag mutable
kubectl set image deployment/secure-app \
app=${{ needs.build-and-sign.outputs.image-ref }} \
-n productionModule 4 : Threat Model STRIDE
Réalisez une analyse STRIDE simplifiée de votre architecture :
# Threat Model - Architecture DevSecOps
## Application : API Backend + Base de données
### Diagramme de flux de donnéesUser → [HTTPS] → API Gateway → [internal] → App Container → [PostgreSQL] → DB
↑
Vault Agent Injector
## Analyse STRIDE
| Menace | Composant | Exemple d'attaque | Mitigation |
|--------|-----------|-------------------|------------|
| **S**poofing | API | Usurpation d'identité JWT | OIDC + rotation des clés |
| **T**ampering | Image Docker | Modification après build | cosign + vérification au déploiement |
| **R**epudiation | Actions CI/CD | "Je n'ai pas fait ce commit" | Rekor (journal immuable) |
| **I**nformation Disclosure | Secrets | Clé API dans les logs | Vault + pas d'env vars littérales |
| **D**enial of Service | API | Surcharge des pods | Resource limits + HPA |
| **E**levation of Privilege | Pods | Container escape → root | runAsNonRoot + RBAC |
## Risques résiduels
- Compromission du compte GitHub (MFA obligatoire)
- Vault dev mode en prod (configurer HA avec TLS)Module 5 : Runbook de réponse à incident - CVE critique
# Runbook : CVE critique dans une dépendance
## Déclencheur
- Alerte Dependabot CVE CVSS >= 9.0
- Alert Trivy CRITICAL dans une image en production
## Étapes (objectif : < 2h de résolution)
### T+0 : Détection
1. Vérifier la portée : `trivy image --severity CRITICAL mon-image:prod`
2. Identifier si la CVE est exploitable dans notre contexte
3. Ouvrir un incident Slack/ticket avec priorité P1
### T+15min : Containment
1. Si exploitable : scaler à 0 le déploiement (`kubectl scale --replicas=0`)
2. Sinon : continuer en observation
### T+30min : Remediation
1. Mettre à jour le package dans `package.json` / `requirements.txt`
2. Rebuilder l'image avec la version patchée
3. Trivy scan de la nouvelle image (0 CRITICAL requis)
4. Déploiement via pipeline CI normal
### T+90min : Vérification
1. `trivy image mon-image:nouvelle-version` → 0 CRITICAL
2. Tests de non-régression passent
3. Déploiement en production validé
### T+120min : Post-mortem
1. Documenter la CVE, l'impact, la timeline
2. Mise à jour du runbook si nécessaire# Sauvegarder le runbook
mkdir -p docs/runbooks
# Copier le contenu du runbook dans docs/runbooks/cve-critique.md
git add .
git commit -m "security: architecture DevSecOps complète - Vault, cosign, OPA, runbook CVE"
git push✅ Vérification du résultat
- Vault Agent Injector injecte les secrets dans le pod
/vault/secrets/ - Le pipeline CI refuse de signer une image avec CRITICAL
- Le pipeline vérifie la signature avant le déploiement
- OPA Gatekeeper refuse les images avec tag
:latesten production - Le threat model STRIDE couvre les 6 catégories de menaces
- Le runbook CVE définit des étapes actionnables avec une timeline
💡 À retenir
L'architecture DevSecOps avancée forme une boucle de confiance :
Code → SAST (CodeQL) → Build → SBOM → Scan (Trivy) → Signe (cosign)
↓
Production ← Vault (secrets) ← RBAC ← OPA ← Vérifie signatureChaque maillon valide le suivant. Si un maillon est compromis, les autres l'arrêtent.
Principe fondamental : La sécurité ne doit pas être une case à cocher à la fin du développement. Elle s'intègre à chaque étape, de façon automatique et non optionnelle.
✨ Solution Complète
La solution complète est définie dans les modules 1 à 5 ci-dessus. La clé est l'intégration de chaque composant dans la pipeline CI/CD :
- Vault : secrets injectés au runtime, jamais dans les manifestes
- cosign : signature cryptographique dans la CI, vérification au déploiement
- Trivy : scan bloquant avant la signature
- OPA/Gatekeeper : admission control Kubernetes
- Runbook : processus défini avant qu'une crise arrive