🐳 Sécurité Containers Pragmatique : Contrôler Sans Se Ruiner

🐳 Sécurité Containers Pragmatique : Contrôler Sans Se Ruiner

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)

  1. Audit existant avec Trivy
  2. Refactoring Dockerfiles les plus critiques
  3. Mise en place CI/CD scanning
  4. Politique « no latest tag »

Phase 2 : Automatisation (500€, 1 mois)

  1. Registry Harbor auto-hébergé
  2. Policies OPA Gatekeeper
  3. Dashboard monitoring
  4. Cleanup automatique

Phase 3 : Optimisation (2000€, 2 mois)

  1. Distroless généralisé
  2. Multi-stage builds systématiques
  3. Image signing avec Cosign
  4. 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.

Non classé
Share on Social Media

En savoir plus sur Wet & sea & IA

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture