Aller au contenu
Sécurité medium
🔐 Alerte sécurité — Incident supply chain Trivy : lire mon analyse de l'attaque

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

15 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

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*']
jobs:
verify-provenance:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- name: Install slsa-verifier
uses: slsa-framework/slsa-verifier/actions/installer@v2.7.0
- 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'

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

kyverno-slsa-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
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:
validationFailureAction: Enforce # Bloque réellement
background: true
webhookTimeoutSeconds: 30
rules:
- name: verify-slsa-keyless
match:
any:
- resources:
kinds:
- Pod
namespaces:
- production
- staging
verifyImages:
- imageReferences:
- "ghcr.io/mon-org/*"
attestations:
- type: https://slsa.dev/provenance/v1
predicateType: https://slsa.dev/provenance/v1
attestors:
- entries:
- keyless:
subject: "https://github.com/mon-org/*/.github/workflows/*"
issuer: "https://token.actions.githubusercontent.com"
rekor:
url: https://rekor.sigstore.dev
conditions:
- all:
# Vérifie que le build provient du bon repository
- key: "{{ invocation.configSource.uri }}"
operator: Equals
value: "git+https://github.com/mon-org/*"
  1. Mode Audit : Observez sans bloquer

    spec:
    validationFailureAction: 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 Enforce sur staging

    spec:
    validationFailureAction: Enforce
    rules:
    - match:
    any:
    - resources:
    namespaces: ["staging"]
  4. Extension progressive à production

    spec:
    rules:
    - match:
    any:
    - resources:
    namespaces: ["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.

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
.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
jobs:
create-exception:
runs-on: ubuntu-latest
environment: production-breakglass # Requires approval
steps:
- uses: actions/checkout@v4
- 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@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.

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: bitnami/kubectl:latest
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
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"})
EnvironnementModeExceptionsNotification
devAuditToutes autoriséesLog uniquement
stagingEnforceVia PR reviewéeSlack #deploy
productionEnforce strictBreak-glass + approbation SecOpsPagerDuty + SIEM
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
# 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
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 tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn