Sécuriser une image de conteneur en 2026 nécessite quatre couches coordonnées : construction durcie (Dockerfile minimal, base image distroless ou Chainguard, gestion BuildKit des secrets, multi-stage build), distribution sécurisée (registry privé avec scan continu, signature Cosign keyless, attestations SLSA), runtime hardening (utilisateur non-root, capabilities drop, seccomp, AppArmor, read-only rootfs) et admission policies (vérification de signature, refus des images non conformes au déploiement). Le scan de vulnérabilités (Trivy, Grype, Docker Scout) reste essentiel mais n'est qu'une des quatre couches. Les référentiels qui structurent ce périmètre en 2026 sont NIST SP 800-190 (Application Container Security Guide), CIS Docker Benchmark v1.6, OWASP Container Security Verification Standard. Les CVE récentes runc Leaky Vessels (CVE-2024-21626, CVE-2024-23651, CVE-2024-23652, février 2024) ont rappelé que la sécurité conteneur ne se résume pas au scan applicatif. Cet article détaille la construction d'images sécurisées (Dockerfile patterns, base images comparées), la gestion des secrets au build, la signature et la provenance, le durcissement runtime, les outils 2026 (BuildKit, Cosign, Hadolint, Dive, Sigstore Policy Controller) et un Dockerfile production de référence.
La menace : pourquoi les images conteneurs sont une cible
Trois caractéristiques font des images conteneurs une cible privilégiée en 2026.
Multiplication des images dans une chaîne de production. Une application moderne déploie typiquement 10 à 100 images conteneurs distinctes (services applicatifs, sidecars, init containers, jobs batch). Chaque image est une surface d'attaque potentielle, et une seule image compromise peut servir de pivot pour latéraliser dans le cluster.
Hérédité des CVE depuis les bases. Une image construite sur ubuntu:22.04 hérite immédiatement de toutes les CVE des paquets système installés. Une image qui n'est pas reconstruite régulièrement accumule des CVE critiques en quelques semaines. Les bases les plus minces (distroless, Chainguard) réduisent drastiquement cette accumulation.
Container escape : la menace asymétrique. Une vulnérabilité dans le runtime container (runc, containerd, Docker, Kubernetes kubelet) permet de s'évader du container vers l'hôte. CVE-2024-21626 (Leaky Vessels, février 2024) a démontré qu'une CVE runc unique pouvait potentiellement compromettre des milliers de clusters Kubernetes. Le hardening container réduit la probabilité d'exploitation de ces CVE.
Construction d'images sécurisées : le Dockerfile
La majorité des vulnérabilités évitables sont introduites lors de la rédaction du Dockerfile. Six règles cumulatives produisent une image durcie.
Multi-stage build
Séparer la phase de build (compilation, installation de dépendances de build) de la phase finale qui ne contient que le runtime nécessaire. Permet de retirer les outils de build (gcc, npm dev dependencies, debug symbols) de l'image finale.
# Multi-stage : build complet vs runtime minimal
FROM golang:1.23-bookworm AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags='-s -w' -o /app/server ./cmd/server
# Stage final : distroless avec seulement le binaire
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/server"]Différence de surface d'attaque : l'image finale Go avec distroless static fait typiquement 5 à 15 MB et ne contient ni shell, ni libc, ni package manager. Le binaire et les certificats CA, c'est tout.
Base image minimale
Trois familles par ordre de durcissement croissant.
| Famille | Exemple | Taille typique | Surface | Débuggabilité |
|---|---|---|---|---|
| Full distribution | ubuntu:22.04, debian:bookworm | 70 à 120 MB | Large | Excellente |
| Slim / alpine | python:3.12-slim, node:22-alpine | 30 à 80 MB | Moyenne | Bonne |
| Distroless (Google) | gcr.io/distroless/python3-debian12 | 20 à 60 MB | Faible | Difficile |
| Wolfi / Chainguard | cgr.dev/chainguard/python | 15 à 50 MB | Très faible (zéro CVE) | Difficile |
| Static binary | gcr.io/distroless/static, scratch | 1 à 15 MB | Minimale | Impossible sans shell ext. |
Choix recommandé en 2026 : Chainguard Images pour les workloads production, fondées sur Wolfi (distribution née pour conteneurs, mises à jour quotidiennes, signées Cosign par défaut, posture zéro CVE connue). Distroless reste une excellente option gratuite. Alpine garde une niche sur les workloads développement et les images simples.
Utilisateur non-root
Tout container production doit s'exécuter en utilisateur non-privilégié.
# Mauvais : par défaut, USER root
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y nginx
CMD ["nginx", "-g", "daemon off;"]
# Container exécuté en root, capabilities root sur l'image
# Bon : utilisateur dédié non-root
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y nginx \
&& groupadd -g 10001 app \
&& useradd -u 10001 -g app -s /sbin/nologin -M app \
&& chown -R app:app /var/cache/nginx /var/log/nginx
USER 10001:10001
CMD ["nginx", "-g", "daemon off;"]Si l'application doit lier un port inférieur à 1024 (typiquement 80 ou 443), remplacer par 8080/8443 ou utiliser setcap cap_net_bind_service=+ep sur le binaire au build (ne nécessite pas root au runtime).
Capabilities et privileges
Compléter au runtime via Kubernetes ou Docker run :
# Kubernetes : pod sécurisé
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: my-registry.example.test/app@sha256:abc123...
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
limits:
memory: "256Mi"
cpu: "500m"
requests:
memory: "128Mi"
cpu: "100m"
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}Configuration équivalente avec Docker run :
docker run \
--read-only \
--tmpfs /tmp \
--user 10001:10001 \
--cap-drop ALL \
--security-opt no-new-privileges \
--security-opt seccomp=default.json \
my-registry.example.test/app@sha256:abc123....dockerignore strict
Le contexte de build envoyé à Docker daemon inclut tout le répertoire courant par défaut. Sans .dockerignore strict, des fichiers sensibles (.env, .git/, clés SSH) finissent involontairement dans l'image.
# .dockerignore type pour application Node.js
.git
.github
node_modules
npm-debug.log
.env*
*.log
.vscode
.idea
.DS_Store
coverage/
test/
docs/
*.md
Dockerfile*
docker-compose*
.terraform
*.tfstate*
secrets/Pinning par digest SHA256
Référencer les bases par digest SHA256 plutôt que par tag mutable.
# Mauvais : tag mutable
FROM python:3.12-slim
# Bon : digest immuable + commentaire de version
FROM python:3.12-slim@sha256:7c8b41e8f8a9b6c5d3e2a1f4b7c8e9d0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6
# python:3.12-slim au 2026-04-15Renovate ou Dependabot mettent à jour automatiquement ces digests.
Gestion des secrets au build
Pattern dangereux fréquent : passer des secrets via ARG ou COPY qui les persiste dans une layer permanente, visible via docker history.
# DANGEREUX : secret persiste dans l'image
FROM debian:bookworm-slim
ARG NPM_TOKEN
ENV NPM_TOKEN=$NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc \
&& npm install
# Le NPM_TOKEN apparaît dans docker history et dans toutes les layers ENVSolution 2026 : BuildKit secret mounts.
# syntax=docker/dockerfile:1.7
FROM debian:bookworm-slim
# Le secret est monté en mémoire pendant l'exécution du RUN seulement
RUN --mount=type=secret,id=npm_token,target=/root/.npmrc \
cat /root/.npmrc \
&& npm install
# Aucune trace du secret dans les layers de l'image finaleBuild avec BuildKit secret :
# Activer BuildKit (par défaut depuis Docker 23.0)
export DOCKER_BUILDKIT=1
# Build avec secret monté
docker build \
--secret id=npm_token,src=$HOME/.secrets/npm-token.txt \
-t my-app:latest .Dans GitHub Actions :
- uses: docker/build-push-action@v6
with:
secrets: |
"npm_token=${{ secrets.NPM_TOKEN }}"Signature et provenance avec Sigstore
La signature d'image avec Cosign est devenue standard en 2026. Avec OIDC keyless (GitHub Actions, GitLab CI), aucune clé à gérer.
# GitHub Actions : signature keyless via OIDC GitHub
permissions:
contents: read
id-token: write # requis pour OIDC keyless
packages: write
steps:
- uses: sigstore/cosign-installer@v3
- name: Build et push image
id: build
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/my-org/my-app:${{ github.sha }}
- name: Sign image (keyless)
run: |
cosign sign --yes \
ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}
format: cyclonedx-json
output-file: sbom.cyclonedx.json
- name: Attest SBOM
run: |
cosign attest --yes \
--predicate sbom.cyclonedx.json \
--type cyclonedx \
ghcr.io/my-org/my-app@${{ steps.build.outputs.digest }}Vérification côté consommateur (admission controller, déploiement) :
# Vérifier la signature et la provenance attendue
cosign verify \
--certificate-identity-regexp='^https://github.com/my-org/my-app/' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
ghcr.io/my-org/my-app@sha256:abc123...
# Vérifier l'attestation SBOM
cosign verify-attestation \
--type cyclonedx \
--certificate-identity-regexp='^https://github.com/my-org/my-app/' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
ghcr.io/my-org/my-app@sha256:abc123...Registries sécurisés
Quatre options principales en 2026 selon le contexte.
| Registry | Type | Scan intégré | Signature support | Avantage clé |
|---|---|---|---|---|
| Harbor | Self-hosted open source | Trivy intégré | Cosign, Notary v2 | Contrôle total, OSS, RBAC fin |
| GitHub Container Registry (GHCR) | Cloud SaaS | Dependabot | Cosign keyless GitHub OIDC | Intégration native GitHub Actions |
| AWS ECR | Cloud SaaS | Inspector / Enhanced Scanning | Cosign + ECR | Intégration native AWS IAM, OIDC |
| Azure Container Registry | Cloud SaaS | Microsoft Defender | Notation, Cosign | Intégration Azure |
| Google Artifact Registry | Cloud SaaS | Container Analysis API | Cosign keyless GCP OIDC | Intégration GCP |
| JFrog Artifactory | SaaS / self-hosted | Xray | Cosign, custom | Multi-format, enterprise |
| Quay.io (Red Hat) | Cloud SaaS | Clair | Cosign | OpenShift integration |
Configuration registry recommandée 2026 :
- Repos privés par défaut, public uniquement si justifié.
- Vulnerability scanning automatique à chaque push, alerting sur critical / high.
- Image immutable tags activé (interdit la réécriture d'un tag déjà pushé).
- Retention policy : suppression automatique des images non utilisées au-delà de N jours, sauf tags signés ou taggés
release. - Authentification OIDC depuis le CI, pas de credentials long-lived.
Admission control : la dernière ligne
Empêcher le déploiement d'images non conformes. Trois outils dominants en Kubernetes 2026.
Kyverno
Approche policy-as-code en YAML, syntaxe Kubernetes-native, courbe d'apprentissage faible.
# Refuser les images non signées par notre identité GitHub Actions
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signature
spec:
validationFailureAction: Enforce
rules:
- name: verify-cosign
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "ghcr.io/my-org/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/my-org/*"
issuer: "https://token.actions.githubusercontent.com"
# Refuser les containers root
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-non-root
spec:
validationFailureAction: Enforce
rules:
- name: check-runAsNonRoot
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Le container doit s'exécuter en non-root (runAsNonRoot: true)"
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: falseSigstore Policy Controller
Spécialisé Sigstore, plus simple si l'usage est uniquement signature verification.
OPA Gatekeeper
Plus puissant et plus complexe (Rego). Recommandé pour les organisations avec besoin de policies très avancées.
Runtime hardening avancé
Au-delà des securityContext de base, trois technologies ajoutent une isolation supplémentaire.
Seccomp profiles
Restreindre les syscalls accessibles au container.
# Pod avec seccomp profile RuntimeDefault (recommandé)
securityContext:
seccompProfile:
type: RuntimeDefault
# Ou profile custom localisé
securityContext:
seccompProfile:
type: Localhost
localhostProfile: profiles/my-app.jsonAppArmor (Ubuntu, Debian) ou SELinux (RHEL, Fedora)
Profils MAC (Mandatory Access Control) qui restreignent les capacités d'un processus au-delà de seccomp.
# Pod avec AppArmor profile
metadata:
annotations:
container.apparmor.security.beta.kubernetes.io/app: localhost/my-app-profileRuntimes alternatifs : gVisor, Kata Containers
Pour les workloads multi-tenant ou les containers exécutant du code non-fiable, remplacer runc par un runtime à isolation plus forte.
# Kubernetes : RuntimeClass pour gVisor
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runsc
---
apiVersion: v1
kind: Pod
spec:
runtimeClassName: gvisor
containers:
- image: untrusted/code:latestOutils d'audit et linting
Quatre outils open source à intégrer en CI pour ne pas dépendre uniquement du scan post-build.
Hadolint : Dockerfile linter
# Audit d'un Dockerfile
hadolint Dockerfile
# En CI GitHub Actions
- uses: hadolint/hadolint-action@v3
with:
dockerfile: Dockerfile
failure-threshold: warningDétecte les anti-patterns courants : apt-get sans cleanup, COPY . trop large, absence de USER, version non pinée.
Dive : analyse de layers
# Inspection interactive d'une image (taille par layer, fichiers)
dive my-image:latest
# Mode CI pour vérifier l'efficacité (ratio espace utile / espace total)
CI=true dive my-image:latest --ci-config dive-ci.yamlTrivy / Grype / Docker Scout
Scan de vulnérabilités. Détaillé dans l'article dédié scan-conteneurs-pourquoi-comment.
Syft : génération de SBOM
# SBOM d'une image au format CycloneDX
syft packages docker:my-image:latest -o cyclonedx-json=sbom.jsonDockerfile production de référence
Exemple complet combinant l'ensemble des bonnes pratiques pour une application Go.
# syntax=docker/dockerfile:1.7
# ============================================================
# Stage 1 — Builder avec Go toolchain complète
# ============================================================
FROM golang:1.23-bookworm@sha256:abc123def456789012345678901234567890abcdef123456789012345678901234 AS builder
WORKDIR /src
# Cache des dépendances en layer séparée
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Copie du code source et build statique
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags='-s -w -buildid=' \
-o /app/server ./cmd/server
# ============================================================
# Stage 2 — Image finale distroless static
# ============================================================
FROM gcr.io/distroless/static-debian12:nonroot@sha256:def456abc789012345678901234567890abcdef123456789012345678901234567
# Métadonnées OCI
LABEL org.opencontainers.image.title="my-app"
LABEL org.opencontainers.image.description="Application Go production-ready"
LABEL org.opencontainers.image.source="https://github.com/my-org/my-app"
LABEL org.opencontainers.image.licenses="Apache-2.0"
# Binaire compilé du builder
COPY --from=builder --chown=nonroot:nonroot /app/server /server
# Port non-privilégié
EXPOSE 8080
# Utilisateur non-root (UID 65532 dans distroless nonroot)
USER nonroot:nonroot
# Pas de SHELL : ENTRYPOINT direct au binaire
ENTRYPOINT ["/server"]Caractéristiques de cette image :
- Multi-stage : build env complet retiré de l'image finale.
- Base distroless static : zéro shell, zéro libc, ~5 à 10 MB.
- Pinning par digest SHA256 sur les deux stages.
- Build cache mounts BuildKit pour accélérer les builds successifs.
- Compilation reproductible (
-trimpath,-ldflags='-s -w -buildid='). - Utilisateur non-root par défaut (UID 65532).
- Métadonnées OCI normalisées.
Points clés à retenir
- La sécurité d'image conteneur en 2026 combine quatre couches : construction durcie (Dockerfile, base minimale), distribution sécurisée (registry, signature Cosign, attestations), runtime hardening (non-root, capabilities, seccomp), admission policies. Le scan seul est insuffisant.
- Les bases Chainguard Images ou distroless réduisent la surface d'attaque et la dette CVE quasi à zéro pour les workloads production. Standard 2026.
- BuildKit secret mounts (
RUN --mount=type=secret) est le seul pattern correct pour passer un secret au build sans persistence dans une layer. Jamais de secret via ENV ni COPY. - Cosign keyless via OIDC (GitHub Actions, GitLab CI) est le pattern standard 2026 pour signer et attester les images sans gérer de clés privées.
- Le runtime hardening (non-root, drop capabilities, seccomp, AppArmor) reste indispensable même avec une image parfaite : CVE-2024-21626 Leaky Vessels a démontré que les CVE runtime existent et sont exploitables.
Pour aller plus loin
- Scan de conteneurs : pourquoi et comment - approfondissement de la couche scan complémentaire.
- Trivy : à quoi ça sert - outil de scan de référence open source.
- Sécurité GitHub Actions - durcir le pipeline qui produit les images.
- CI/CD sécurisée : définition - vue transverse des pipelines de production.





