`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.enc (AES-256-GCM) — backup criptografado para recovery zero-touch em outro servidor

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

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 Copia keys entre projetos no mesmo servidor 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 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 admin@example.com --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