Secrets Encryption (Sealed Secrets) · MemphisLab Docs
MemphisLab Docs
k3s Platform

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:

  1. Gera valores aleatórios localmente (openssl rand) para as chaves marcadas em generatedSecretKeys (ex.: JWT_SECRET); deixa vazia qualquer chave em extraSecretKeys que não esteja em generatedSecretKeys — essas são credenciais de contrato com algo fora do cluster (ex.: LICENSE_SERIAL_KEY), preenchidas manualmente depois.
  2. Monta um Secret comum localmente com kubectl create secret --dry-run=client -o yaml — nunca chega a existir no cluster nessa forma.
  3. 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).
  4. 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.