Aller au contenu
Sécurité medium

De la provenance SLSA à la policy : bloquer les artefacts non conformes

20 min de lecture

Générer de la provenance SLSA ne suffit pas. Sans vérification systématique, ces attestations restent des documents ignorés. Le vrai gain de sécurité vient de la mise en application : bloquer automatiquement tout artefact qui ne respecte pas vos exigences de provenance.

Ce guide montre comment implémenter une chaîne de vérification complète : d'abord en CI (avant publication), puis au déploiement (admission Kubernetes), avec une gestion rigoureuse des exceptions.

La vérification de provenance s'applique à deux points de contrôle complémentaires :

┌─────────────────────────────────────────────────────────────────────────┐
│ Pipeline CI/CD │
│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ ┌────────────┐ │
│ │ Build │───>│ Generate │───>│ Push image + │───>│ Verify │ │
│ │ artifact │ │provenance│ │ attestation │ │ before tag │ │
│ └──────────┘ └──────────┘ └────────────────┘ │ "release" │ │
│ └────────────┘ │
└────────────────────────────────────────────┬────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ Kubernetes cluster │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Admission Controller (Kyverno / Gatekeeper) │ │
│ │ ┌─────────────┐ │ │
│ │ │ Pod create │──> Verify provenance ──> PASS ──> Allow deploy │ │
│ │ │ request │ └── FAIL ──> Block + log │ │
│ │ └─────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Point de contrôleObjectifOutils
CI pipelineEmpêcher la publication d'artefacts sans provenance valideslsa-verifier, cosign
Admission KubernetesBloquer le déploiement d'images non conformesKyverno, Gatekeeper, Connaisseur

Le pipeline est le premier endroit où refuser un artefact, parce qu'il connaît le contexte de build : dépôt source, tag, identité du builder. Deux outils couvrent ce besoin, slsa-verifier pour les provenances produites par les générateurs SLSA, cosign pour les attestations signées via Sigstore. Dans les deux cas, la vérification doit précéder la promotion de l'image vers un tag de release : sinon rien n'empêche de publier d'abord et de constater ensuite.

L'outil slsa-verifier du SLSA Framework vérifie cryptographiquement qu'une provenance provient bien d'un builder SLSA connu.

.github/workflows/verify-release.yml
name: Verify and Release
on:
push:
tags: ['v*']
permissions: {}
jobs:
verify-provenance:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # requis uniquement par le `crane tag` final
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Install slsa-verifier
uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6 # v2.7.1
- name: Verify container image provenance
env:
IMAGE: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
run: |
slsa-verifier verify-image "$IMAGE" \
--source-uri "github.com/${{ github.repository }}" \
--source-tag "${{ github.ref_name }}" \
--builder-id "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.1.0"
- name: Promote to release tag
if: success()
run: |
# Tag l'image comme release uniquement après vérification
crane tag "$IMAGE" "release-${{ github.ref_name }}"

Pour les attestations signées avec Sigstore (keyless), utilisez cosign :

Vérification d'attestation SLSA avec cosign
# Vérifier qu'une image possède une attestation SLSA signée par GitHub Actions
cosign verify-attestation \
ghcr.io/mon-org/mon-app:v1.0.0 \
--type slsaprovenance \
--certificate-identity-regexp "^https://github.com/mon-org/mon-app/.github/workflows/build.yml@.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
# Extraire et inspecter le contenu de la provenance
cosign verify-attestation \
ghcr.io/mon-org/mon-app:v1.0.0 \
--type slsaprovenance \
--certificate-identity-regexp "^https://github.com/mon-org/mon-app/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
| jq -r '.payload' | base64 -d | jq '.predicate'

Le contrôle en CI ne couvre que les images passées par le pipeline officiel. Un kubectl apply manuel, un opérateur qui déploie une image tierce ou un pipeline parallèle le contournent sans effort. Le contrôleur d'admission ferme cette porte : il intercepte chaque requête de création de Pod avant que l'objet ne soit persisté dans etcd, et refuse celles dont l'image n'a pas d'attestation valide. Ce contrôle s'applique donc quelle que soit la manière dont le Pod a été demandé.

Kyverno permet de définir des policies déclaratives pour vérifier les images à l'admission :

kyverno-slsa-policy.yaml
apiVersion: policies.kyverno.io/v1
kind: ImageValidatingPolicy
metadata:
name: verify-slsa-provenance
annotations:
policies.kyverno.io/title: "Verify SLSA Provenance"
policies.kyverno.io/category: "Supply Chain Security"
policies.kyverno.io/severity: high
policies.kyverno.io/description: >-
Vérifie que les images de conteneurs possèdent une attestation
SLSA valide signée par un workflow GitHub Actions autorisé.
spec:
validationActions: [Deny]
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
matchImageReferences:
- glob: "ghcr.io/mon-org/*"
attestors:
- name: slsa-keyless
cosign:
keyless:
identities:
- subjectRegExp: "^https://github.com/mon-org/.*/\\.github/workflows/"
issuer: "https://token.actions.githubusercontent.com"
validations:
- expression: "true"
message: "Image must have a valid SLSA provenance attestation."

Passer directement en blocage sur un cluster existant revient à casser les déploiements de toutes les équipes qui n'ont pas encore de provenance. Les quatre étapes ci-dessous font varier deux réglages seulement : validationActions, qui décide entre observer et refuser, et matchConditions, qui restreint le périmètre aux namespaces choisis.

  1. Mode Audit : Observez sans bloquer

    spec:
    validationActions: [Audit]

    Collectez les violations dans les PolicyReports pendant 1-2 semaines.

  2. Analysez les violations

    Fenêtre de terminal
    kubectl get policyreport -A -o json | jq '.items[].results[] | select(.result == "fail")'
  3. Mode Deny sur staging : appliquez une matchCondition pour cibler les namespaces

    spec:
    validationActions: [Deny]
    matchConditions:
    - name: staging-only
    expression: "object.metadata.namespace == 'staging'"
  4. Extension progressive à production

    spec:
    matchConditions:
    - name: staging-and-prod
    expression: "object.metadata.namespace in ['staging', 'production']"

Même la meilleure policy nécessite un mécanisme d'exception pour les urgences. L'objectif : permettre le contournement sans le normaliser.

Une PolicyException désactive une règle nommée pour un ensemble de ressources précis. Le point à retenir : Kyverno ne fait pas expirer une exception tout seul. L'annotation expiry du manifeste ci-dessous est purement informative, c'est à vous de supprimer l'objet ou de programmer sa suppression. Les champs names et namespaces sont ce qui limite réellement la portée.

kyverno-breakglass-exception.yaml
apiVersion: kyverno.io/v1
kind: PolicyException
metadata:
name: emergency-deploy-incident-12345
namespace: kyverno
labels:
exception-type: break-glass
created-by: alice@example.com
incident-id: "12345"
annotations:
expiry: "2026-02-10T12:00:00Z" # Expire dans 24h
reason: "Hotfix critique CVE-2026-XXXX, provenance sera ajoutée post-incident"
approved-by: "security-team@example.com"
spec:
exceptions:
- policyName: verify-slsa-provenance
ruleNames:
- verify-slsa-keyless
match:
any:
- resources:
kinds:
- Pod
namespaces:
- production
names:
- "hotfix-*" # Limite aux pods hotfix

Créer l'exception à la main en pleine astreinte produit systématiquement un objet trop large et non tracé. Le workflow ci-dessous impose au contraire un formulaire : durée choisie dans une liste fermée, identifiant d'incident obligatoire, et une environment protégée qui exige une approbation avant exécution. Il notifie enfin le canal sécurité, ce qui rend l'exception visible au moment où elle est créée et non lors de l'audit trimestriel.

.github/workflows/breakglass-request.yml
name: Break-Glass Request
on:
workflow_dispatch:
inputs:
namespace:
description: 'Target namespace'
required: true
type: choice
options: [staging, production]
pod_pattern:
description: 'Pod name pattern (e.g., hotfix-*)'
required: true
duration_hours:
description: 'Exception duration (hours)'
required: true
default: '4'
type: choice
options: ['1', '4', '12', '24']
reason:
description: 'Reason for exception'
required: true
incident_id:
description: 'Incident/ticket ID'
required: true
permissions: {}
jobs:
create-exception:
runs-on: ubuntu-latest
environment: production-breakglass # Requires approval
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Calculate expiry
id: expiry
run: |
EXPIRY=$(date -u -d "+${{ inputs.duration_hours }} hours" +%Y-%m-%dT%H:%M:%SZ)
echo "expiry=$EXPIRY" >> $GITHUB_OUTPUT
- name: Create PolicyException
run: |
cat <<EOF | kubectl apply -f -
apiVersion: kyverno.io/v1
kind: PolicyException
metadata:
name: breakglass-${{ inputs.incident_id }}
namespace: kyverno
labels:
exception-type: break-glass
created-by: ${{ github.actor }}
incident-id: "${{ inputs.incident_id }}"
annotations:
expiry: "${{ steps.expiry.outputs.expiry }}"
reason: "${{ inputs.reason }}"
workflow-run: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
spec:
exceptions:
- policyName: verify-slsa-provenance
ruleNames: ["verify-slsa-keyless"]
match:
any:
- resources:
kinds: [Pod]
namespaces: ["${{ inputs.namespace }}"]
names: ["${{ inputs.pod_pattern }}"]
EOF
- name: Notify Security Channel
uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0
with:
channel-id: 'security-alerts'
slack-message: |
:warning: *BREAK-GLASS EXCEPTION CREATED*
• Namespace: `${{ inputs.namespace }}`
• Pattern: `${{ inputs.pod_pattern }}`
• Duration: ${{ inputs.duration_hours }}h
• Reason: ${{ inputs.reason }}
• Incident: ${{ inputs.incident_id }}
• Created by: ${{ github.actor }}
• Expires: ${{ steps.expiry.outputs.expiry }}

Chaque décision de vérification doit être tracée pour la conformité et le debugging.

Les PolicyReport vivent dans le cluster et suivent le cycle de vie des ressources qu'ils décrivent : un Pod supprimé emporte son rapport. Pour garder une trace exploitable en audit, il faut les exporter vers un stockage externe. Le CronJob ci-dessous les pousse chaque heure vers un SIEM ; adaptez l'URL et l'authentification à votre collecteur.

policyreport-export.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: export-policy-reports
namespace: kyverno
spec:
schedule: "0 * * * *" # Toutes les heures
jobTemplate:
spec:
template:
spec:
containers:
- name: export
image: bitnamilegacy/kubectl:1.33.4
command:
- /bin/sh
- -c
- |
kubectl get policyreport -A -o json | \
curl -X POST -H "Content-Type: application/json" \
-d @- https://siem.example.com/api/kyverno-reports
restartPolicy: OnFailure

Exposez des métriques Prometheus pour monitorer la santé de vos policies :

servicemonitor-kyverno.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: kyverno-metrics
namespace: monitoring
spec:
selector:
matchLabels:
app.kubernetes.io/name: kyverno
endpoints:
- port: metrics
path: /metrics
interval: 30s

Métriques clés à surveiller :

MétriqueDescriptionAlerte si
kyverno_policy_results_total{result="fail"}Violations bloquéesSpike soudain
kyverno_policy_execution_duration_secondsLatence de vérification> 5s
kyverno_admission_review_duration_secondsTemps de réponse admission> 10s

Trois requêtes suffisent à un tableau de bord utile : le taux de refus par policy, la répartition pass/fail par namespace, et le nombre d'exceptions break-glass actives. Cette dernière est la plus parlante en revue : si le compteur ne redescend jamais à zéro, les exceptions sont devenues permanentes et la policy ne protège plus rien.

Requêtes PromQL utiles
# Taux de violations par policy
sum by (policy) (rate(kyverno_policy_results_total{result="fail"}[1h]))
# Ratio pass/fail par namespace
sum by (namespace, result) (rate(kyverno_policy_results_total[24h]))
# Exceptions break-glass actives
count(kyverno_policy_exception_total{type="break-glass"})

Une même policy ne se règle pas de la même façon selon l'environnement : le coût d'un blocage injustifié est nul en dev et élevé en production. Cette matrice donne un point de départ raisonnable, à lire ligne par ligne comme un contrat entre l'équipe plateforme et les équipes applicatives. La colonne Notification compte autant que la colonne Mode : un refus silencieux se transforme en ticket incident dans l'heure.

EnvironnementModeExceptionsNotification
devAuditToutes autoriséesLog uniquement
stagingEnforceVia PR reviewéeSlack #deploy
productionEnforce strictBreak-glass + approbation SecOpsPagerDuty + SIEM

Trois symptômes couvrent la quasi-totalité des appels après une mise en blocage. Le premier vient de l'artefact, le deuxième du réseau, le troisième de l'installation de Kyverno. Commencez toujours par déterminer lequel des trois avant de toucher à la policy elle-même.

L'attestation n'est pas dans le manifeste de l'image mais dans un artefact rattaché ; ces deux commandes montrent ce que le registry expose réellement pour le tag visé.

Fenêtre de terminal
# Vérifiez que l'attestation existe
cosign tree ghcr.io/mon-org/mon-app:v1.0.0
# Listez les attestations attachées
crane manifest ghcr.io/mon-org/mon-app:v1.0.0 | jq '.manifests[] | select(.annotations["org.opencontainers.image.ref.name"] | contains("att"))'

Causes fréquentes :

  • Le pipeline de build n'a pas généré de provenance
  • L'attestation est sur un tag différent (:v1.0.0 vs :sha-abc123)
  • Le registry ne supporte pas les referrers OCI

La vérification interroge le registry et le log de transparence pendant l'admission, donc sur le chemin critique de création du Pod. Le timeout du webhook vaut 10 secondes par défaut, ce qui est court dès que le cluster sort par un proxy.

# Augmenter le timeout dans la policy
spec:
webhookTimeoutSeconds: 60 # Défaut: 10

Causes fréquentes :

  • Rekor (transparency log) temporairement lent
  • Réseau cluster → Internet filtré
  • Registry externe lent

Une policy qui laisse tout passer signale presque toujours que le webhook n'est plus enregistré ou que le contrôleur d'admission ne répond pas. Ces trois commandes vérifient successivement les Pods de Kyverno, l'enregistrement du webhook auprès de l'API server, puis les journaux du contrôleur.

Fenêtre de terminal
# Vérifier que Kyverno est opérationnel
kubectl get pods -n kyverno
# Vérifier les webhooks
kubectl get validatingwebhookconfigurations | grep kyverno
# Logs du controller
kubectl logs -n kyverno -l app.kubernetes.io/component=admission-controller --tail=100

Double vérification

Vérifiez en CI (avant publication) et à l'admission (avant déploiement). Une seule vérification peut être contournée.

Policy as Code

Les policies Kyverno sont versionnées en Git. Changements = PR + review.

Break-glass encadré

Exceptions possibles mais : durée limitée, scope minimal, traçabilité complète.

Observabilité

Chaque décision (pass/fail/exception) doit être loggée et monitorable.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn