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 |