# Example 2: Container Registry with Rotation + Reloader # # Use case: Docker registry with basic auth that rotates every 90 days # # Characteristics: # - Password rotates every 90 days # - Registry doesn't need old password to accept new one # - InitContainer regenerates htpasswd on pod restart # - Reloader automatically restarts pod when secret changes # - No keepPreviousVersion needed --- apiVersion: v1 kind: Namespace metadata: name: registry --- # ManagedSecret generates and rotates registry credentials apiVersion: secrets.c5ai.ch/v1alpha1 kind: ManagedSecret metadata: name: registry-auth namespace: registry spec: vault: address: "http://openbao.openbao.svc.cluster.local:8200" authMethod: kubernetes role: managedsecret-operator kvVersion: v2 mount: secret path: registry/auth fields: - name: username type: static value: "admin" - name: password type: generated generator: type: password length: 32 minDigits: 5 minSymbols: 5 minLowercase: 5 minUppercase: 5 symbolCharacters: "!@#$%^&*" allowRepeat: false destination: name: registry-credentials type: Opaque # No need for previous version - htpasswd is regenerated fresh rotation: enabled: true schedule: 2160h # 90 days rotateGeneratedOnly: true # Only rotate password, keep username --- # PersistentVolumeClaim for registry storage apiVersion: v1 kind: PersistentVolumeClaim metadata: name: registry-pvc namespace: registry spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: longhorn --- # Registry Deployment with Reloader integration apiVersion: apps/v1 kind: Deployment metadata: name: registry namespace: registry labels: app: registry spec: replicas: 1 selector: matchLabels: app: registry template: metadata: labels: app: registry annotations: # Reloader watches registry-credentials and restarts pod on change reloader.stakater.com/auto: "true" spec: securityContext: runAsNonRoot: true runAsUser: 65534 fsGroup: 65534 seccompProfile: type: RuntimeDefault initContainers: # Generate htpasswd file from current credentials at pod startup - name: generate-htpasswd image: httpd:2.4-alpine command: - sh - -c - | USERNAME=$(cat /credentials/username) PASSWORD=$(cat /credentials/password) htpasswd -Bbn "$USERNAME" "$PASSWORD" > /auth/htpasswd chmod 644 /auth/htpasswd echo "htpasswd generated with user: $USERNAME" volumeMounts: - name: credentials mountPath: /credentials readOnly: true - name: auth mountPath: /auth securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 65534 capabilities: drop: - ALL containers: - name: registry image: registry:2 ports: - containerPort: 5000 name: http env: - name: REGISTRY_STORAGE_DELETE_ENABLED value: "true" - name: REGISTRY_HTTP_ADDR value: "0.0.0.0:5000" # Enable HTTP basic auth - name: REGISTRY_AUTH value: "htpasswd" - name: REGISTRY_AUTH_HTPASSWD_REALM value: "Registry Realm" - name: REGISTRY_AUTH_HTPASSWD_PATH value: "/auth/htpasswd" securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 65534 capabilities: drop: - ALL seccompProfile: type: RuntimeDefault volumeMounts: - name: registry-storage mountPath: /var/lib/registry - name: auth mountPath: /auth readOnly: true livenessProbe: httpGet: path: / port: 5000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: / port: 5000 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi volumes: - name: registry-storage persistentVolumeClaim: claimName: registry-pvc - name: credentials secret: secretName: registry-credentials - name: auth emptyDir: {} --- # Service for internal access apiVersion: v1 kind: Service metadata: name: registry namespace: registry spec: type: ClusterIP selector: app: registry ports: - port: 5000 targetPort: 5000 protocol: TCP name: http --- # Ingress for external access with TLS apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: registry namespace: registry annotations: cert-manager.io/cluster-issuer: letsencrypt-production nginx.ingress.kubernetes.io/proxy-body-size: "0" nginx.ingress.kubernetes.io/proxy-read-timeout: "600" nginx.ingress.kubernetes.io/proxy-send-timeout: "600" nginx.ingress.kubernetes.io/force-ssl-redirect: "true" nginx.ingress.kubernetes.io/backend-protocol: "HTTP" spec: ingressClassName: nginx tls: - hosts: - registry.c5ai.ch secretName: registry-ingress-tls rules: - host: registry.c5ai.ch http: paths: - path: / pathType: Prefix backend: service: name: registry port: number: 5000 --- # How the rotation flow works: # # 1. After 90 days, ManagedSecret operator: # - Generates new password # - Updates Vault # - Updates registry-credentials Secret # # 2. Reloader detects Secret change: # - Sees resourceVersion changed # - Triggers rolling restart of registry Deployment # # 3. Pod restart with new credentials: # - InitContainer runs # - Reads NEW password from registry-credentials # - Generates fresh htpasswd file # - Registry container starts with new htpasswd # # 4. Result: # - Old clients with old password: FAIL (401 Unauthorized) # - New clients with new password: SUCCESS # - Downtime: ~1-2 minutes during rolling restart --- # Testing commands: # # # Get current credentials # kubectl get secret registry-credentials -n registry -o jsonpath='{.data.username}' | base64 -d # kubectl get secret registry-credentials -n registry -o jsonpath='{.data.password}' | base64 -d # # # Test without auth (should fail) # curl -i https://registry.c5ai.ch/v2/ # # # Test with auth (should succeed) # REGISTRY_USER=$(kubectl get secret registry-credentials -n registry -o jsonpath='{.data.username}' | base64 -d) # REGISTRY_PASS=$(kubectl get secret registry-credentials -n registry -o jsonpath='{.data.password}' | base64 -d) # curl -u $REGISTRY_USER:$REGISTRY_PASS https://registry.c5ai.ch/v2/_catalog # # # Docker login # docker login registry.c5ai.ch -u $REGISTRY_USER -p $REGISTRY_PASS # # # Force rotation test # kubectl annotate managedsecret registry-auth -n registry reconcile="$(date +%s)" --overwrite # kubectl get pods -n registry -w # Watch pod restart