O Problema: Por Que Gerenciar Secrets em Kubernetes?
Quando você trabalha com Kubernetes em produção, rapidamente percebe que armazenar senhas, chaves de API e certificados diretamente no código ou em ConfigMaps é um risco de segurança inaceitável. O Kubernetes oferece o recurso nativo de Secrets, mas este é apenas o ponto de partida: os dados são armazenados em base64 (não é criptografia real) no etcd, e qualquer pessoa com acesso ao cluster pode decodificar facilmente.
Empresas sérias precisam de uma solução que: rotacione secrets automaticamente, mantenha um histórico de auditoria, integre-se com diversos provedores (AWS Secrets Manager, Azure Key Vault, Google Secret Manager), e injete credenciais de forma segura nas aplicações sem que elas precisem conhecer detalhes de conexão. É aqui que entram HashiCorp Vault Agent e External Secrets Operator — duas abordagens diferentes para o mesmo problema crítico.
Entendendo as Duas Abordagens
HashiCorp Vault Agent: Injeção via Sidecar
O Vault Agent funciona como um sidecar (container adicional no Pod) que se conecta ao Vault, autentica-se e injeta secrets diretamente no filesystem do seu container principal. A aplicação lê o arquivo gerado, não precisa conhecer Vault e os secrets nunca passam pelas variáveis de ambiente (que são visíveis em logs de diagnóstico).
O fluxo é simples: Vault Agent acorda periodicamente, valida sua autenticação com o Vault, busca os secrets atualizados e reescreve os arquivos. A aplicação pode ser notificada via signal para recarregar as credenciais. Isso é especialmente útil se você já usa Vault internamente ou precisa de rotação muito frequente.
External Secrets Operator: Sincronização de Secrets Nativos
O External Secrets Operator (ESO) é um controller Kubernetes que cria Secrets nativos do Kubernetes sincronizados com um backend externo. Você define um recurso YAML chamado SecretStore (ou ClusterSecretStore) que aponta para seu provedor (Vault, AWS Secrets Manager, etc.), e então cria um ExternalSecret que especifica quais secrets buscar. O ESO faz o trabalho pesado e mantém o Secret do Kubernetes sempre sincronizado.
A vantagem aqui é que sua aplicação continua usando Secrets normais do Kubernetes — sem mudança de código. É mais "nativo" do ecossistema, mas você perde o controle fino que o Vault Agent oferece em rotações muito rápidas. External Secrets é ideal quando você quer abstrair o backend sem mudar sua aplicação.
Vault Agent: Implementação Passo a Passo
Instalando e Configurando o Vault
Primeiro, você precisa de um Vault rodando. Para desenvolvimento, pode ser local:
# Instalar Vault (macOS com Homebrew)
brew install vault
# Iniciar servidor em modo desenvolvimento
vault server -dev
# Em outro terminal, exportar a variável de ambiente
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='s.xxxxxxxxxxxxxxxx' # Token exibido acima
Agora crie um secret no Vault:
# Habilitar o backend KV v2
vault secrets enable -path=secret kv-v2
# Armazenar um secret
vault kv put secret/myapp/database \
username=dbuser \
password=supersecret123 \
host=postgres.default.svc.cluster.local \
port=5432
Configurar Autenticação Kubernetes no Vault
Para que Vault Agent autentique Pods automaticamente, configure a autenticação Kubernetes:
# Habilitar o método de autenticação kubernetes
vault auth enable kubernetes
# Configurar o kubernetes auth
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token
# Criar uma policy para o app
vault policy write myapp-policy - <<EOF
path "secret/data/myapp/*" {
capabilities = ["read", "list"]
}
EOF
# Criar um role Kubernetes
vault write auth/kubernetes/role/myapp \
bound_service_account_names=myapp \
bound_service_account_namespaces=default \
policies=myapp-policy \
ttl=24h
Declarar o Pod com Vault Agent
Agora configure seu Pod para usar Vault Agent. O Vault Agent é injetado via mutating webhook ou manualmente:
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: default
---
apiVersion: v1
kind: Pod
metadata:
name: myapp
namespace: default
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp"
vault.hashicorp.com/agent-inject-secret-database: "secret/data/myapp/database"
vault.hashicorp.com/agent-inject-template-database: |
{{- with secret "secret/data/myapp/database" -}}
export DB_USER="{{ .Data.data.username }}"
export DB_PASSWORD="{{ .Data.data.password }}"
export DB_HOST="{{ .Data.data.host }}"
export DB_PORT="{{ .Data.data.port }}"
{{- end }}
spec:
serviceAccountName: myapp
containers:
- name: myapp
image: myapp:latest
command: ["/bin/sh", "-c"]
args:
- |
source /vault/secrets/database
exec python3 app.py
ports:
- containerPort: 8080
A anotação vault.hashicorp.com/agent-inject-secret-database diz ao Vault Agent qual secret buscar. O campo agent-inject-template-database define como formatar o output (pode ser JSON, variáveis de ambiente, arquivo de configuração, etc.).
Aplicação Python Consumindo o Secret
Sua aplicação não precisa saber que veio do Vault — apenas lê variáveis de ambiente:
import os
import psycopg2
from flask import Flask
app = Flask(__name__)
# Variáveis injetadas pelo Vault Agent
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')
DB_HOST = os.getenv('DB_HOST')
DB_PORT = int(os.getenv('DB_PORT', 5432))
def get_db_connection():
conn = psycopg2.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASSWORD,
port=DB_PORT,
database='mydb'
)
return conn
@app.route('/health')
def health():
try:
conn = get_db_connection()
conn.close()
return {'status': 'healthy'}, 200
except Exception as e:
return {'status': 'unhealthy', 'error': str(e)}, 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
External Secrets Operator: Implementação Passo a Passo
Instalação do ESO
External Secrets Operator é instalado como um Helm chart:
# Adicionar repositório Helm
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
# Instalar o operador
helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets-system \
--create-namespace \
--set installCRDs=true
Configurar SecretStore (Backend Vault)
Crie um SecretStore que aponta para seu Vault:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: default
spec:
provider:
vault:
server: "http://vault.vault.svc.cluster.local:8200"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "myapp"
serviceAccountRef:
name: myapp
Se estiver usando AWS Secrets Manager em vez de Vault:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-backend
namespace: default
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: myapp
Criar um ExternalSecret
Agora defina qual secret deve ser sincronizado:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-database
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: myapp-database-secret
creationPolicy: Owner
template:
engineVersion: v2
data:
database.conf: |
DB_USER={{ .username }}
DB_PASSWORD={{ .password }}
DB_HOST={{ .host }}
DB_PORT={{ .port }}
data:
- secretKey: username
remoteRef:
key: myapp/database
property: username
- secretKey: password
remoteRef:
key: myapp/database
property: password
- secretKey: host
remoteRef:
key: myapp/database
property: host
- secretKey: port
remoteRef:
key: myapp/database
property: port
O ESO lê essas configurações, conecta ao Vault usando a ServiceAccount myapp, e cria um Secret nativo do Kubernetes chamado myapp-database-secret. Cada 1 hora (refreshInterval), ele sincroniza novamente.
Consumir o Secret no Pod
Como é um Secret nativo, sua aplicação usa normalmente:
apiVersion: v1
kind: Pod
metadata:
name: myapp
namespace: default
spec:
serviceAccountName: myapp
containers:
- name: myapp
image: myapp:latest
command: ["/bin/sh", "-c"]
args:
- |
cat /etc/secrets/database.conf
exec python3 app.py
volumeMounts:
- name: db-secret
mountPath: /etc/secrets
readOnly: true
volumes:
- name: db-secret
secret:
secretName: myapp-database-secret
A aplicação lê do arquivo montado em /etc/secrets/database.conf, exatamente como faria com qualquer Secret do Kubernetes.
Comparação e Escolha da Abordagem
| Aspecto | Vault Agent | External Secrets |
|---|---|---|
| Curva de aprendizado | Mais alta (Vault é complexo) | Mais baixa (integrado com Kubernetes) |
| Rotação de secrets | Muito rápida (minutos) | Mais lenta (depende de refreshInterval) |
| Mudança na aplicação | Nenhuma (lê arquivos ou env vars) | Nenhuma (usa Secrets nativos) |
| Compatibilidade | Apenas com Vault | Vault, AWS, Azure, Google, etc. |
| Auditoria | Excellent (logs do Vault) | Boa (logs do ESO + backend) |
| Consumo de recursos | Baixo (sidecar leve) | Médio (controller + watchers) |
Minha recomendação prática: Se você já investe em Vault e precisa de rotação frequente, use Vault Agent. Se quer flexibilidade multi-provider e seus secrets não mudam constantemente, use External Secrets. Muitos ambientes usam ambos — ESO para a maioria dos casos e Vault Agent apenas para aplicações críticas com rotação agressiva.
Conclusão
O gerenciamento de secrets em Kubernetes evoluiu para ir além de Secrets nativos. Vault Agent oferece controle fino, rotação rápida e uma experiência centrada em Vault, ideal para organizações que já adotaram Vault como solução de segurança principal. External Secrets Operator traz flexibilidade, abstração do backend e integração nativa com o Kubernetes, perfeito para ambientes multi-cloud ou que precisam mudar de provedor no futuro.
A escolha certa depende do seu contexto: complexidade aceitável, frequência de rotação, número de backends diferentes e se sua equipe já domina Vault. O importante é não deixar secrets em base64 no etcd — ambas as soluções resolvem isso de forma segura e profissional.