`runner ckeys`

Comandos para gerenciar content keys (ckeys) — passphrases usadas para encriptar .env + .secrets num blob .runner/secrets.enc por app, transportável entre servidores.

A ckey de cada app é guardada encriptada com a master key (mkey) em:

  • /opt/runner/state/{app}.yml — campo ckey: (apps com ckey legacy desde v2.12)
  • /opt/runner/.keys/ckeys/{id}.json — ckeys standalone (compartilháveis entre apps)

Subcomandos

Comando Função
runner ckeys list Lista todas as ckeys (por-app + shared)
runner ckeys add Cria uma ckey standalone para um projeto
runner ckeys export <id-or-app> Exporta o blob sealed (re-importável em servidor com a mesma mkey)
runner ckeys import-sealed Importa um blob sealed
runner ckeys reveal <id-or-app> Revela a ckey plaintext (gated por --insecure, v2.22.7+)
runner ckeys delete <id> Remove uma ckey standalone do vault (v2.23.0+)
runner ckeys rotate-secret <app> Rotaciona a ckey de um app e re-encripta o bundle (v2.53.0+)
runner ckeys hash <app> Hash da ckey que o runner usa, geração e esquema; com --bundle, se o bundle decifra (v2.167.0+)

`runner ckeys rotate-secret` (v2.53.0+)

Troca a ckey de um app por uma nova e re-encripta o secrets.<instance>.enc atomicamente. A ckey nova vem via STDIN (nunca por argv, nunca ecoada nem logada). Dois modos, exclusivos:

Flag Fonte da ckey nova
--to-plain Ckey em texto puro via stdin — informada pelo CCS ou pelo usuário. Não passa pelo cofre.
--to-sealed Blob selado do cofre via stdin (verify + unwrap). Requer o contrato do cofre (fase futura).
# Manual (CCS ou usuário): pipe a ckey nova pra stdin
printf '%s' "$NOVA_CKEY" | runner ckeys rotate-secret meu-app --to-plain --yes -o json

Atômico com failover/rollback: decifra o bundle com a ckey atual, re-encripta com a nova, valida, e só então troca state.ckey + ckey_generation. Se o update do state falhar, o bundle é revertido pra ckey antiga — nunca fica meio-trocado.

Qual bundle (v2.164.0+): a rotação re-cifra e publica o mesmo arquivo do push normal — o bundle: do manifesto, senão o do target, senão secrets.<instância>.enc, com o prefixo do serviço em compose.

Prova (JSON): op_id, status (aplicada | ja_aplicada), new_ckey_hash (hash domain-separated, não a ckey), bundle_sha, bundle_path (caminho no repo), branch, push_commit, generation e pushed. Contrato all-green: o comando sai com exit≠0 se o push era esperado (--no-push ausente) e falhou — o consumidor (CCS) só credita sucesso com pushed: true + bundle_sha conferido no repo.

Flag Efeito
--yes, -y Pula a confirmação (obrigatório em modo não-interativo, ex. CCS)
--no-push Não faz push do bundle re-encriptado (só local)
--op-id <ID> Id da operação, devolvido na prova (v2.167.0+)
--generation <N> Geração da ckey nova (v2.167.0+, ver abaixo)
-i, --instance <INST> Instância do bundle (padrão: a do registro)
-o json Saída da prova em JSON

Geração (--generation N, v2.167.0+):

Caso Resultado
N maior que a geração atual aplica e grava N
N igual, com a mesma ckey status: "ja_aplicada": prova nova do bundle atual, sem re-cifrar
N igual com outra ckey, ou N menor recusa com geracao_obsoleta, sem mudar nada
sem a flag a geração sobe 1

A rotação roda sob o lock da app: env set e deploy concorrentes esperam. Se o lock não vier em 10 s, falha com app_lock_ocupado. App com esquema v2 recusa com esquema_nao_suportado. Com -o json, um erro sai no stdout como {"error": "<código>", "message": "…"}, com exit≠0.


`runner ckeys hash` (v2.167.0+)

Mostra o hash da ckey que o runner usa para a app (nunca a ckey), a geração e o esquema (v1, v2 ou none). Sem ckey, ckey_hash é null.

runner ckeys hash meu-app -o json
{"app":"meu-app","ckey_hash":"…","generation":3,"scheme":"v1"}

Com --bundle [-i <instância>], diz também se o bundle que a rotação usaria decifra com a ckey do state:

{"app":"meu-app","ckey_hash":"…","generation":3,"scheme":"v1","bundle_decifra":true,"bundle_sha":"…","bundle_path":".runner/secrets.production.enc"}

Bundle ausente: bundle_decifra: false, bundle_sha e bundle_path null.


`runner ckeys list`

Lista todas as ckeys do servidor.

runner ckeys list
runner ckeys list --json

Output (human):

Ckeys (2):
------------------------------------------------------------------------------------------
  ck_0177…6afd | minha-api       | aes-256-gcm | app-state | secrets:8 | ck_0177…6afd
  ck_a3b2…7e91 | loja-front      | aes-256-gcm | shared    | secrets:5 | ck_a3b2…7e91
      shared_with: loja-backend, loja-admin

Nota: o id ck_xxxx…xxxx contém o caractere unicode … (U+2026). Para usar em outros comandos, copie-cole — não digite manualmente. Você também pode passar apenas o nome do app desde v2.22.7.


`runner ckeys add`

Cria uma ckey standalone para um projeto. Útil para compartilhar a mesma ckey entre múltiplos apps.

runner ckeys add meu-projeto
runner ckeys add meu-projeto --algorithm aes-256-gcm --json

A ckey criada fica em /opt/runner/.keys/ckeys/{id}.json, sealed com a mkey do servidor.


`runner ckeys export`

Exporta uma ckey como blob sealed (JSON encriptado com a mkey atual). Importável em outro servidor que tenha a mesma mkey.

# Por id
runner ckeys export ck_0177…6afd

# Por nome do app (v2.22.7+)
runner ckeys export minha-api

# JSON com metadata
runner ckeys export minha-api --json

Output (raw): o JSON sealed (uma linha base64 ou bloco JSON).

Use case: migrar um app para outro servidor sem precisar regerar a ckey. Apenas se a mkey estiver sincronizada entre os servidores (via runner mkey export no original + runner mkey import no destino).


`runner ckeys import-sealed`

Importa um blob sealed (de ckeys export) num servidor que tenha a mesma mkey.

runner ckeys import-sealed --blob "$(cat ckey.sealed.json)"
runner ckeys import-sealed --blob /path/to/ckey.sealed.json --project meu-app

A flag --project permite renomear durante o import.


`runner ckeys reveal` (v2.22.7+)

Revela a ckey plaintext de um app ou ckey id. Gated por --insecure para evitar leak acidental — o plaintext vai pra stdout.

# Pelo nome do app (recomendado)
runner ckeys reveal minha-api --insecure

# Por id
runner ckeys reveal ck_0177…6afd --insecure

# JSON
runner ckeys reveal minha-api --insecure --json

Output (sem --json):

WARNING: plaintext ckey printed below. Store in password manager and clear terminal scrollback.
minha-chave-secreta-do-projeto

Output (--json):

{
  "ckey_id": "minha-api",
  "plaintext": "minha-chave-secreta-do-projeto",
  "warning": "Store this value in a password manager. Anyone with this ckey can decrypt the app's secrets."
}

Quando usar

  • Recovery: você precisa re-registrar um app num servidor novo (runner add --ckey "...") mas só tem o servidor velho com a ckey encriptada
  • Backup manual: copiar a ckey pra password manager antes de descomissionar um servidor
  • Debug: verificar qual ckey está em uso

O que precisa estar disponível

  • A mkey do servidor (/opt/runner/.keys/master.key) — para decifrar a ckey sealed
  • A ckey sealed no state file (/opt/runner/state/{app}.yml::ckey) ou numa standalone (/opt/runner/.keys/ckeys/{id}.json)

Se a mkey foi perdida

Sem a mkey original, a ckey sealed é irrecuperável. Sua única opção é:

  1. Re-registrar a app gerando uma ckey nova (runner add --ckey)
  2. Recriar .env + .secrets do zero
  3. Re-encriptar e commitar novo .runner/secrets.<instancia>.enc

Por isso o runner mkey export deve sempre fazer parte do procedimento de backup do servidor.


Recuperação completa de uma app

Cenário: servidor antigo srv-old tem minha-api com ckey desconhecida. Você quer levar pro srv-new.

# 1. No srv-old: pega a ckey plaintext
ssh srv-old
runner ckeys reveal minha-api --insecure
# Copia o valor pro password manager

# 2. No srv-new: registra a app com a mesma ckey
ssh srv-new
runner add --repo seu/repo --branch main \
  --token ghp_xxx \
  --ckey "<valor-copiado-do-passo-1>"

# 3. Runner detecta .runner/secrets.<instancia>.enc (ou o flat legado) no repo e restaura .env/.secrets automaticamente

Alternativa (se a mkey for compartilhada entre os servidores):

# 1. No srv-old: export sealed
ssh srv-old "runner ckeys export minha-api" > ckey.sealed.json

# 2. No srv-new: import sealed
scp ckey.sealed.json srv-new:/tmp/
ssh srv-new "runner ckeys import-sealed --blob /tmp/ckey.sealed.json"

`runner ckeys delete` (v2.23.0+)

Remove uma ckey standalone do vault (/opt/runner/.keys/ckeys/). Útil quando uma ckey órfã foi criada por engano (ex: runner ckeys add <project> antes de runner add, gerando uma ckey que ficou desconectada de qualquer app).

runner ckeys delete ck_a3b2…7e91
runner ckeys delete ck_a3b27e91 --yes
runner ckeys delete ck_a3b2…7e91 --json
Flag Default Descrição
--force, -y, --yes false Skipa o prompt interativo de confirmação
--json false Output em JSON

Aceita tanto o storage id (ck_<8hex>) quanto o formatted id (ck_xxxx…xxxx). Não toca em per-app state ckeys — para essas, use runner unregister <app> (que remove a app inteira).

{
  "action": "ckeys_delete",
  "ckey_id": "ck_a3b2…7e91",
  "deleted": true,
  "deleted_at": "2026-05-16T..."
}

A partir de v2.23.0, runner add consulta o vault antes de criar uma ckey nova — se já existe uma shared ckey para o projeto, herda silenciosamente. Isso elimina a fonte mais comum de órfãs.


Veja também

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