Guide pratique pour sécuriser vos images containers avec des moyens limités
🎯 Le Problème : Images Gonflées, Sécurité Dégonflée
La Réalité Terrain
Vous héritez d’un zoo de containers :
- Images Ubuntu complètes : 1.2GB avec 847 packages installés
- Base images « kitchen sink » : Apache + MySQL + PHP + Node.js + Python dans la même image
- Versions figées depuis 2019 : « Si ça marche, on ne touche pas »
- Scanning inexistant : « On verra plus tard quand on aura le budget »
Résultat : Surface d’attaque énorme, vulnérabilités non trackées, compliance impossible.
L’Anti-Pattern Classique
# ❌ L'horreur qu'on voit trop souvent
FROM ubuntu:18.04
RUN apt-get update && apt-get install -y \
apache2 \
mysql-server \
php \
php-mysql \
nodejs \
npm \
python3 \
python3-pip \
curl \
wget \
vim \
nano \
htop \
net-tools \
&& rm -rf /var/lib/apt/lists/*
COPY everything/ /app/
EXPOSE 80 3306 3000 8000
CMD ["./start-everything.sh"]
Problèmes :
- 🔴 Surface d’attaque : 4 runtimes + OS complet
- 🔴 Maintenance : Impossible de tracker les vulnérabilités
- 🔴 Performance : 1.2GB pour servir une API REST
- 🔴 Sécurité : Multiples points d’entrée potentiels
🛡️ Stratégie de Contrôle Multi-Layer
Layer 1 : Minimisation Agressive (0€)
Technique des Base Images Distroless
# ✅ Approche minimaliste pour application Go
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app
# Image finale ultra-légère
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/app /
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app"]
Impact mesuré :
- Taille : 1.2GB → 12MB (-99%)
- Packages : 847 → 0 composants OS
- Surface d’attaque : Quasi-nulle
- Scan time : 45s → 2s
Templates par Runtime Optimisés
# Node.js optimisé
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
FROM node:18-alpine AS runtime
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
WORKDIR /app
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./
COPY --chown=nextjs:nodejs src/ ./src/
USER nextjs
EXPOSE 3000
CMD ["node", "src/index.js"]
# Python optimisé
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
FROM python:3.11-slim AS runtime
RUN adduser --disabled-password --gecos '' appuser
WORKDIR /app
COPY --from=builder /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
EXPOSE 8000
CMD ["python", "app.py"]
Layer 2 : Contrôle Continu Gratuit
Scanning avec Trivy (Open Source)
# .github/workflows/container-security.yml
name: Container Security Scan
on: [push, pull_request]
jobs:
container-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail si vulnérabilités critiques
- name: Upload results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Policy-as-Code avec OPA Gatekeeper
# policy-secure-images.yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: secureimages
spec:
crd:
spec:
names:
kind: SecureImages
validation:
properties:
allowedRegistries:
type: array
items:
type: string
disallowedTags:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package secureimages
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
image := container.image
# Bloquer les images avec tag 'latest'
endswith(image, ":latest")
msg := "L'utilisation du tag 'latest' est interdite"
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
image := container.image
# Bloquer les images non-autorisées
not startswith(image, "myregistry.com/")
msg := "Seules les images du registry autorisé sont permises"
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
# Exiger un utilisateur non-root
not container.securityContext.runAsNonRoot
msg := "Les containers doivent tourner avec un utilisateur non-root"
}
Layer 3 : Gouvernance Image Registry
Stratégie de Tagging Intelligente
#!/bin/bash
# build-and-tag.sh - Script de build avec tagging sémantique
VERSION=$(git describe --tags --always)
COMMIT=$(git rev-parse --short HEAD)
DATE=$(date +%Y%m%d)
BRANCH=$(git branch --show-current)
# Build de l'image
docker build -t myapp:${VERSION} .
# Tags multiples pour traçabilité
docker tag myapp:${VERSION} myapp:${COMMIT}
docker tag myapp:${VERSION} myapp:${DATE}-${COMMIT}
# Tag de branche pour dev/staging
if [ "$BRANCH" != "main" ]; then
docker tag myapp:${VERSION} myapp:${BRANCH}-latest
fi
# Tag stable uniquement pour main
if [ "$BRANCH" = "main" ]; then
docker tag myapp:${VERSION} myapp:stable
fi
echo "✅ Image taguée : myapp:${VERSION}"
echo "📝 Tags disponibles :"
docker images myapp --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}"
Cleanup Automatique Registry
#!/usr/bin/env python3
# registry-cleanup.py - Nettoyage automatique images obsolètes
import requests
import json
from datetime import datetime, timedelta
import os
class RegistryCleanup:
def __init__(self, registry_url, username, password):
self.registry_url = registry_url.rstrip('/')
self.auth = (username, password)
def get_repositories(self):
"""Liste tous les repositories"""
response = requests.get(
f"{self.registry_url}/v2/_catalog",
auth=self.auth
)
return response.json().get('repositories', [])
def get_tags(self, repository):
"""Liste les tags d'un repository"""
response = requests.get(
f"{self.registry_url}/v2/{repository}/tags/list",
auth=self.auth
)
return response.json().get('tags', [])
def get_manifest(self, repository, tag):
"""Récupère le manifest d'une image"""
response = requests.get(
f"{self.registry_url}/v2/{repository}/manifests/{tag}",
auth=self.auth,
headers={'Accept': 'application/vnd.docker.distribution.manifest.v2+json'}
)
return response.json()
def cleanup_old_images(self, repository, keep_count=10, keep_days=30):
"""Supprime les anciennes images selon les règles"""
tags = self.get_tags(repository)
# Tri par date de création (approximative via tag si format date)
dated_tags = []
for tag in tags:
if tag in ['latest', 'stable', 'main']:
continue # Garder les tags spéciaux
try:
manifest = self.get_manifest(repository, tag)
dated_tags.append((tag, manifest))
except:
continue
# Garde les N plus récents
dated_tags.sort(key=lambda x: x[1].get('created', ''), reverse=True)
to_delete = dated_tags[keep_count:]
cutoff_date = datetime.now() - timedelta(days=keep_days)
for tag, manifest in to_delete:
created = manifest.get('created', '')
if created and datetime.fromisoformat(created.replace('Z', '+00:00')) < cutoff_date:
self.delete_image(repository, tag)
print(f"🗑️ Supprimé {repository}:{tag}")
def delete_image(self, repository, tag):
"""Supprime une image du registry"""
# Récupère le digest
response = requests.head(
f"{self.registry_url}/v2/{repository}/manifests/{tag}",
auth=self.auth,
headers={'Accept': 'application/vnd.docker.distribution.manifest.v2+json'}
)
digest = response.headers.get('Docker-Content-Digest')
if digest:
# Supprime via digest
requests.delete(
f"{self.registry_url}/v2/{repository}/manifests/{digest}",
auth=self.auth
)
# Utilisation
if __name__ == "__main__":
cleanup = RegistryCleanup(
registry_url=os.getenv('REGISTRY_URL'),
username=os.getenv('REGISTRY_USER'),
password=os.getenv('REGISTRY_PASS')
)
repositories = cleanup.get_repositories()
for repo in repositories:
print(f"🧹 Nettoyage {repo}...")
cleanup.cleanup_old_images(repo, keep_count=5, keep_days=14)
—
🔧 Outils Gratuits & Efficaces
Stack de Scanning Gratuite
# docker-compose.monitoring.yml
version: '3.8'
services:
# Scanner Trivy local
trivy:
image: aquasec/trivy:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- trivy-cache:/root/.cache/trivy
command: ["server", "--listen", "0.0.0.0:4954"]
ports:
- "4954:4954"
# Harbor Registry (gratuit)
harbor-core:
image: goharbor/harbor-core:v2.8.0
environment:
- CORE_SECRET=YourSecretKey
- JOBSERVICE_SECRET=YourJobSecret
depends_on:
- harbor-db
- redis
volumes:
- harbor-data:/data
# Base de données
harbor-db:
image: goharbor/harbor-db:v2.8.0
environment:
- POSTGRES_PASSWORD=HarborPassword
volumes:
- harbor-db:/var/lib/postgresql/data
redis:
image: goharbor/redis-photon:v2.8.0
volumes:
trivy-cache:
harbor-data:
harbor-db:
Dashboard Sécurité DIY
#!/usr/bin/env python3
# security-dashboard.py - Dashboard sécurité fait maison
from flask import Flask, render_template, jsonify
import subprocess
import json
import docker
from datetime import datetime
app = Flask(__name__)
client = docker.from_env()
class SecurityDashboard:
def __init__(self):
self.client = docker.from_env()
def scan_image(self, image_name):
"""Scan une image avec Trivy"""
try:
result = subprocess.run([
'trivy', 'image', '--format', 'json',
'--severity', 'HIGH,CRITICAL', image_name
], capture_output=True, text=True)
if result.returncode == 0:
return json.loads(result.stdout)
return None
except Exception as e:
return {'error': str(e)}
def get_running_containers(self):
"""Liste les containers en cours"""
containers = []
for container in self.client.containers.list():
image_name = container.image.tags[0] if container.image.tags else container.image.id
scan_result = self.scan_image(image_name)
containers.append({
'name': container.name,
'image': image_name,
'status': container.status,
'created': container.attrs['Created'],
'vulnerabilities': self.count_vulnerabilities(scan_result)
})
return containers
def count_vulnerabilities(self, scan_result):
"""Compte les vulnérabilités par sévérité"""
if not scan_result or 'Results' not in scan_result:
return {'HIGH': 0, 'CRITICAL': 0, 'total': 0}
high_count = 0
critical_count = 0
for result in scan_result['Results']:
if 'Vulnerabilities' in result:
for vuln in result['Vulnerabilities']:
severity = vuln.get('Severity', '')
if severity == 'HIGH':
high_count += 1
elif severity == 'CRITICAL':
critical_count += 1
return {
'HIGH': high_count,
'CRITICAL': critical_count,
'total': high_count + critical_count
}
def get_registry_stats(self):
"""Statistiques du registry local"""
images = self.client.images.list()
total_size = sum(img.attrs['Size'] for img in images)
return {
'total_images': len(images),
'total_size_gb': round(total_size / (1024**3), 2),
'last_scan': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
}
dashboard = SecurityDashboard()
@app.route('/')
def index():
containers = dashboard.get_running_containers()
registry_stats = dashboard.get_registry_stats()
return render_template('dashboard.html',
containers=containers,
stats=registry_stats)
@app.route('/api/containers')
def api_containers():
return jsonify(dashboard.get_running_containers())
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Monitoring Grafana Simplifié
# grafana-dashboard.json - Configuration dashboard containers
{
"dashboard": {
"title": "Container Security Overview",
"panels": [
{
"title": "Vulnérabilités par Sévérité",
"type": "piechart",
"targets": [
{
"expr": "sum by (severity) (container_vulnerabilities)",
"legendFormat": "{{severity}}"
}
]
},
{
"title": "Images par Taille",
"type": "bargraph",
"targets": [
{
"expr": "sort_desc(container_image_size_bytes)",
"legendFormat": "{{image}}"
}
]
},
{
"title": "Trend Vulnérabilités",
"type": "graph",
"targets": [
{
"expr": "increase(container_vulnerabilities_total[1d])",
"legendFormat": "Nouvelles vulnérabilités/jour"
}
]
}
],
"time": {
"from": "now-7d",
"to": "now"
},
"refresh": "1m"
}
}
—
💡 Stratégies Budget Contraint
Approche Incrémentale
Phase 1 : Baseline Gratuite (0€, 2 semaines)
- Audit existant avec Trivy
- Refactoring Dockerfiles les plus critiques
- Mise en place CI/CD scanning
- Politique « no latest tag »
Phase 2 : Automatisation (500€, 1 mois)
- Registry Harbor auto-hébergé
- Policies OPA Gatekeeper
- Dashboard monitoring
- Cleanup automatique
Phase 3 : Optimisation (2000€, 2 mois)
- Distroless généralisé
- Multi-stage builds systématiques
- Image signing avec Cosign
- Runtime security avec Falco
ROI Démontré : Cas Client
Contexte : Startup FinTech, 50 microservices, budget sécurité 5k€/an
Avant transformation :
- Images moyennes : 890MB
- Vulnérabilités critiques : 23/image
- Temps de build : 12 minutes
- Coût storage : 450€/mois
Après 3 mois optimisation :
- Images moyennes : 45MB (-95%)
- Vulnérabilités critiques : 0,8/image (-97%)
- Temps de build : 3 minutes (-75%)
- Coût storage : 67€/mois (-85%)
ROI calculé :
- Économie storage : 4,596€/an
- Gain productivité : 8,200€/an (temps build)
- Évitement incidents : 15,000€/an (estimé)
- ROI total : 556% première année
—
🎯 Actions Immédiates
Checklist Week 1
# 1. Audit rapide existant
trivy image --severity HIGH,CRITICAL $(docker images --format "{{.Repository}}:{{.Tag}}")
# 2. Identifier les plus gros images
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | sort -k3 -hr
# 3. Template Dockerfile optimisé
cp dockerfile-templates/* ./
# 4. Setup CI/CD scanning
cp .github/workflows/container-security.yml .github/workflows/
# 5. Politique registry
kubectl apply -f policies/secure-images.yaml
Templates Prêts à l’Emploi
Dockerfile Node.js optimisé : 947MB → 87MB
Dockerfile Python optimisé : 1.2GB → 156MB
Dockerfile Go optimisé : 875MB → 12MB
Pipeline CI/CD sécurisé : Trivy + OPA + Gates
Policies Kubernetes : Security contexts + image restrictions
—
🏆 Conditions de Succès
Métriques de Pilotage
Objectives_90_Days:
Image_Size_Reduction: "> 70%"
Critical_Vulnerabilities: "< 2 par image"
Build_Time_Improvement: "> 50%"
Registry_Cleanup: "Automatisé"
Developer_Satisfaction: "> 7/10"
Success_Indicators:
- Zero vulnérabilités critiques en production
- Images rebuild automatique si CVE
- Temps scan < 30 secondes
- Storage cost reduction > 60%
- Onboarding new dev < 1 jour
Roadmap Continue
Mois 1-3 : Fondations sécurisées
Mois 4-6 : Automatisation complète
Mois 7-12 : Optimisation avancée
Juste après IA aidant : Innovation sécurité
🎯 Key Takeaway : La sécurité containers efficace ne coûte pas cher, elle fait économiser de l’argent. Commencez par les bases gratuites, mesurez l’impact, réinvestissez les économies dans l’amélioration continue.
« La meilleure image container est celle qui contient exactement ce dont vous avez besoin, et rien d’autre. »
En savoir plus sur Wet & sea & IA
Subscribe to get the latest posts sent to your email.
