`runner secrets`

Conjunto de subcommands para gerenciar secrets criptografados de uma app.

Secrets são armazenados em duas camadas:

  • .env / .secrets plaintext em /data/apps/<app>/ — lidos pelo bind mount do container
  • .runner/secrets.<instancia>.enc (AES-256-GCM) — backup criptografado para recovery zero-touch em outro servidor. Um arquivo por instancia desde a v2.129.0: staging (branch dev) e producao (branch main) nao reescrevem mais o mesmo binario, e o merge de release deixa de conflitar. O flat secrets.enc legado continua sendo lido enquanto for o unico; no primeiro push ele e promovido para o nome da instancia

A partir de v2.23.3, todos os comandos de escrita atualizam as 3 fontes atomicamente.

Prefira a ckey assimétrica (`--ckey v2`)

O Runner cifra secrets em dois esquemas. Ao registrar uma app, prefira o assimétrico:

runner add --repo owner/app --token ghp_xxx --ckey v2
simétrica (v1) assimétrica (v2)
Chave uma só, cifra e decifra par: privada no servidor, pública no repo
Para escrever um secret precisa da chave que também lê tudo basta a pública
Para ler um secret quem tem a ckey lê todos precisa da privada, que não sai do servidor
Ver que chaves existem não — blob binário opaco sim: nomes legíveis, valores selados
Bundle monolítico texto, valor a valor

O ganho central: escrever um secret deixa de dar o poder de ler os outros. Uma automação, um operador ou o front adicionam valores tendo só o recipient público. E como os nomes ficam visíveis e os valores selados individualmente, dá para revisar num diff de PR o que entrou ou saiu sem que ninguém leia conteúdo.

# runner-secrets:v1
# instance:production
# recipient:age1jdfj2nx0lh2kc7us0syqrndn4w40pwquhr6tysrup4g8t988c3cqwuz27r
DATABASE_URL=enc1:hQEMAxk...
SMTP_PASSWORD=enc1:wV4DR2b...

Valores aceitos pelo `--ckey`

Forma Resultado
--ckey v2 Assimétrica — par X25519 gerado
--ckey v1 Simétrica, valor gerado
--ckey (sem valor) Esquema default — v2 (assimétrica) desde a v2.124.0
--ckey "minha-chave" Simétrica com a ckey literal informada

O default de --ckey sem valor ainda é o simétrico. Isso muda numa versão futura — por isso peça v2 explicitamente em vez de contar com o default.

Quando ficar na simétrica

Um caso legítimo, e é por ele que a simétrica continua existindo: quando você precisa fixar uma chave para uso externo — um processo fora do Runner abre o bundle, ou várias apps compartilham a mesma ckey (ver `ckeys copy`). No modo assimétrico a privada é do servidor por construção e não deve sair dele, então não há como fixar uma chave para terceiros.

Migrar uma app existente

runner secrets migrate <app> --instance production

A migração é explícita e nunca acontece sozinha: faz backup, valida antes de publicar, é idempotente e preserva a ckey até a confirmação. Desde a v2.129.0 o .secrets plano e aposentado em .secrets.pre-v2-<ts> (nao apagado): o v2 nao o le, e ele ficava no disco com valores velhos confundindo env show. Detalhes conceituais em ckey simétrica e assimétrica.

Subcomandos

Comando Função Desde
runner secrets show <app> Exibe valores decifrados v2.10.0
runner secrets set <app> <key> <value> Salva valor plaintext em .secrets v2.23.0
runner secrets generate <app> <key> --generator <spec> Gera valor via pattern e salva v2.23.0
runner secrets share <app> <key> --reference <ref> Referência cross-project (sys orchestrator) v2.23.0
runner secrets delete <app> <key> Remove uma key v2.23.4
runner secrets paste-encoded Import cross-server de valor encriptado v2.10.0
runner secrets push <app> Push de .deploy.yml + .runner/secrets.enc pro repo v2.23.1
runner secrets copy --from <a> --to <b> --keys K1,K2 [--from-instance <i>] [--to-instance <i>] Copia keys entre apps no mesmo servidor, em qualquer esquema. Com alvo v2, cifra cada valor com a chave pública do alvo; nenhum valor sai na saída (v2.152.0+) v2.10.0
runner secrets export Exporta keys como blob sealed v2.10.0
runner secrets import-sealed Importa blob sealed v2.10.0
runner secrets migrate <app> Migra a instância de ckey simétrica para identity assimétrica #65
runner secrets pull <app> Decifra o bundle e popula .env/.secrets no working dir v2.30.10
runner secrets diff <a> <b> Diff de secrets entre apps, antes de unificar ckeys v2.30.6
runner secrets reset-bundle <app> Reconstrói .runner/secrets.enc a partir do plaintext local v2.30.3
runner secrets cleanup-bundle <app> Remove placeholders ${VAR} puros do bundle commitado v2.28.0

`runner secrets show `

Decifra .runner/secrets.enc e exibe os valores.

runner secrets show meu-app
runner secrets show meu-app --json

Output indica explicitamente o source (a partir de v2.23.0):

--- env (from .runner/secrets.enc) ---
APP_NAME=meu-app
PORT=3000

--- secrets (from .runner/secrets.enc) ---
DATABASE_URL=postgres://user:pass@db:5432/app
JWT_SECRET=AbC123...

`runner secrets set ` (v2.23.0+)

Salva um valor plaintext direto no .secrets cifrado. Substitui o antigo paste-encoded --mode manual.

runner secrets set meu-app DB_PASSWORD "s3cr3t"
runner secrets set meu-app SMTP_USER [email protected] --json

A partir de v2.23.3, escreve atomicamente em .env/.secrets/.runner/secrets.enc. Próximo deploy enxerga o valor sem precisar refazer encoding.


`runner secrets generate --generator ` (v2.23.0+)

Gera um valor aleatório via spec e salva. Substitui paste-encoded --mode generate.

runner secrets generate meu-app JWT_SECRET --generator '${GENERATE:hex:64}'
runner secrets generate meu-app API_TOKEN --generator '{RANDOM}'

Specs suportadas:

Spec Resultado
{RANDOM} 32 chars alfanuméricos
${GENERATE:hex:N} N chars hexadecimais
${GENERATE:base64:N} N bytes em base64

`runner secrets share --reference ` (v2.23.0+)

Declara uma referência cross-project (ex: @ccs/SMTP_PASS). A resolução ocorre via sys orchestrator — o CLI sozinho não consegue resolver referências cross-project.

runner secrets share meu-app SMTP_PASS --reference @ccs/SMTP_PASS

Substitui paste-encoded --mode shared.


`runner secrets delete ` (v2.23.4+)

Remove uma key do app. Procura em .env e .secrets (nessa ordem), remove de onde encontrar, reescreve os 3 stores atomicamente.

runner secrets delete meu-app TEST_FOO          # prompt interativo
runner secrets delete meu-app TEST_FOO --yes    # skip confirmation
runner secrets delete meu-app TEST_FOO --json   # programático
Flag Default Descrição
--force, -y, --yes false Skipa o prompt interativo
--json false Output em JSON

Key ausente é no-op silencioso (removed: false).

{
  "action": "secrets_delete",
  "app": "meu-app",
  "key": "TEST_FOO",
  "removed": true,
  "removed_from": ".secrets",
  "deleted_at": "2026-05-16T..."
}

Comportamento ao deletar a última key (v2.23.5+)

Quando a operação resulta em ambos os maps (.env + .secrets) vazios, runner remove o .runner/secrets.enc em vez de manter um bundle encriptado vazio. .env e .secrets plaintext ficam como files de 0 bytes (docker bind mount lê como "sem env"). Próximo secrets set repopula normalmente.

Antes da v2.23.5, runner secrets delete da última key falhava com No .env or .secrets content to encrypt — bug corrigido.


`runner secrets paste-encoded` (legacy + cross-server)

Import de um valor já encriptado com a ckey de outro projeto/servidor.

runner secrets paste-encoded --project meu-app --key DB_PASSWORD \
  --encoded-value 'enc:v1:...' \
  --source-ckey ck_a1b2…c3d4

A partir de v2.23.0, os modos --mode manual|generate|shared continuam funcionando mas emitem deprecation warning — prefira os subcommands explícitos (set/generate/share).


`runner secrets push ` (v2.23.1+)

Push de .deploy.yml + .runner/secrets.enc pro repo do app. Usa o token GitHub já encriptado no state da app — zero credentials por flag/env.

runner secrets push meu-app
runner secrets push meu-app --json

Use case

Você acabou de registrar a app via runner add --ckey, o runner gerou .deploy.yml + .runner/secrets.enc localmente, mas o repo no GitHub ainda não tem esses files. Pra recovery zero-touch funcionar em outro server, eles precisam estar no repo. Antes de v2.23.1 isso só ocorria após o primeiro deploy bem-sucedido (Phase 8 do pipeline).

Garantias de segurança (encrypted-only por design)

  1. Whitelist de staging: só git add .deploy.yml .runner/secrets.enc (paths explícitos). Nunca git add . / git add -A. Plaintext .env/.secrets não consegue ser staged.
  2. Auto-fix do .gitignore (v2.23.2+): se faltarem .env/.env.*/.secrets/.secrets.* no .gitignore, runner injeta no mesmo commit do bootstrap.
  3. Reescrita de .runner/ (v2.23.2+): se o .gitignore tem .runner/ (template comum), reescreve como .runner/* + adiciona !.runner/secrets.enc. Sem essa reescrita, a negação não funciona em git e o secrets.enc ficaria invisível.
  4. Encryption-required: se .deploy.yml declara secrets: mas não há .runner/secrets.enc, encripta on-the-fly. Sem ckey ou .env/.secrets pra encriptar → erro claro, sem push.

Output JSON

{
  "action": "secrets_push",
  "app": "meu-app",
  "repo": "owner/repo",
  "branch": "main",
  "pushed": true,
  "commit": "abc123456789",
  "no_changes": false,
  "pushed_at": "2026-05-16T..."
}

pushed: false + no_changes: true significa que o repo já tem o estado atual (idempotente).

Auto-bootstrap no `runner add`

A partir de v2.23.1, quando --ckey e token estão disponíveis E --deploy não foi passado, runner add tenta o push automaticamente ao final. Failure não falha o add — operador pode retry via runner secrets push <app>.


Erros comuns

Erro Causa Fix
push_no_token App não tem token no state Re-registrar com --token-from <outra-app>
push_no_ckey App não tem ckey Re-registrar com --ckey "<valor>"
push_no_encrypted_bundle .deploy.yml declara secrets: mas não há .env/.secrets nem secrets.enc runner secrets set <app> <K> <V> para cada key
push_clone_failed Token sem permissão de push no repo Regenerar PAT com escopo repo
Container sobe com secret vazio Pre-v2.23.3: divergência entre stores Atualizar pra v2.23.3+

Audit trail (v2.23.6+)

Cada show/set/generate/delete/paste-encoded registra uma linha JSON em /opt/runner/logs/secrets-access.jsonl (perm 0640 root:devs):

{
  "timestamp": "2026-05-16T17:58:58Z",
  "action": "show",
  "app": "meu-app",
  "keys": ["DB_PASSWORD", "JWT_SECRET"],
  "user": "root",
  "uid": 0,
  "hostname": "ns3177045",
  "pid": 1067444
}
Campo Significado
action show / set / generate / delete / paste-encoded:<mode>
app Nome do app afetado
keys Nomes das keys envolvidas (NUNCA valores)
user / uid Output de whoami + getuid() do processo
hostname / pid Útil pra correlacionar com syslog/auditd

Escrita é best-effort — falha de I/O não aborta a operação. Trail é append-only.

Queries comuns

# Todos os acessos das últimas 24h
jq -c 'select(.timestamp > (now - 86400 | strftime("%Y-%m-%dT%H:%M:%SZ")))' \
  /opt/runner/logs/secrets-access.jsonl

# Quem rodou `show` em qual app
jq -c 'select(.action == "show") | {timestamp, app, user, keys: .keys | length}' \
  /opt/runner/logs/secrets-access.jsonl

# Detectar repetição suspeita (mesmo user batendo show muitas vezes)
jq -r 'select(.action == "show") | "\(.user) \(.app)"' \
  /opt/runner/logs/secrets-access.jsonl | sort | uniq -c | sort -rn | head

Veja também

By Borlot.com.br on 16/05/2026