Aller au contenu

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.