Pular para o conteúdo principal

Kubernetes Mutating Admission Webhook

A partir de v1.7.5, o CipherVault entrega secrets em pods via Mutating Admission Webhook + sidecar/initContainer leve. Pods anotados com ciphervault.io/inject: "true" recebem injeção automática.

Webhook vs. Operator — A partir da v4.1, há também um Kubernetes Operator nativo com 3 CRDs (CipherVaultSecret, CipherVaultLease, CipherVaultDynamicRole). Ambos coexistem: use Webhook quando você não controla os manifests (annotations no Pod); use Operator quando você quer GitOps puro com CRDs versionados, ou quando precisa de Federation multi-cluster.

Pré-requisitos

  • Cluster Kubernetes 1.28+ (sidecar containers nativos)
  • Helm 3.10+
  • AppConnection criada para o cluster (idealmente com mTLS + DPoP)

Instalação via Helm

helm repo add ciphervault https://charts.ciphervault.com.br
helm repo update

helm install ciphervault-injector ciphervault/injector \
--namespace ciphervault-system \
--create-namespace \
--set webhook.caBundle=$(kubectl get secret cv-webhook-tls -o jsonpath='{.data.ca\.crt}') \
--set ciphervault.url=https://cv.acme.com.br \
--set webhook.failurePolicy=Ignore

failurePolicy: Ignore é o padrão — webhook offline não bloqueia o cluster (deploys continuam, só não injetam secrets até o webhook voltar).

Annotations

Coloque na pod spec (spec.template.metadata.annotations):

AnnotationTipoDescrição
ciphervault.io/injectboolLiga injeção ("true")
ciphervault.io/client-idstringclient_id da AppConnection
ciphervault.io/secretsCSVvault/secret separados por vírgula
ciphervault.io/volumestringNome do volume tmpfs (default cv-secrets)
ciphervault.io/cv-urlURLEndpoint do backend CV
ciphervault.io/secret-namestringNome do K8s Secret a montar (alternativa ao tmpfs)
ciphervault.io/refresh-intervalduração0 = init only; 5m = sidecar com refresh
ciphervault.io/sidecar-imageimagemOverride da imagem do sidecar

Modos

init (refresh-interval: 0)

Init container popula tmpfs e termina. Pod sobe lendo do volume montado. Simples, sem sidecar.

apiVersion: apps/v1
kind: Deployment
metadata:
name: billing-api
spec:
template:
metadata:
annotations:
ciphervault.io/inject: "true"
ciphervault.io/client-id: "cv_app_01HXY..."
ciphervault.io/secrets: "producao/api/stripe/secret_key,producao/db/postgres/billing-master"
ciphervault.io/refresh-interval: "0"
ciphervault.io/cv-url: "https://cv.acme.com.br"
spec:
containers:
- name: api
image: acme/billing-api:1.4.0
# /run/cv/secrets/* contém os secrets em arquivos
env:
- name: CV_SECRETS_DIR
value: /run/cv/secrets

sidecar (refresh-interval ≥ 1m)

Sidecar K8s 1.28+ (restartPolicy: Always) faz refresh atomico via tmp+rename — apps que reabrem o arquivo sempre veem a versão atual. Sem rolling-restart em rotação de secret.

metadata:
annotations:
ciphervault.io/inject: "true"
ciphervault.io/client-id: "cv_app_01HXY..."
ciphervault.io/secrets: "producao/api/openai/key,producao/api/stripe/*"
ciphervault.io/refresh-interval: "2m"
ciphervault.io/cv-url: "https://cv.acme.com.br"

Reload hooks (v3.0+)

Quando o sidecar detecta rotação (hash-based, comparando file content), pode disparar trigger de reload no app sem rolling-restart:

spec:
containers:
- name: nginx
image: nginx:1.25
env:
# Apps que suportam SIGHUP (nginx, haproxy)
- name: CV_RELOAD_SIGNAL
value: SIGHUP

# OU comando custom
- name: CV_RELOAD_CMD
value: "/usr/local/bin/reload-config.sh"

# Debounce para múltiplas rotações próximas
- name: CV_RELOAD_DEBOUNCE_MS
value: "5000"

SIGHUP é o default para sinal. CV_RELOAD_CMD tem precedência sobre CV_RELOAD_SIGNAL quando ambos definidos.

O que o webhook faz (JSONPatch RFC 6902)

1. + emptyDir volume tmpfs (memory-backed)
2. + initContainer OR + sidecar container (com restartPolicy: Always)
3. + volumeMount read-only nos containers da pod

Sidecar/init lê a AppConnection do client-id, autentica no CV via mTLS (certs montados no volume cv-creds), busca secrets e escreve em /run/cv/secrets/<vault>/<secret>.

Segurança

  • Sidecar non-root (uid 65534), RO root FS, sem capabilities, sem privesc
  • Volume tmpfs read-only no app (apenas o sidecar escreve)
  • Webhook TLS com caBundle validado
  • failurePolicy: Ignore — offline não bloqueia cluster
  • Compatível com Pod Security Standard restricted

Observabilidade

Métricas Prometheus expostas em :8080/metrics (sidecar e webhook), gated por CV_METRICS_ENABLED=true:

  • cv_sidecar_admissions_total{result} — contador de mutations
  • cv_sidecar_secret_refresh_total{result} — contador de refreshes
  • cv_sidecar_secret_age_seconds — gauge (idade do secret)
  • cv_sidecar_reload_triggered_total{trigger} — contador (v3.0+, label: signal ou cmd)
  • cv_sidecar_errors_total{type} — contador (v3.0+)

ServiceMonitor (Prometheus Operator):

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: ciphervault-injector
namespace: ciphervault-system
spec:
selector:
matchLabels:
app: ciphervault-injector
endpoints:
- port: metrics
interval: 30s

Troubleshooting

SintomaDiagnóstico
Pods sobem sem secretsVerifique annotations exatas e webhook funcionando: kubectl get mutatingwebhookconfigurations
Webhook timeoutAumentar failurePolicy.timeoutSeconds; verificar latência rede para cv-url
403 IP_NOT_ALLOWED no sidecarNAT do cluster não está na ip_allowlist da AppConnection
Cert mTLS expirandocv CLI: cv app-connection rotate-cert --id app_... ou aguardar rotação automática
Container init em CrashLoopBackOffLogs: kubectl logs -c cv-init <pod>. Geralmente policy não permite secret request

Boas práticas

  • AppConnection por cluster, não compartilhar entre clusters — isola blast radius.
  • Annotations no Deployment, não no Pod direto — sobrevive a kubectl rollout.
  • NetworkPolicy obrigatória: egress do sidecar apenas pra cv.acme.com.br + DNS.
  • PSA restricted — testado e suportado.
  • Secretless Proxy (sidecar Postgres) é alternativa quando você quer zero arquivo de secret no pod — app conecta em localhost:5432 e o proxy injeta cred efêmera.