Aller au contenu

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.

set +x  # desactiver le debug
podman login -u "${USER}" -p "${PASS}" harbor.example.com
set -x  # reactiver si necessaire

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 :

vault audit enable file file_path=/var/log/vault/audit.log

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.