# Example 6: Comprehensive Multi-Service Deployment # # This example demonstrates a complete application stack with: # - PostgreSQL (rotating with previous version) # - Redis (rotating without previous version) # - External API key (no rotation) # - TLS certificates (no rotation) # - Application credentials (rotating with Reloader) # # Shows different rotation strategies working together --- apiVersion: v1 kind: Namespace metadata: name: myapp --- # ============================================================================ # 1. PostgreSQL Credentials - Rotates with previous version support # ============================================================================ apiVersion: secrets.c5ai.ch/v1alpha1 kind: ManagedSecret metadata: name: postgres-credentials namespace: myapp spec: vault: address: "http://openbao.openbao.svc.cluster.local:8200" authMethod: kubernetes role: managedsecret-operator kvVersion: v2 mount: secret path: myapp/postgres fields: - name: username type: static value: "myapp_user" - name: password type: generated generator: type: password length: 32 minDigits: 6 minSymbols: 6 - name: host type: static value: "postgres.database.svc.cluster.local" - name: port type: static value: "5432" - name: database type: static value: "myapp_db" destination: name: postgres-creds type: Opaque keepPreviousVersion: true # Needs old password for rotation previousVersionTTL: 1h rotation: enabled: true schedule: 2160h # 90 days --- # ============================================================================ # 2. Redis Password - Rotates without previous version (CONFIG SET) # ============================================================================ apiVersion: secrets.c5ai.ch/v1alpha1 kind: ManagedSecret metadata: name: redis-credentials namespace: myapp spec: vault: address: "http://openbao.openbao.svc.cluster.local:8200" authMethod: kubernetes role: managedsecret-operator kvVersion: v2 mount: secret path: myapp/redis fields: - name: password type: generated generator: type: password length: 40 minDigits: 8 minSymbols: 0 # Redis doesn't like special chars symbolCharacters: "" - name: host type: static value: "redis.myapp.svc.cluster.local" - name: port type: static value: "6379" destination: name: redis-creds type: Opaque # No previous version needed - Redis CONFIG SET doesn't require auth rotation: enabled: true schedule: 720h # 30 days --- # ============================================================================ # 3. External API Keys - No rotation # ============================================================================ apiVersion: secrets.c5ai.ch/v1alpha1 kind: ManagedSecret metadata: name: external-api-keys namespace: myapp spec: vault: address: "http://openbao.openbao.svc.cluster.local:8200" authMethod: kubernetes role: managedsecret-operator kvVersion: v2 mount: secret path: myapp/api-keys fields: - name: stripe-api-key type: generated generator: type: password length: 64 minDigits: 0 minSymbols: 0 - name: sendgrid-api-key type: generated generator: type: password length: 64 minDigits: 0 minSymbols: 0 destination: name: api-keys type: Opaque rotation: enabled: false # Managed externally --- # ============================================================================ # 4. TLS Certificate - No rotation (managed by cert-manager) # ============================================================================ apiVersion: secrets.c5ai.ch/v1alpha1 kind: ManagedSecret metadata: name: app-tls-cert namespace: myapp spec: vault: address: "http://openbao.openbao.svc.cluster.local:8200" authMethod: kubernetes role: managedsecret-operator kvVersion: v2 mount: secret path: myapp/tls fields: - name: tls.crt type: static value: | -----BEGIN CERTIFICATE----- # Your certificate here -----END CERTIFICATE----- - name: tls.key type: static value: | -----BEGIN PRIVATE KEY----- # Your private key here -----END PRIVATE KEY----- destination: name: app-tls type: kubernetes.io/tls rotation: enabled: false --- # ============================================================================ # 5. Application JWT Secret - Rotates with Reloader # ============================================================================ apiVersion: secrets.c5ai.ch/v1alpha1 kind: ManagedSecret metadata: name: app-jwt-secret namespace: myapp spec: vault: address: "http://openbao.openbao.svc.cluster.local:8200" authMethod: kubernetes role: managedsecret-operator kvVersion: v2 mount: secret path: myapp/jwt fields: - name: jwt-secret type: generated generator: type: password length: 64 minDigits: 16 minSymbols: 0 symbolCharacters: "" destination: name: jwt-secret type: Opaque rotation: enabled: true schedule: 4320h # 180 days --- # ============================================================================ # PostgreSQL Password Rotation Job # ============================================================================ apiVersion: batch/v1 kind: CronJob metadata: name: postgres-password-rotator namespace: myapp spec: schedule: "*/5 * * * *" jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: rotate image: postgres:15-alpine env: - name: PGUSER valueFrom: secretKeyRef: name: postgres-creds key: username - name: NEW_PASSWORD valueFrom: secretKeyRef: name: postgres-creds key: password - name: OLD_PASSWORD valueFrom: secretKeyRef: name: postgres-creds-previous key: password optional: true - name: PGHOST valueFrom: secretKeyRef: name: postgres-creds key: host command: - sh - -c - | if [ -n "$OLD_PASSWORD" ] && [ "$OLD_PASSWORD" != "$NEW_PASSWORD" ]; then echo "Rotating PostgreSQL password..." PGPASSWORD=$OLD_PASSWORD psql -c "ALTER USER $PGUSER PASSWORD '$NEW_PASSWORD';" echo "Rotation complete!" fi --- # ============================================================================ # Redis Password Rotation Job # ============================================================================ apiVersion: batch/v1 kind: CronJob metadata: name: redis-password-rotator namespace: myapp spec: schedule: "*/5 * * * *" jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: rotate image: redis:7-alpine env: - name: REDIS_HOST valueFrom: secretKeyRef: name: redis-creds key: host - name: REDIS_PORT valueFrom: secretKeyRef: name: redis-creds key: port - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: redis-creds key: password command: - sh - -c - | # Redis CONFIG SET doesn't require current password redis-cli -h $REDIS_HOST -p $REDIS_PORT CONFIG SET requirepass "$REDIS_PASSWORD" echo "Redis password updated" --- # ============================================================================ # Main Application Deployment # ============================================================================ apiVersion: apps/v1 kind: Deployment metadata: name: myapp namespace: myapp spec: replicas: 3 selector: matchLabels: app: myapp template: metadata: labels: app: myapp annotations: # Reloader restarts pods when ANY of these secrets change reloader.stakater.com/auto: "true" spec: containers: - name: app image: myapp:latest ports: - containerPort: 8080 name: http env: # PostgreSQL connection - name: POSTGRES_HOST valueFrom: secretKeyRef: name: postgres-creds key: host - name: POSTGRES_PORT valueFrom: secretKeyRef: name: postgres-creds key: port - name: POSTGRES_DB valueFrom: secretKeyRef: name: postgres-creds key: database - name: POSTGRES_USER valueFrom: secretKeyRef: name: postgres-creds key: username - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgres-creds key: password # Redis connection - name: REDIS_HOST valueFrom: secretKeyRef: name: redis-creds key: host - name: REDIS_PORT valueFrom: secretKeyRef: name: redis-creds key: port - name: REDIS_PASSWORD valueFrom: secretKeyRef: name: redis-creds key: password # API Keys - name: STRIPE_API_KEY valueFrom: secretKeyRef: name: api-keys key: stripe-api-key - name: SENDGRID_API_KEY valueFrom: secretKeyRef: name: api-keys key: sendgrid-api-key # JWT Secret - name: JWT_SECRET valueFrom: secretKeyRef: name: jwt-secret key: jwt-secret volumeMounts: - name: tls mountPath: /etc/tls readOnly: true livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi volumes: - name: tls secret: secretName: app-tls --- # ============================================================================ # Service # ============================================================================ apiVersion: v1 kind: Service metadata: name: myapp namespace: myapp spec: selector: app: myapp ports: - port: 80 targetPort: 8080 name: http --- # ============================================================================ # Ingress with TLS # ============================================================================ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: myapp namespace: myapp annotations: cert-manager.io/cluster-issuer: letsencrypt-production spec: ingressClassName: nginx tls: - hosts: - myapp.c5ai.ch secretName: myapp-tls-ingress rules: - host: myapp.c5ai.ch http: paths: - path: / pathType: Prefix backend: service: name: myapp port: number: 80 --- # ============================================================================ # Summary of Rotation Strategies # ============================================================================ # # 1. PostgreSQL (postgres-creds): # - Rotates every 90 days # - keepPreviousVersion: true # - CronJob uses old password to authenticate and change # - Reloader restarts app pods after rotation # # 2. Redis (redis-creds): # - Rotates every 30 days # - No previous version needed (CONFIG SET doesn't require auth) # - CronJob updates password directly # - Reloader restarts app pods after rotation # # 3. API Keys (api-keys): # - No automatic rotation # - Generated once, managed externally # - Update via kubectl or Vault UI when provider rotates keys # # 4. TLS Certificates (app-tls): # - No rotation # - Managed by cert-manager or external process # - Static content stored in Vault # # 5. JWT Secret (jwt-secret): # - Rotates every 180 days # - Simple rotation - just restart with new secret # - Reloader handles pod restart # # ============================================================================ # Timeline Example: What Happens on Day 90 # ============================================================================ # # T+0: ManagedSecret operator triggers rotation # - Generates new passwords for postgres-creds and redis-creds # - Updates Vault # - Creates postgres-creds-previous # - Updates postgres-creds # - Updates redis-creds (no previous needed) # # T+1min: CronJobs detect changes # - Postgres rotator: Uses old password to set new password in DB # - Redis rotator: Sets new password via CONFIG SET # # T+2min: Reloader detects secret changes # - Triggers rolling restart of myapp Deployment # - Pods start with new credentials # # T+1hour: Cleanup # - postgres-creds-previous automatically deleted # - Old credentials no longer accessible # # ============================================================================ # Monitoring Commands # ============================================================================ # # # Watch all secrets # kubectl get secrets -n myapp -w # # # Check rotation status # kubectl get managedsecrets -n myapp # # # View rotation logs # kubectl logs -n myapp -l job-name=postgres-password-rotator-XXXXX # kubectl logs -n myapp -l job-name=redis-password-rotator-XXXXX # # # Check Reloader activity # kubectl logs -n reloader -l app=reloader | grep myapp # # # Verify app pods restarted # kubectl get pods -n myapp -o wide # # # Test connections with new credentials # kubectl exec -it -n myapp deployment/myapp -- sh # # psql -h $POSTGRES_HOST -U $POSTGRES_USER -d $POSTGRES_DB # # redis-cli -h $REDIS_HOST -a $REDIS_PASSWORD ping # # ============================================================================ # Force Rotation for Testing # ============================================================================ # # # Force PostgreSQL rotation # kubectl annotate managedsecret postgres-credentials -n myapp \ # reconcile="$(date +%s)" --overwrite # # # Force Redis rotation # kubectl annotate managedsecret redis-credentials -n myapp \ # reconcile="$(date +%s)" --overwrite # # # Force all rotations # kubectl annotate managedsecrets -n myapp --all \ # reconcile="$(date +%s)" --overwrite # # # Watch the cascade of events # kubectl get events -n myapp --sort-by='.lastTimestamp' -w