Secrets Encryption (Sealed Secrets)
Como um segredo pode ser commitado em Git público sem vazar, e como o kubeseal faz isso
O problema
Um Secret normal do Kubernetes não é cifrado — é só codificado em base64, que qualquer pessoa com o YAML decodifica em uma linha. Isso é incompatível com GitOps (ver gitops-argocd): se o estado desejado do cluster vive inteiro no Git, e um Secret faz parte desse estado, o valor real do segredo precisaria estar no Git também — em texto claro, para qualquer um com acesso ao repositório ler.
Criptografia assimétrica: a peça que resolve isso
Criptografia assimétrica usa duas chaves matematicamente ligadas, mas com papéis diferentes: uma chave pública, que cifra e pode ser distribuída livremente, e uma chave privada, que decifra e nunca sai de onde está. Cifrar com a pública e decifrar com a privada é o caminho — o inverso (cifrar com a privada) é o que assinatura digital usa, não é o caso aqui.
O controller do Sealed Secrets, rodando dentro do cluster, gera esse par na primeira instalação e guarda a chave privada só nele mesmo — nunca em disco fora do cluster, nunca no Git. A chave pública, por outro lado, pode ir para qualquer lugar, porque só serve para cifrar: ter a chave pública não dá a ninguém a capacidade de decifrar nada.
O fluxo: kubeseal
kubeseal é o cliente que pega um Secret normal e devolve um SealedSecret — a versão cifrada com a chave pública do controller. O SealedSecret é commitável em Git, público ou privado, porque decifrar ele exige a chave privada que só existe dentro do cluster de destino. Um SealedSecret roubado do Git de um cluster não abre em outro cluster — cada controller tem seu próprio par de chaves.
# sealed/license-manager.yaml (real, já cifrado — os valores abaixo não são o segredo)
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: license-manager-secrets
namespace: license-manager
spec:
encryptedData:
APP_KEY: AgDL22R6MztsLt0AIVM6hHgLSzcygmNanVBNhdNxv8XSrW3Fr...
AWS_ACCESS_KEY_ID: AgAEedRTHl5OdrcD6cFybbQqU6vka+DTmSR7geE6a...
template:
metadata:
name: license-manager-secrets
namespace: license-manager
Dentro do cluster, o controller do Sealed Secrets observa SealedSecrets (é um control loop, o mesmo padrão de orchestration-fundamentals) e, para cada um, decifra com a chave privada e materializa um Secret nativo comum no mesmo namespace — que é o que a aplicação de fato lê via variável de ambiente. metadata.name/metadata.namespace fazem parte do texto cifrado (não são metadados soltos): um SealedSecret copiado para outro namespace ou renomeado falha ao decifrar, porque o valor cifrado está amarrado a esse par nome+namespace especificamente. Isso impede um ataque óbvio — copiar o SealedSecret de um cliente para o namespace de outro e reaproveitar o segredo.
Provisionamento: scripts/provision-client.sh
O fluxo real de criação de segredo para um cliente novo:
- Gera valores aleatórios localmente (
openssl rand) para as chaves marcadas emgeneratedSecretKeys(ex.:JWT_SECRET); deixa vazia qualquer chave emextraSecretKeysque não esteja emgeneratedSecretKeys— essas são credenciais de contrato com algo fora do cluster (ex.:LICENSE_SERIAL_KEY), preenchidas manualmente depois. - Monta um
Secretcomum localmente comkubectl create secret --dry-run=client -o yaml— nunca chega a existir no cluster nessa forma. - Envia esse YAML via pipe para
kubeseal, que cifra contra a chave pública do controller (buscada com--controller-namespace kube-system --controller-name sealed-secrets-controller). - Grava o resultado em
sealed/<slug>.yaml— esse arquivo, sim, é commitado.
O backup que realmente importa
make secrets-backup-key não faz backup de nenhum segredo de cliente — faz backup da chave privada do controller. Sem essa chave, nenhum SealedSecret já commitado pode ser decifrado de novo, nem pelo próprio time: reinstalar o controller do zero gera um par de chaves novo, incompatível com tudo que já foi cifrado. É por isso que esse backup é o único passo do bootstrap da plataforma marcado como “guardar fora do Git” — guardar dentro do Git anularia o motivo de existir.