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):
| Annotation | Tipo | Descrição |
|---|---|---|
ciphervault.io/inject | bool | Liga injeção ("true") |
ciphervault.io/client-id | string | client_id da AppConnection |
ciphervault.io/secrets | CSV | vault/secret separados por vírgula |
ciphervault.io/volume | string | Nome do volume tmpfs (default cv-secrets) |
ciphervault.io/cv-url | URL | Endpoint do backend CV |
ciphervault.io/secret-name | string | Nome do K8s Secret a montar (alternativa ao tmpfs) |
ciphervault.io/refresh-interval | duração | 0 = init only; 5m = sidecar com refresh |
ciphervault.io/sidecar-image | imagem | Override 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
caBundlevalidado 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 mutationscv_sidecar_secret_refresh_total{result}— contador de refreshescv_sidecar_secret_age_seconds— gauge (idade do secret)cv_sidecar_reload_triggered_total{trigger}— contador (v3.0+, label:signaloucmd)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
| Sintoma | Diagnóstico |
|---|---|
| Pods sobem sem secrets | Verifique annotations exatas e webhook funcionando: kubectl get mutatingwebhookconfigurations |
| Webhook timeout | Aumentar failurePolicy.timeoutSeconds; verificar latência rede para cv-url |
403 IP_NOT_ALLOWED no sidecar | NAT do cluster não está na ip_allowlist da AppConnection |
| Cert mTLS expirando | cv CLI: cv app-connection rotate-cert --id app_... ou aguardar rotação automática |
| Container init em CrashLoopBackOff | Logs: 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:5432e o proxy injeta cred efêmera.