Pular para o conteúdo principal

Secretless Proxy

O Secretless Proxy é um sidecar Go que intercepta a conexão da aplicação ao banco/cache, pede um lease dinâmico no CV, conecta no upstream com a credencial efêmera, e faz bridge byte-puro bidirecional.

A app nunca toca a credencial — conecta em localhost:<porta> com user/pass arbitrários, e o proxy injeta a cred real transparentemente.

A v1.9 introduziu o proxy com Postgres apenas. A v3.0 expandiu para MySQL, Redis e MongoDB, adicionou TLS upstream e lease pool.

Por que

Modelo padrão (lease via SDK)Secretless Proxy
App importa SDK CVApp não muda — driver Postgres normal
App pede lease, recebe user/pass, configura connectionApp conecta localhost:5432 user=foo pass=bar
Lease vive no env/memória da appLease vive só no proxy
Logs da app podem vazar credencialApp nunca vê credencial real
Reconectar = pedir novo leaseProxy pede lease e gerencia transparentemente

Trade-off: uma rede hop a mais + complexidade de deploy do sidecar.

Setup

1. Pré-requisitos no CipherVault

  • Backend Postgres + Role criadas em Dynamic Secrets
  • AppConnection com permissão dynamic:request_lease no role

2. Subir proxy

secretless-proxy \
--listen :5432 \
--upstream db-billing.internal.acme.com.br:5432 \
--protocol postgres \
--upstream-tls \
--upstream-ca-file /etc/ssl/cv-internal-ca.pem \
--lease-pool-size 5 \
--cv-url https://cv.acme.com.br \
--conn-id app_01HXY... \
--role-id 42 \
--cert ./client.crt \
--key ./client.key \
--sig-key ./sig.key

Protocolos suportados (v3.0+)

Protocolo--protocolAuth no upstream
PostgrespostgresStartupMessage intercept (CleartextPassword, MD5, SCRAM-SHA-256)
MySQLmysqlHandshake v10, nativePasswordAuth (SHA1-XOR), HandshakeResponse41 rewrite
RedisredisAUTH no upstream com cred do lease, intercepta AUTH do client (responde +OK fake)
MongoDBmongoSCRAM-SHA-256 client (PBKDF2 + HMAC RFC 7677), wire framing OP_MSG/OP_QUERY

Exemplo Kubernetes (MySQL)

apiVersion: apps/v1
kind: Deployment
metadata:
name: billing-api
spec:
template:
spec:
containers:
- name: api
image: acme/billing-api:2.0.0
env:
- name: DATABASE_URL
# App conecta no proxy local
value: mysql://dummy:dummy@localhost:3306/billing

- name: secretless-proxy
image: ciphervault/secretless-proxy:3.0
args:
- --listen=:3306
- --upstream=mysql-prd.internal.acme.com.br:3306
- --protocol=mysql
- --upstream-tls
- --lease-pool-size=5
- --cv-url=https://cv.acme.com.br
- --conn-id=app_01HXY...
- --role-id=42
ports:
- { name: metrics, containerPort: 9090 }
volumeMounts:
- name: cv-creds
mountPath: /etc/cv
readOnly: true

volumes:
- name: cv-creds
secret:
secretName: cv-app-credentials

TLS upstream

FlagDescrição
--upstream-tlsLiga TLS na conexão proxy → upstream
--upstream-ca-file <path>CA bundle para validar cert do upstream
--upstream-tls-skip-verify⚠️ Apenas dev — bypass de validação

Lease pool (v3.0+)

Pré-cria leases idle para reduzir RTT do primeiro request:

Flag / configOndeDescrição
--lease-pool-size NproxyPool client-side de N leases pré-criados; drena no shutdown
role.lease_reuse_max_usesrole no CVMesma (role, app_connection) reaproveita lease server-side via SKIP LOCKED
dynamic_leases.use_counttabelaContador para cap de reuso

Resultado: primeiro request cai de ~100ms (lease cold) para ~5ms (lease warm).

3. App conecta normalmente

import psycopg2
conn = psycopg2.connect(
"postgres://anything:anything@localhost:5432/billing"
)
# user/pass na URL são ignorados — proxy os substitui

Como funciona

┌─────┐ Postgres wire ┌───────────────┐ Postgres wire ┌──────────┐
│ App │ ───────────────▶ │ secretless- │ ────────────────▶ │ Postgres │
│ │ user=foo │ proxy │ user=cv_lease.. │ │
│ │ pass=bar │ (sidecar) │ pass=<gerada> │ │
└─────┘ └───────┬───────┘ └──────────┘
│ HTTP+mTLS

┌───────────────────┐
│ CipherVault │
│ POST /lease │
│ role-id=42 │
└───────────────────┘
  1. App abre TCP em localhost:5432
  2. Proxy intercepta Postgres StartupMessage
  3. Proxy chama POST /dynamic-secrets/leases no CV via mTLS+DPoP, recebe {username, password, expires_at}
  4. Proxy abre conexão upstream com username/password reais
  5. Daí em diante: byte-puro bidirecional (sem parsing extra)
  6. Em Close, proxy chama POST /dynamic-secrets/leases/:id/release

Limitações atuais

  • 1:1 com upstream — um proxy fala com um único host:port. Para múltiplos DBs, múltiplos proxies (cada um com seu listen port).
  • Auth no upstream limitada por protocolo — Postgres suporta CleartextPassword/MD5/SCRAM; MySQL apenas mysql_native_password; Redis apenas AUTH user pass; Mongo apenas SCRAM-SHA-256.

Observabilidade

Métricas Prometheus expostas em :9090/metrics (gated por env CV_METRICS_ENABLED=true):

  • cv_proxy_connections_active{protocol} — gauge
  • cv_proxy_lease_requests_total{result} — contador
  • cv_proxy_lease_pool_size{state} — gauge (warm/cold)
  • cv_proxy_bytes_total{direction,protocol} — contador
  • cv_proxy_errors_total{type} — contador

Health endpoint: GET /health (HTTP 200 quando proxy está pronto e lease pool aquecido).

Boas práticas

  • Use Secretless Proxy quando:

    • App é legada e não pode importar SDK
    • Você quer garantir que credencial nunca aparece em log/env
    • Compliance exige "app não conhece credencial real"
  • Use SDK + Dynamic Secrets quando:

    • App pode importar SDK e gerenciar lease
    • Você precisa de pool de connections sofisticado
    • Você precisa de TLS end-to-end
  • NetworkPolicy obrigatório — restrinja egress do proxy apenas pra cv.acme.com.br + DB upstream

  • Health check — incluir TCP probe em localhost:5432 no liveness/readiness do pod (proxy escuta antes de pedir lease, então proxy down = liveness fail)

  • Logs do proxy vão para stdout — ingerir no SIEM como qualquer outro container