Skip to main content

Segredo

Quando usar: quando o serviço precisa de uma credencial ou de uma chave que não pode ser lida por quem só consulta a configuração: chave de assinatura, token de terceiro, credencial de banco.

O valor nunca está no repositório, nunca é montado pelo módulo e nunca é impresso. O serviço declara o slot; a plataforma provisiona o conteúdo e o entrega montado como arquivo.

Declarar o slot​

nns/service.yaml:

secrets:
- slot: db-runtime
description: "Conta de runtime: username, password, database, ca.crt. Só a DML concedida por tabela pela migração."
- slot: db-migrator
description: "Conta de migração: username, password, database, ca.crt, runtime_account. Só no Job de migração."
- slot: notifier-signature
source: generator:random
description: "Chave HMAC com que o notificador confere a assinatura dos avisos enviados."
  • db-runtime e db-migrator são do banco: existem só quando database está declarado, e o conteúdo deles é a credencial (a de migração é a única com DDL).
  • Qualquer outro slot declara a source:
    • generator:random — a plataforma gera um valor aleatório (chave de assinatura, por exemplo);
    • generator:ed25519-seed — a plataforma gera uma seed Ed25519;
    • external — valor de terceiro, entregue uma vez pelo operador;
    • cloud-tenant-credential — credencial de nuvem por tenant (só para módulo de nuvem).
  • No máximo 16 slots, slot em ^[a-z][a-z0-9-]{0,30}$.

Ler​

O conteúdo chega como um diretório por slot, um arquivo por chave, em NN_SECRETS_DIR (/var/run/nnumbers/secrets no pod). A leitura é por NnsConfig.readSecret(slot, chave):

/** O slot do Secret com a chave da assinatura (`service.yaml`, `secrets[].slot`). */
static final String SIGNATURE_SLOT = "notifier-signature";

/** A chave dentro do slot (um arquivo por chave, dentro do diretório do slot). */
static final String SIGNATURE_KEY = "value";

/** A assinatura do corpo com a chave do slot: `HMAC-SHA256`, em base64url sem preenchimento. */
String sign(byte[] payload) {
byte[] key = config.readSecret(SIGNATURE_SLOT, SIGNATURE_KEY);
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal(payload));
} catch (java.security.GeneralSecurityException e) {
throw new IllegalStateException("sample: assinatura indisponível", e);
} finally {
java.util.Arrays.fill(key, (byte) 0); // o valor não fica na memória além do uso
}
}

O que o SDK já garante na leitura:

  • o caminho é montado como <NN_SECRETS_DIR>/<slot>/<chave>, com slot e chave validados — não há como escapar do diretório;
  • arquivo maior que 64 KiB, permissão mais aberta que 0640 (ou 0644 de bico de pena), arquivo ausente e arquivo ilegível recusam, cada um com a sua mensagem — e a mensagem cita slot e chave, nunca o conteúdo;
  • o valor é byte[]: quem usa decide como interpretar (texto, PEM, chave HMAC).

Como testar​

O teste escreve o diretório do slot num diretório temporário e aponta NN_SECRETS_DIR para lá:

Path slot = dir.resolve("secrets").resolve(NotifierClient.SIGNATURE_SLOT);
Files.createDirectories(slot);
Path value = slot.resolve(NotifierClient.SIGNATURE_KEY);
Files.writeString(value, SECRET);
Files.setPosixFilePermissions(value, PosixFilePermissions.fromString("rw-------"));

config =
NnsConfig.fromEnviron(
Map.of(
"NN_ENV", "development",
"NN_SERVICE_ID", "sample",
"NN_MODULE_ID", "sample",
"NN_VERSION", "0.1.0",
"NN_SECRETS_DIR", dir.resolve("secrets").toString()));

E o efeito é conferido no que sai — a assinatura é recalculada de forma independente:

@Test
void theBodyIsSignedWithTheSecretOfTheSlot() throws Exception {
client().announce(Ctx.background().withTimeout(Duration.ofSeconds(5)), "item.scan.finished",
Map.of("item_id", "0be1d3c6-6f3e-4a6b-a4f5-2a2f2a9f0d10", "score", 75));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = "v1=" + Base64.getUrlEncoder().withoutPadding()
.encodeToString(mac.doFinal(seenBody.get().getBytes(StandardCharsets.UTF_8)));
assertThat(seenSignature.get()).isEqualTo(expected);
}

A chave do teste é escrita pelo próprio teste num diretório temporário: nenhuma chave privada literal entra no repositório, e nada é impresso no log do build.

Erros comuns​

  • Colocar o valor no código ou na configuração — config é lida por quem opera; segredo não. E o valor no repositório é vazamento permanente, mesmo depois de removido.
  • Ler System.getenv("MEU_TOKEN") — variável de ambiente só chega ao pod se estiver declarada; o slot é o caminho do contrato, com validação de permissão e de tamanho.
  • Passar o segredo para log, mensagem de erro ou details de erro — o SDK redige o que tem forma de segredo, mas o certo é não passar.
  • Chamar o slot pelo nome do Secret do Kubernetes — o serviço conhece o slot (notifier-signature), não o nome do Secret (nns-status-mailersend); quem traduz é a plataforma.
  • Assumir que a chave existe em todo ambiente — readSecret recusa quando falta, e é isso que você quer: melhor não subir do que subir sem assinar.
  • Guardar o valor num campo estático para "não reler" — a rotação do Secret vale para a próxima leitura; reler é barato e é o comportamento esperado.
  • Comparar segredo com equals sem cuidado de tempo — para verificação de valor secreto, use comparação de tempo constante (MessageDigest.isEqual).