Integration¶
CI/CD : pousser les images depuis Gitea Actions¶
Workflow de build et push¶
# .gitea/workflows/build-push.yaml
name: Build and Push
on:
push:
tags:
- 'v*'
env:
REGISTRY: harbor.example.com
PROJECT: app-backend
IMAGE: api
jobs:
build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.HARBOR_ROBOT_USER }}
password: ${{ secrets.HARBOR_ROBOT_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-,format=short
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE }}:buildcache,mode=max
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
output-file: sbom.spdx.json
- name: Sign image with Cosign
uses: sigstore/cosign-installer@v3
- name: Sign
env:
COSIGN_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
# Signer chaque tag
for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n'); do
cosign sign --key env://COSIGN_KEY "${tag}"
done
# Attacher le SBOM
cosign attach sbom \
--sbom sbom.spdx.json \
"${{ env.REGISTRY }}/${{ env.PROJECT }}/${{ env.IMAGE }}:${{ steps.meta.outputs.version }}"
Secrets Gitea a configurer¶
| Secret | Contenu | Rotation |
|---|---|---|
HARBOR_ROBOT_USER | Nom du robot account Harbor | N/A |
HARBOR_ROBOT_TOKEN | Token du robot account | 90 jours |
COSIGN_PRIVATE_KEY | Cle privee Cosign (PEM) | 1 an |
COSIGN_PASSWORD | Passphrase de la cle Cosign | 1 an |
Kubernetes : imagePullSecrets¶
Créer le secret Docker¶
# Creer le secret pour tirer les images depuis Harbor
kubectl create secret docker-registry harbor-pull \
--docker-server=harbor.example.com \
--docker-username=robot-k8s-pull \
--docker-password=ROBOT_TOKEN \
-n app-backend
Configurer le ServiceAccount¶
# serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-backend
namespace: app-backend
imagePullSecrets:
- name: harbor-pull
ServiceAccount plutôt que Pod spec
Configurer imagePullSecrets sur le ServiceAccount plutôt que sur chaque Pod. Tous les Pods utilisant ce ServiceAccount heritent automatiquement du secret.
Utilisation dans un Deployment¶
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: app-backend
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
serviceAccountName: app-backend
containers:
- name: api
# Utiliser le digest pour l'immutabilite
image: harbor.example.com/app-backend/api@sha256:abc123...
ports:
- containerPort: 8080
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 1000m
memory: 1Gi
Automatiser la rotation des tokens¶
#!/bin/bash
# /home/ops/scripts/rotate-harbor-pull-secret.sh
set -euo pipefail
NAMESPACES="app-backend app-frontend"
ROBOT_NAME="robot-k8s-pull"
# Renouveler le token robot via l'API Harbor
NEW_TOKEN=$(curl -s -X PATCH \
"https://harbor.example.com/api/v2.0/robots/${ROBOT_ID}" \
-H "Content-Type: application/json" \
-u admin:ADMIN_PASSWORD \
-d '{"secret": ""}' | jq -r '.secret')
# Mettre a jour le secret dans chaque namespace
for NS in ${NAMESPACES}; do
kubectl create secret docker-registry harbor-pull \
--docker-server=harbor.example.com \
--docker-username="${ROBOT_NAME}" \
--docker-password="${NEW_TOKEN}" \
-n "${NS}" \
--dry-run=client -o yaml | kubectl apply -f -
# Redemarrer les pods pour prendre en compte le nouveau token
kubectl rollout restart deployment -n "${NS}"
done
echo "Harbor pull secret rotated in: ${NAMESPACES}"
Signature Cosign dans le pipeline¶
Flux de signature complet¶
Build image → Push → Scan (Trivy) → Sign (Cosign) → Attacher SBOM
│
Push tag signe
│
Deploiement autorise
Générer les cles Cosign¶
# Generer une paire de cles (a faire une seule fois)
cosign generate-key-pair
# Stocker la cle privee dans Vault
vault kv put secret/cosign/signing-key \
private_key=@cosign.key \
password="COSIGN_PASSWORD"
# Distribuer la cle publique aux clusters Kubernetes
kubectl create configmap cosign-pub \
--from-file=cosign.pub=cosign.pub \
-n gatekeeper-system
Vérifier une signature manuellement¶
# Verifier qu'une image est signee
cosign verify --key cosign.pub \
harbor.example.com/app-backend/api:v3.0.1
# Sortie attendue :
# Verification for harbor.example.com/app-backend/api:v3.0.1 --
# The following checks were performed on each of these signatures:
# - The cosign claims were validated
# - The signatures were verified against the specified public key
Admission control : uniquement des images signees¶
OPA Gatekeeper : politique de signature¶
# constraint-template.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredimagsigning
spec:
crd:
spec:
names:
kind: K8sRequiredImageSigning
validation:
openAPIV3Schema:
type: object
properties:
registries:
type: array
items:
type: string
cosignKey:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredimagsigning
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
registry := input.parameters.registries[_]
startswith(container.image, registry)
not image_signed(container.image)
msg := sprintf("Image %v is not signed with Cosign", [container.image])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
registry := input.parameters.registries[_]
startswith(container.image, registry)
not image_signed(container.image)
msg := sprintf("Init container image %v is not signed with Cosign", [container.image])
}
image_signed(image) {
# Verification via external data provider ou webhook
external_data.cosign_verified[image]
}
# constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredImageSigning
metadata:
name: require-cosign-signature
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment", "StatefulSet", "DaemonSet"]
namespaces:
- app-backend
- app-frontend
parameters:
registries:
- "harbor.example.com"
Kyverno comme alternative
Kyverno offre une vérification native des signatures Cosign sans external data provider, ce qui simplifie le déploiement :
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:
- "harbor.example.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
Metriques Grafana¶
Metriques exposees par Harbor¶
Harbor expose des metriques Prometheus sur le port 9090 :
| Metrique | Type | Description |
|---|---|---|
harbor_project_total | Gauge | Nombre total de projets |
harbor_project_repo_total | Gauge | Nombre de repositories par projet |
harbor_project_member_total | Gauge | Nombre de membres par projet |
harbor_project_quota_usage_byte | Gauge | Utilisation du quota en octets |
harbor_artifact_pulled | Counter | Nombre de pulls d'artefacts |
harbor_artifact_pushed | Counter | Nombre de push d'artefacts |
harbor_scanner_scan_total | Counter | Nombre total de scans |
harbor_scanner_vuln_total | Gauge | Nombre de vulnerabilites par sévérité |
harbor_replication_transfer_total | Counter | Transferts de réplication |
harbor_gc_collected_blobs | Counter | Blobs collectes par le GC |
Dashboard Grafana¶
{
"title": "Harbor Registry",
"panels": [
{
"title": "Storage Usage by Project",
"type": "bargauge",
"targets": [
{
"expr": "harbor_project_quota_usage_byte / 1073741824",
"legendFormat": "{{ project_name }}"
}
]
},
{
"title": "Pull/Push Rate",
"type": "timeseries",
"targets": [
{
"expr": "rate(harbor_artifact_pulled[5m])",
"legendFormat": "pulls/s"
},
{
"expr": "rate(harbor_artifact_pushed[5m])",
"legendFormat": "pushes/s"
}
]
},
{
"title": "Vulnerabilities by Severity",
"type": "piechart",
"targets": [
{
"expr": "harbor_scanner_vuln_total",
"legendFormat": "{{ severity }}"
}
]
}
]
}
Alertes recommandees¶
| Alerte | Condition | Sévérité | Action |
|---|---|---|---|
| Quota proche de la limite | quota_usage / quota_limit > 0.85 | Warning | Augmenter le quota ou nettoyer |
| Scan critique détecte | vuln_total{severity="Critical"} > 0 | Critical | Bloquer le déploiement |
| Réplication en echec | replication_status == "failed" | Warning | Vérifier la connectivité |
| GC non execute depuis 7j | time() - gc_last_run > 604800 | Warning | Vérifier le job service |
| Espace stockage bas | minio_disk_usage / minio_disk_total > 0.9 | Critical | Étendre le stockage |
Webhook notifications¶
Configurer les webhooks Harbor¶
# Creer un webhook pour notifier sur les evenements de push et scan
curl -X POST "https://harbor.example.com/api/v2.0/projects/app-backend/webhook/policies" \
-H "Content-Type: application/json" \
-u admin:ADMIN_PASSWORD \
-d '{
"name": "notify-push-scan",
"targets": [
{
"type": "http",
"address": "https://webhook.example.com/harbor",
"skip_cert_verify": false
}
],
"event_types": [
"PUSH_ARTIFACT",
"SCANNING_COMPLETED",
"SCANNING_FAILED",
"TAG_RETENTION",
"REPLICATION"
],
"enabled": true
}'
Payload de notification (exemple push)¶
{
"type": "PUSH_ARTIFACT",
"occur_at": 1713264000,
"operator": "robot-ci-push",
"event_data": {
"resources": [
{
"digest": "sha256:abc123...",
"tag": "v3.0.1",
"resource_url": "harbor.example.com/app-backend/api:v3.0.1"
}
],
"repository": {
"name": "api",
"namespace": "app-backend",
"repo_full_name": "app-backend/api",
"repo_type": "private"
}
}
}
Les webhooks permettent de déclencher des actions automatiques : déploiement dans un environnement de staging, notification Slack, mise à jour d'un tableau de bord.