Aller au contenu

Bonnes pratiques

Garbage collection

Le garbage collection (GC) supprime les blobs orphelins (layers non références par aucun manifest) pour libérer de l'espace de stockage.

Fonctionnement

graph LR
    subgraph Avant["Avant GC"]
        M1a["manifest:v1.0"] --> LA1["layer-A"]
        M1a --> LB1["layer-B"]
        M2a["manifest:v1.1"] --> LB1
        M2a --> LC1["layer-C"]
        tag["tag supprime"] -.-> LD["layer-D"]
        tag -.-> LE["layer-E<br/>orphelins"]
    end
    subgraph Apres["Apres GC"]
        M1b["manifest:v1.0"] --> LA2["layer-A"]
        M1b --> LB2["layer-B"]
        M2b["manifest:v1.1"] --> LB2
        M2b --> LC2["layer-C"]
    end

Planification

Parametre Valeur recommandee Justification
Fréquence Hebdomadaire (dimanche 2h) Hors heures de CI/CD
Mode En ligne (Harbor 2.x) Pas d'interruption de service
Delete untagged Oui Supprimer les manifests sans tag
Dry-run prealable Oui (mensuel) Vérifier avant de supprimer

Dry-run

Toujours faire un dry-run d'abord

Le dry-run montre ce qui serait supprime sans rien effacer. Exécuter un dry-run avant le premier GC et apres tout changement de politique de retention.

# Lancer un GC dry-run via API
curl -X POST "https://harbor.example.com/api/v2.0/system/gc/schedule" \
  -H "Content-Type: application/json" \
  -u admin:ADMIN_PASSWORD \
  -d '{
    "parameters": {
      "delete_untagged": true,
      "dry_run": true
    },
    "schedule": {
      "type": "Manual"
    }
  }'

# Verifier les resultats du dry-run
curl -s "https://harbor.example.com/api/v2.0/system/gc" \
  -u admin:ADMIN_PASSWORD | jq '.[0]'

Planifier le GC automatique

# Configurer le GC hebdomadaire
curl -X PUT "https://harbor.example.com/api/v2.0/system/gc/schedule" \
  -H "Content-Type: application/json" \
  -u admin:ADMIN_PASSWORD \
  -d '{
    "parameters": {
      "delete_untagged": true,
      "dry_run": false
    },
    "schedule": {
      "type": "Weekly",
      "cron": "0 0 2 * * 0"
    }
  }'

Politiques de retention des tags

La retention determine quels tags sont conserves et lesquels sont candidats a la suppression.

Stratégie recommandee

Projet Regle de retention Justification
app-* Garder les 10 derniers tags v* Permettre le rollback
app-* Garder les tags des 90 derniers jours Historique recent
library Garder les 5 derniers tags par image Images de base stables
mirror Garder les 3 derniers tags par image Miroir, pas d'historique profond
charts Garder les 10 dernières versions Permettre le rollback Helm

Configuration

# Creer une politique de retention pour app-backend
curl -X POST "https://harbor.example.com/api/v2.0/retentions" \
  -H "Content-Type: application/json" \
  -u admin:ADMIN_PASSWORD \
  -d '{
    "algorithm": "or",
    "scope": {
      "level": "project",
      "ref": 2
    },
    "trigger": {
      "kind": "Schedule",
      "settings": {
        "cron": "0 0 3 * * 0"
      }
    },
    "rules": [
      {
        "action": "retain",
        "template": "latestPushedK",
        "params": {"latestPushedK": 10},
        "tag_selectors": [
          {"kind": "doublestar", "decoration": "matches", "pattern": "v*"}
        ],
        "scope_selectors": {
          "repository": [
            {"kind": "doublestar", "decoration": "repoMatches", "pattern": "**"}
          ]
        }
      },
      {
        "action": "retain",
        "template": "nDaysSinceLastPush",
        "params": {"nDaysSinceLastPush": 90},
        "tag_selectors": [
          {"kind": "doublestar", "decoration": "matches", "pattern": "**"}
        ],
        "scope_selectors": {
          "repository": [
            {"kind": "doublestar", "decoration": "repoMatches", "pattern": "**"}
          ]
        }
      }
    ]
  }'

Réplication pour la reprise d'activité

Stratégie DR

Composant Méthode RPO RTO
Images/Charts Réplication event-based vers Harbor DR Quasi-zero < 5 min
Metadonnees (DB) PostgreSQL streaming réplication < 1 min < 5 min
Configuration Export YAML en Git (IaC) Quotidien < 30 min
Secrets Vault réplication Quasi-zero < 5 min

Configuration de la réplication DR

# Replication push event-based : chaque artefact pousse est immediatement replique
curl -X POST https://harbor.example.com/api/v2.0/replication/policies \
  -H "Content-Type: application/json" \
  -u admin:ADMIN_PASSWORD \
  -d '{
    "name": "dr-all-projects",
    "dest_registry": {"id": 1},
    "trigger": {
      "type": "event_based"
    },
    "filters": [
      {"type": "name", "value": "**"},
      {"type": "resource", "value": "image"},
      {"type": "resource", "value": "chart"}
    ],
    "enabled": true,
    "override": true,
    "speed": -1
  }'

Test de bascule DR

#!/bin/bash
# /home/ops/scripts/test-dr-failover.sh
set -euo pipefail

DR_REGISTRY="harbor-dr.example.com"

echo "=== Test de bascule DR ==="

# 1. Verifier la connectivite
echo "1. Test connectivite..."
curl -sf "https://${DR_REGISTRY}/api/v2.0/health" | jq '.status'

# 2. Verifier que la replication est a jour
echo "2. Verification replication..."
curl -s "https://${DR_REGISTRY}/api/v2.0/replication/executions?page_size=5" \
  -u admin:DR_PASSWORD | jq '.[] | {id, status, trigger}'

# 3. Tester le pull depuis le DR
echo "3. Test pull..."
podman pull "${DR_REGISTRY}/app-backend/api:v3.0.1"

# 4. Verifier le scan
echo "4. Verification scan..."
curl -s "https://${DR_REGISTRY}/api/v2.0/projects/app-backend/repositories/api/artifacts/v3.0.1?with_scan_overview=true" \
  -u admin:DR_PASSWORD | jq '.scan_overview'

echo "=== Bascule DR : OK ==="

Tester la bascule DR trimestriellement

Un plan DR non teste est un faux sentiment de sécurité. Planifier un test de bascule complet chaque trimestre, incluant le changement DNS et le déploiement Kubernetes depuis le registre DR.

Hygiène des images

Images de base minimales

Image de base Taille Packages Usage recommande
scratch 0 Mo Aucun Binaires statiques Go
alpine:3.20 7 Mo Minimal Outils CLI, scripts
debian:bookworm-slim 80 Mo Moyen Applications générales
distroless/static 2 Mo Aucun Binaires statiques
distroless/base 20 Mo glibc Applications C/C++
distroless/java21 220 Mo JRE Applications Java

Distroless pour la production

Les images distroless ne contiennent ni shell ni gestionnaire de paquets, ce qui reduit drastiquement la surface d'attaque. Debug en production via des conteneurs ephemeres (kubectl debug).

Multi-stage builds

# Dockerfile — multi-stage build
# Stage 1 : Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /api ./cmd/api

# Stage 2 : Runtime (image minimale)
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /api /api
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/api"]

Résultats :

Approche Taille image Vulnerabilites typiques
golang:1.22 (single) ~800 Mo 50-200 CVE
Multi-stage + alpine ~15 Mo 5-15 CVE
Multi-stage + distroless ~8 Mo 0-3 CVE

.dockerignore

# .dockerignore
.git
.github
.gitea
*.md
docs/
tests/
docker-compose*.yml
.env*
*.secret
node_modules/
__pycache__/
.pytest_cache/
coverage/
.vscode/
.idea/

Empecher les fuites de secrets

Le .dockerignore est la première ligne de defense contre l'inclusion accidentelle de fichiers .env, de cles SSH ou de tokens dans l'image. Le vérifier à chaque modification du Dockerfile.

Gestion des quotas

Dimensionner les quotas

Projet Quota stockage Justification
library 50 Go Images de base, peu de versions
app-backend 20 Go 10 versions × ~200 Mo apres dedup
app-frontend 20 Go 10 versions × ~150 Mo apres dedup
charts 5 Go Charts Helm legers
mirror 100 Go Miroir d'images publiques

Surveiller l'utilisation

# Lister l'utilisation des quotas par projet
curl -s "https://harbor.example.com/api/v2.0/quotas" \
  -u admin:ADMIN_PASSWORD | \
  jq '.[] | {
    project: .ref.name,
    used_gb: (.used.storage / 1073741824 | floor),
    limit_gb: (.hard.storage / 1073741824 | floor),
    pct: ((.used.storage / .hard.storage) * 100 | floor)
  }'

Migration vers Harbor

Depuis Docker Registry (distribution)

#!/bin/bash
# /home/ops/scripts/migrate-from-distribution.sh
set -euo pipefail

SOURCE="old-registry.example.com:5000"
TARGET="harbor.example.com"
PROJECT="library"

# Lister les repositories sur l'ancien registre
REPOS=$(curl -s "https://${SOURCE}/v2/_catalog" | jq -r '.repositories[]')

for REPO in ${REPOS}; do
  echo "Migrating ${REPO}..."

  # Lister les tags
  TAGS=$(curl -s "https://${SOURCE}/v2/${REPO}/tags/list" | jq -r '.tags[]')

  for TAG in ${TAGS}; do
    echo "  ${REPO}:${TAG}"

    # Copier avec skopeo (plus efficace que pull+push)
    skopeo copy \
      --src-tls-verify=true \
      --dest-tls-verify=true \
      --dest-creds "robot-migration:TOKEN" \
      "docker://${SOURCE}/${REPO}:${TAG}" \
      "docker://${TARGET}/${PROJECT}/${REPO}:${TAG}"
  done
done

echo "Migration complete."

Depuis Nexus Repository

# Migrer les images Docker depuis Nexus
skopeo copy \
  --src-creds "nexus-user:PASSWORD" \
  --dest-creds "robot-migration:TOKEN" \
  "docker://nexus.example.com/docker-hosted/myapp:v1.0" \
  "docker://harbor.example.com/app-backend/myapp:v1.0"

# Migrer les charts Helm depuis Nexus
# 1. Telecharger depuis Nexus
helm pull myapp --repo https://nexus.example.com/repository/helm-hosted/ --version 1.0.0

# 2. Pousser vers Harbor OCI
helm push myapp-1.0.0.tgz oci://harbor.example.com/charts

Skopeo pour les migrations

skopeo copy transfere les images directement entre registres sans les telecharger localement. C'est nettement plus rapide et economise de la bande passante.

Plan de migration

Phase Duree Actions
1. Préparation 1 semaine Déployer Harbor, créer les projets, configurer RBAC
2. Migration images 1-2 jours Script skopeo, vérifier les digests
3. Double push 2 semaines CI/CD pousse vers les deux registres
4. Bascule pull 1 jour Kubernetes pointe vers Harbor
5. Arrêt ancien 1 semaine Vérifier aucun pull sur l'ancien, decomissionner

Troubleshooting

Push failures

Symptome Cause probable Solution
unauthorized: authentication required Token expire ou scope insuffisant Renouveler le token robot, vérifier le scope
denied: requested access is not authorized Pas de permission push sur le projet Ajouter le rôle Developer ou Maintainer
manifest invalid Image corrompue ou format non supporte Reconstruire l'image, vérifier le Dockerfile
blob upload unknown Timeout sur un layer volumineux Augmenter le timeout, vérifier le réseau
quota exceeded Quota de stockage du projet atteint Augmenter le quota ou nettoyer les anciennes images

Scan timeouts

# Verifier l'etat du scanner
curl -s "https://harbor.example.com/api/v2.0/scanners" \
  -u admin:ADMIN_PASSWORD | jq '.[].health'

# Verifier les logs Trivy
kubectl logs -n harbor -l component=trivy --tail=50

# Causes frequentes :
# - Base CVE non telechargee (premier demarrage) → attendre 10-15 min
# - Image tres volumineuse (> 2 Go) → augmenter le timeout a 600s
# - Trivy en OOM → augmenter les limites memoire

Réplication conflicts

Symptome Cause Solution
conflict: tag already exists Tag immutable sur la destination Supprimer la regle d'immutabilite sur le tag concerne ou utiliser override: true
status: failed, network error Connectivité réseau perdue Vérifier les regles de firewall, DNS, TLS
unauthorized on destination Robot token expire sur la cible Renouveler le token robot sur le registre cible
execution stuck in InProgress Job service sature Vérifier les logs du job service, augmenter max_job_workers

Croissance du stockage

# Identifier les projets les plus volumineux
curl -s "https://harbor.example.com/api/v2.0/quotas" \
  -u admin:ADMIN_PASSWORD | \
  jq 'sort_by(-.used.storage) | .[:5] | .[] | {
    project: .ref.name,
    used_gb: (.used.storage / 1073741824 * 100 | floor / 100)
  }'

# Identifier les images les plus volumineuses dans un projet
curl -s "https://harbor.example.com/api/v2.0/projects/app-backend/repositories?page_size=100" \
  -u admin:ADMIN_PASSWORD | \
  jq 'sort_by(-.artifact_count) | .[:10] | .[] | {
    name: .name,
    artifact_count: .artifact_count
  }'

# Actions correctives :
# 1. Verifier la politique de retention (trop de versions gardees ?)
# 2. Lancer un GC dry-run pour estimer le gain
# 3. Identifier les images sans tag (untagged) et les supprimer
# 4. Verifier les .dockerignore (fichiers inutiles dans les images ?)
# 5. Passer a des images de base plus legeres (distroless)

Checklist de maintenance

Tâche Fréquence Automatisee
Garbage collection Hebdomadaire Oui
Mise à jour base CVE Trivy Quotidienne Oui
Revue des quotas Mensuelle Non
Rotation des tokens robot 90 jours Oui
Test de réplication DR Trimestrielle Non
Upgrade Harbor Trimestrielle Non
Revue des CVE allowlists Mensuelle Non
Revue des accès (rôles, robots) Trimestrielle Non
Vérification des backups PostgreSQL Mensuelle Non
Nettoyage des images untagged Hebdomadaire Oui