Registry Docker avec TLS

Registry privé auto-signé déployé sur Kubernetes avec Traefik IngressRoute et authentification basic auth

Importer manuellement chaque image sur chaque nœud du cluster est rapidement impraticable. La solution : un registry privé hébergé dans le cluster, accessible en HTTPS avec un certificat auto-signé et protégé par mot de passe.

Installation de Docker

Docker est installé sur cube01 pour construire et pousser des images multi-architecture depuis le réseau local.

sudo apt-get install ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

Configuration du daemon (/etc/docker/daemon.json) :

{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "experimental": true,
  "log-driver": "json-file",
  "storage-driver": "overlay2",
  "log-opts": {
    "max-size": "100m"
  }
}
sudo systemctl enable docker
sudo systemctl start docker

# Support multi-architecture (arm64 + amd64)
sudo apt-get install binfmt-support qemu-user-static
docker buildx create --use --platform=linux/arm64,linux/amd64 --name multi-platform-builder
docker buildx inspect --bootstrap

# Ajouter l'utilisateur courant au groupe docker
sudo groupadd docker
sudo usermod -aG docker $USER

Mise en place du registry HTTP (étape initiale)

Dans un premier temps, le registry est déployé sans TLS pour valider le fonctionnement de base. Les manifestes Kubernetes créent un namespace dédié, un PVC, un Deployment, un Service et un Ingress.

Sur chaque nœud, ajouter le registry à /etc/hosts :

<IP-cube04>    registry    docker-registry.local

Puis créer /etc/rancher/k3s/registries.yaml pour que containerd connaisse le mirror :

mirrors:
  docker-registry:
    endpoint:
      - "http://docker-registry.local:80"

Test de validation :

curl http://docker-registry.local:80/v2/_catalog
# Retourne la liste des dépôts

Passage en TLS avec CA locale

.cluster n’est pas un domaine public, Let’s Encrypt ne peut pas émettre de certificat. On crée donc une CA interne et on signe soi-même le certificat du registry.

Génération de la CA et du certificat

mkdir registry-ca && cd registry-ca

# Clé et certificat CA (valable 10 ans)
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes \
  -key ca.key -sha256 -days 3650 \
  -out ca.crt -subj "/CN=Cluster Registry CA"

# Clé et CSR pour registry.cluster
openssl genrsa -out registry.key 4096
openssl req -new -key registry.key -out registry.csr -subj "/CN=registry.cluster"

# Extension SAN
echo "subjectAltName = DNS:registry.cluster" > registry.ext

# Signature du certificat
openssl x509 -req \
  -in registry.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out registry.crt -days 3650 -sha256 -extfile registry.ext

Déploiement de la CA sur toutes les machines

Sur le poste de travail et sur chaque nœud Raspberry Pi :

sudo cp ca.crt /usr/local/share/ca-certificates/cluster-ca.crt
sudo update-ca-certificates
# Sur les nœuds seulement :
sudo systemctl restart containerd

Ressources Kubernetes

Namespace et secret TLS

kubectl create namespace registry
kubectl create secret tls registry-tls \
  --cert=registry.crt --key=registry.key -n registry

Authentification basic auth

sudo apt install apache2-utils
htpasswd -Bc htpasswd admin
kubectl create secret generic registry-auth \
  --from-file=htpasswd -n registry

PVC (stockage Longhorn)

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: registry-pvc
  namespace: registry
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 20Gi
  storageClassName: longhorn

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: registry
  namespace: registry
spec:
  replicas: 1
  selector:
    matchLabels:
      app: registry
  template:
    metadata:
      labels:
        app: registry
    spec:
      containers:
        - name: registry
          image: registry:2
          ports:
            - containerPort: 5000
          env:
            - name: REGISTRY_HTTP_ADDR
              value: "0.0.0.0:5000"
            - name: REGISTRY_AUTH
              value: "htpasswd"
            - name: REGISTRY_AUTH_HTPASSWD_REALM
              value: "Registry Realm"
            - name: REGISTRY_AUTH_HTPASSWD_PATH
              value: "/auth/htpasswd"
          volumeMounts:
            - name: storage
              mountPath: /var/lib/registry
            - name: auth
              mountPath: /auth
      volumes:
        - name: storage
          persistentVolumeClaim:
            claimName: registry-pvc
        - name: auth
          secret:
            secretName: registry-auth

Service

apiVersion: v1
kind: Service
metadata:
  name: registry
  namespace: registry
spec:
  selector:
    app: registry
  ports:
    - port: 5000
      targetPort: 5000

IngressRoute Traefik (HTTPS)

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: registry
  namespace: registry
spec:
  entryPoints:
    - websecure
  routes:
    - match: Host(`registry.cluster`)
      kind: Rule
      services:
        - name: registry
          port: 5000
  tls:
    secretName: registry-tls

Configuration de k3s sur tous les nœuds

Éditer /etc/rancher/k3s/registries.yaml sur chaque nœud :

mirrors:
  registry.cluster:
    endpoint:
      - "https://registry.cluster"

configs:
  registry.cluster:
    auth:
      username: admin
      password: <password>
    tls:
      ca_file: /usr/local/share/ca-certificates/cluster-ca.crt
# Sur le control plane
sudo systemctl restart k3s
# Sur les workers
sudo systemctl restart k3s-agent

Tests

# Connexion
docker login registry.cluster

# Tag et push
docker tag nginx registry.cluster/nginx:test
docker push registry.cluster/nginx:test

# Vérification via l'API
curl -u admin:<password> https://registry.cluster/v2/_catalog

La réponse {"errors":[{"code":"UNAUTHORIZED"...}]} sans authentification confirme que TLS fonctionne et que le registry est correctement protégé.

Dans les manifestes Kubernetes, les images se référencent directement :

image: registry.cluster/myapp:latest

k3s résout le nom et tire l’image depuis le registry privé avec les credentials configurés.