Confidentialite¶
Le service CI/CD manipule des données classees Confidentiel : tokens de déploiement, credentials de registre, cles de signature, variables d'environnement contenant des secrets. Ce chapitre detaille les mesures de protection correspondantes, en référence au cadre de Classification et zones de confiance.
Rappel de classification¶
| Donnée | Classification | Justification |
|---|---|---|
| Tokens d'accès au registre (Harbor) | Confidentiel | Permet de pousser des images en production |
| Credentials de déploiement | Confidentiel | Accès au cluster de production |
| Cles SSH de connexion aux dépôts | Confidentiel | Accès au code source et aux manifestes |
| Variables CI contenant des secrets | Confidentiel | Mots de passe, API keys, tokens |
| Cle SOPS (age/GPG) | Confidentiel | Permet de dechiffrer tous les secrets dans Git |
| Logs de pipeline (contenu) | Interne | Peuvent révéler des chemins et des configurations |
| Historique des déploiements | Interne | Révèlent les versions et la fréquence de déploiement |
| Configuration des workflows | Interne | Révèle la structure du pipeline |
Impact d'une compromission
La compromission du service CI/CD permet à un attaquant de :
- Injecter du code malveillant dans les artefacts déployés en production
- Exfiltrer les secrets utilises par les pipelines
- Déployer des images compromises via le registre
- Accéder aux clusters via les credentials de déploiement
- Dechiffrer les secrets Git si la cle SOPS est compromise
Le CI/CD est un vecteur d'attaque privilege (supply chain attack). Les mesures ci-dessous sont obligatoires.
Gestion des secrets de pipeline¶
Regles fondamentales¶
| Regle | Implementation |
|---|---|
| Ne jamais logger un secret | echo "::add-mask::${SECRET}" dans le workflow |
| Ne jamais passer en argument CLI | Utiliser des variables d'environnement ou des fichiers |
| Tokens ephemeres | OIDC token exchange avec Vault, duree de vie < 1h |
| Rotation régulière | 90 jours maximum pour les credentials statiques |
| Principe de moindre privilege | Robot account avec permissions minimales (push only, etc.) |
| Pas de secrets dans le code | Pre-commit hook pour détecter les secrets (gitleaks) |
Injection securisee depuis Vault¶
# Pattern recommande : OIDC token exchange
steps:
- name: Authenticate to Vault via OIDC
run: |
# Le runner obtient un JWT temporaire du serveur Gitea
JWT="${ACTIONS_ID_TOKEN_REQUEST_TOKEN}"
# Echange contre un token Vault ephemere
VAULT_TOKEN=$(vault write -field=token \
auth/jwt/login role=ci-runner jwt="${JWT}")
# Le token Vault expire apres le pipeline
export VAULT_TOKEN
# Recuperer les secrets necessaires
DB_PASS=$(vault kv get -field=password secret/ci/database)
echo "::add-mask::${DB_PASS}"
Detection de secrets dans les logs¶
Configurer le runner pour masquer automatiquement les patterns de secrets :
# Patterns a masquer dans les logs du runner
# config.yaml du act runner
runner:
env_file: /data/.env
# Les variables dans .env sont automatiquement masquees dans les logs
Attention aux commandes debug
Désactiver set -x autour des commandes manipulant des secrets. Un set -x affiche chaque commande avant execution, y compris les valeurs des variables.
Isolation des runners¶
Namespace dedie¶
Les runners doivent s'exécuter dans un namespace dedie avec des ressources limitees :
# namespace-runners.yaml
apiVersion: v1
kind: Namespace
metadata:
name: ci-runners
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
Network policies¶
Les runners ne doivent accéder qu'aux services strictement nécessaires :
# networkpolicy-runners.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: runner-egress
namespace: ci-runners
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
ingress: [] # aucun trafic entrant autorise
egress:
# Acces au serveur Gitea (SCM)
- to:
- namespaceSelector:
matchLabels:
name: gitea
ports:
- port: 443
protocol: TCP
# Acces au registre Harbor
- to:
- namespaceSelector:
matchLabels:
name: harbor
ports:
- port: 443
protocol: TCP
# Acces a Vault (secrets)
- to:
- namespaceSelector:
matchLabels:
name: vault
ports:
- port: 8200
protocol: TCP
# Acces au miroir de paquets (builds)
- to:
- namespaceSelector:
matchLabels:
name: mirrors
ports:
- port: 443
protocol: TCP
# DNS interne
- to:
- namespaceSelector: {}
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
Pas de conteneurs privilegies¶
# pod-security-standard: restricted
# Les runners ne doivent JAMAIS s'executer en mode privilegie
container:
privileged: false # dans la config du act runner
options: |
--security-opt=no-new-privileges
--cap-drop=ALL
--read-only
Conteneurs privilegies = compromission totale
Un conteneur privilegie a accès au kernel de l'hôte. Si un pipeline est compromis et s'execute dans un conteneur privilegie, l'attaquant a un accès root sur le nœud. C'est la première regle a respecter.
Resource limits¶
# Limiter les ressources par runner pour empecher l'epuisement
container:
options: |
--memory=4g
--memory-swap=4g
--cpus=2
--pids-limit=256
--ulimit nofile=1024:1024
Audit trail¶
Exigences d'audit¶
| Événement | Données tracees | Retention |
|---|---|---|
| Pipeline declenche | Qui, quand, quel commit, quelle branche | 1 an |
| Pipeline termine | Résultat (succès/echec), duree, artefacts produits | 1 an |
| Déploiement effectue | Quelle version, quel environnement, par quel pipeline | 2 ans |
| Secret accede | Quel secret, par quel pipeline, quand (via Vault audit) | 2 ans |
| Runner enregistre/supprime | Quel runner, par qui, quand | 1 an |
| Configuration pipeline modifiée | Quel fichier, quel commit, par qui | Git natif |
Gitea Actions : audit natif¶
Gitea conserve l'historique complet des runs dans sa base de données :
# Lister les runs recents avec metadata
curl -s -H "Authorization: token ${GITEA_TOKEN}" \
"https://git.example.com/api/v1/repos/org/myapp/actions/runs?limit=100" \
| jq '.workflow_runs[] | {
id,
event,
status,
conclusion,
actor: .actor.login,
head_sha,
head_branch,
run_started_at,
updated_at
}'
Flux v2 : audit des reconciliations¶
Flux enregistre chaque reconciliation comme un événement Kubernetes :
# Historique des reconciliations d'une Kustomization
flux get kustomization myapp-production
# Evenements Kubernetes detailles
kubectl -n flux-system describe kustomization myapp-production
# Evenements de tous les objets Flux
kubectl -n flux-system get events \
--sort-by=.metadata.creationTimestamp \
--field-selector involvedObject.kind=Kustomization
Le notification-controller peut aussi exporter les événements vers un système externe (Mattermost, Slack, webhook) pour un audit centralisé.
Vault audit log¶
Activer l'audit log Vault pour tracer tous les accès aux secrets CI :
Chaque accès a un secret génère une entree :
{
"type": "response",
"auth": {
"display_name": "jwt-ci-runner",
"policies": ["ci-runner"]
},
"request": {
"path": "secret/data/ci/harbor",
"operation": "read"
},
"response": {
"data": {"keys": ["username", "password"]}
}
}
Protection de branche¶
Regles de protection sur main¶
Configurer Gitea pour que seul le CI puisse fusionner dans main apres validation :
# Via l'API Gitea
curl -s -X PATCH \
-H "Authorization: token ${GITEA_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"enable_push": false,
"enable_merge_whitelist": true,
"merge_whitelist_usernames": ["gitea-actions"],
"enable_status_check": true,
"status_check_contexts": [
"CI Pipeline / build",
"CI Pipeline / test",
"CI Pipeline / scan"
],
"required_approvals": 1
}' \
"https://git.example.com/api/v1/repos/org/myapp/branch_protections/main"
| Regle | Valeur | Objectif |
|---|---|---|
| Push direct | Interdit | Tout passe par une PR |
| Merge whitelist | gitea-actions uniquement | Seul le CI merge apres checks |
| Status checks requis | build, test, scan | Le code doit passer les 3 étapes |
| Approbations requises | 1 minimum | Review humaine obligatoire |
| Dismiss stale approvals | Active | Re-review si code modifie apres approbation |
Flux v2 multi-tenancy et RBAC¶
Isolation par namespace¶
Chaque équipe dispose d'un namespace avec un ServiceAccount dédié. Les Kustomizations sont restreintes a leur namespace cible via le RBAC Kubernetes natif.
# Namespace pour l'equipe backend
apiVersion: v1
kind: Namespace
metadata:
name: app-backend
labels:
toolkit.fluxcd.io/tenant: team-backend
---
# ServiceAccount dedie pour Flux dans ce tenant
apiVersion: v1
kind: ServiceAccount
metadata:
name: flux-team-backend
namespace: app-backend
---
# RoleBinding : le ServiceAccount ne peut deployer que dans son namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: flux-team-backend
namespace: app-backend
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin # dans le namespace uniquement
subjects:
- kind: ServiceAccount
name: flux-team-backend
namespace: app-backend
Kustomization restreinte au tenant¶
# clusters/production/tenants/team-backend.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: team-backend-apps
namespace: app-backend
spec:
interval: 5m
prune: true
serviceAccountName: flux-team-backend # restreint au namespace
sourceRef:
kind: GitRepository
name: team-backend-manifests
namespace: flux-system
path: ./overlays/production
targetNamespace: app-backend
RBAC Kubernetes natif
Contrairement aux systèmes RBAC propriétaires, Flux v2 reutilise le RBAC Kubernetes natif. L'isolation par ServiceAccount + namespace garantit qu'un tenant ne peut pas deployer dans le namespace d'un autre tenant. Pas de configuration RBAC specifique a Flux a maintenir.
Sécurité de la cle SOPS¶
Protection de la cle de dechiffrement¶
La cle SOPS (age ou GPG) est le secret le plus critique de Flux : elle permet de dechiffrer tous les secrets stockes dans Git.
| Mesure | Implementation |
|---|---|
| Stockage | Secret Kubernetes dans flux-system, jamais dans Git |
| Accès | Seul le kustomize-controller peut lire le Secret |
| Rotation | Re-chiffrer les secrets avec une nouvelle cle tous les 6 mois |
| Backup | Cle sauvegardee dans Vault (pas sur le filesystem) |
| Network policy | Le namespace flux-system n'est pas accessible depuis les CI |
# NetworkPolicy : proteger le namespace flux-system
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: flux-system-deny-all
namespace: flux-system
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
# Seul le webhook receiver accepte du trafic entrant
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- port: 9292
protocol: TCP
Réseau : matrice des flux autorises¶
| Source | Destination | Port | Protocole | Objectif |
|---|---|---|---|---|
| Runner CI | Gitea (SCM) | 443 | HTTPS | Clone du code |
| Runner CI | Harbor (registre) | 443 | HTTPS | Push des images |
| Runner CI | Miroir paquets | 443 | HTTPS | Telechargement dependencies |
| Runner CI | Vault | 8200 | HTTPS | Injection des secrets |
| Runner CI | SonarQube | 443 | HTTPS | Analyse qualité |
| Flux source-controller | Gitea (SCM) | 22 | SSH | Clone des manifestes |
| Flux kustomize-controller | API Kubernetes | 6443 | HTTPS | Déploiement |
| Flux notification-ctrl | Mattermost | 443 | HTTPS | Alertes |
| Flux image-reflector | Harbor (registre) | 443 | HTTPS | Scan des tags d'images |
| Ingress | Flux Receiver | 9292 | HTTP | Webhooks entrants (Gitea) |
| Développeur | Gitea | 443 | HTTPS | Push code, voir pipelines |
Zero trust entre CI et CD
Le runner CI n'a aucun accès direct au cluster Kubernetes. Le seul lien entre CI et CD est le dépôt Git de manifestes. Flux tire depuis Git — le CI ne pousse jamais vers le cluster.