
Segredo Simples e Seguro
O Keeper é um armazenamento criptográfico de segredos para Go. Ele criptografa payloads arbitrários de bytes em repouso usando derivação de chave Argon2id e criptografia autenticada XChaCha20-Poly1305 (padrão), e os armazena em um banco de dados bbolt embutido.
Ele é fornecido como três coisas que você pode usar de forma independente:
x/keephandler) — monte endpoints do keeper em qualquer mux net/http em uma chamada, com hooks, guardas e codificadores de resposta plugáveis para controle de acesso e registro de auditoria.cmd/keeper) — uma interface de terminal com sessão REPL persistente, entrada de segredos sem eco e zero exposição no histórico do shell.O Keeper foi projetado como a camada fundamental de gerenciamento de segredos para o balanceador de carga Agbero, mas não possui dependência do Agbero e funciona em qualquer projeto Go.
O Keeper particiona segredos em buckets. Cada bucket possui uma BucketSecurityPolicy imutável que governa como sua Chave de Criptografia de Dados (DEK) é protegida. Quatro níveis estão disponíveis.
Um esquema é um prefixo URI que agrupa buckets relacionados (vault://, certs://, space://, ou qualquer nome que você registre). Um nível de segurança é uma propriedade da política do bucket definida na criação e imutável a partir de então.
Você pode misturar níveis de segurança livremente dentro do mesmo esquema. Por exemplo, vault://system pode ser LevelPasswordOnly (desbloqueado automaticamente na inicialização), enquanto vault://admin é LevelAdminWrapped (requer credencial explícita).
A DEK do bucket é derivada da chave mestre usando HKDF-SHA256 com uma string de informação separada por domínio por bucket (keeper-bucket-dek-v1:scheme:namespace). Todos os buckets LevelPasswordOnly são desbloqueados automaticamente quando UnlockDatabase é chamada com a frase-senha mestre correta. Nenhuma credencial por bucket é necessária em tempo de execução. Este nível é apropriado para segredos que o processo precisa na inicialização sem interação humana.
O bucket possui uma DEK de 32 bytes gerada aleatoriamente e única para aquele bucket. A DEK nunca é armazenada em texto claro. Para cada administrador autorizado, uma Chave de Criptografia de Chave (KEK) é derivada de HKDF(masterKey‖adminCred, dekSalt) e usada para envolver a DEK via XChaCha20-Poly1305. O bucket fica inacessível até que um administrador chame UnlockBucket com sua credencial. A frase-senha mestre sozinha não pode descriptografar o bucket. Revogar um administrador não afeta a cópia envolvida de nenhum outro administrador.
A DEK do bucket é gerada no momento do CreateBucket e imediatamente envolvida por um HSMProvider fornecido pelo chamador. O provedor realiza as operações de wrap e unwrap — o keeper nunca manipula a DEK bruta após entregá-la ao provedor. UnlockDatabase chama automaticamente o provedor para desembrulhar e semear o Envelope para todos os buckets HSM registrados. A rotação da chave mestre não recriptografa esses buckets; a DEK é controlada pelo provedor.
Uma implementação interna SoftHSM apoiada por uma chave de wrapping protegida por memguard está disponível em pkg/hsm para ambientes de teste e CI. Não a use em produção.
Idêntico ao LevelHSM no comportamento de gerenciamento de chaves, mas o HSMProvider é implementado por pkg/remote.Provider — um adaptador HTTPS configurável que delega wrap e unwrap para qualquer serviço KMS remoto sobre TLS. Configurações pré-construídas para HashiCorp Vault Transit, AWS KMS e GCP Cloud KMS são fornecidas em pkg/remote. Para uso em produção, configure TLSClientCert e TLSClientKey para habilitar autenticação TLS mútua.
salt ← random 32 bytes, generated once, stored as a versioned SaltStore (unencrypted) masterKey ← Argon2id(passphrase, salt, t=3, m=64 MiB, p=4) → 32 bytes
Um hash de verificação é armazenado na primeira derivação:```
verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes
Chamadas subsequentes de DeriveMaster recalculam este hash e o comparam com crypto/subtle.ConstantTimeCompare. Uma incompatibilidade retorna ErrInvalidPassphrase.
O salt do KDF é armazenado não criptografado por design. Ele deve ser legível antes de UnlockDatabase para derivar a chave mestra — criptografá-lo com uma chave derivada da chave mestra seria circular. Um salt do KDF não é um segredo; seu propósito é exclusividade, não confidencialidade.
Cada valor de texto simples é criptografado com XChaCha20-Poly1305 usando a DEK do bucket:``` nonce ← random 24 bytes ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)
O registro armazenado é uma struct `Secret` codificada em msgpack contendo o texto cifrado, metadados criptografados e a versão do esquema. A autenticação é implícita: um texto cifrado decifrado com a chave errada produz uma falha de autenticação AEAD antes que qualquer texto simples seja retornado.
### Derivação de KEK — LevelAdminWrapped```
salt ← random 32 bytes, generated at bucket creation, stored in policy
ikm ← masterKey ‖ adminCredential
KEK ← HKDF-SHA256(ikm, salt, info="keeper-kek-v1") → 32 bytes
wrappedDEK ← XChaCha20-Poly1305.Seal(nonce, KEK, DEK)
A KEK é derivada usando HKDF em vez de uma segunda passagem Argon2. A chave mestra
já foi produzida por um KDF de alto custo; uma segunda invocação Argon2 adicionaria
centenas de milissegundos de latência a cada chamada UnlockBucket sem
benefício de segurança. HKDF-SHA256 opera em aproximadamente um microssegundo.
Defesa em profundidade: Um atacante que compromete apenas o banco de dados obtém a
DEK encapsulada e o salt HKDF, mas não consegue derivar a KEK sem a chave mestra.
Um atacante que compromete apenas a chave mestra não consegue desencapsular nenhuma
DEK LevelAdminWrapped sem também conhecer a credencial de administrador.
Os metadados secretos (hora de criação, hora de atualização, contagem de acesso, versão) são criptografados separadamente do texto cifrado:``` metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))
Para os buckets `LevelAdminWrapped`, `LevelHSM` e `LevelRemote`, isso significa que os metadados ficam inacessíveis sem a credencial do bucket, impedindo que um invasor com acesso de leitura ao arquivo do banco de dados descubra padrões de acesso ou timestamps.
**Nota sobre canal lateral de temporização:** O XChaCha20-Poly1305 processa o texto cifrado completo antes de retornar um erro de autenticação. O caminho de descriptografia de fallback (nova DEK derivada → antiga chave-mestre-como-DEK) leva o mesmo tempo de relógio de parede, independentemente de qual chave for bem-sucedida. Nenhum canal lateral de temporização vaza o estado de migração de um registro.