Checklist de Hardening
Lista verificável para registrar e deployar uma app nova com a postura de segurança correta desde o dia um.
Destilada do field report de deploys reais que sangraram horas pra contornar bugs e exposição evitável. Cada item tem o que fazer, por que importa e como verificar.
A ordem segue o ciclo de vida real: pré-setup → registro → primeiro deploy → ongoing. Pule itens com checkbox marcado e siga adiante — voltar pra trás é mais caro que avançar.
Fase 0 — Pré-setup (servidor)
Validar uma vez por servidor, antes da primeira app.
0.1 — Runner em versão atual
- Por que: versões anteriores a v2.23.x tinham bugs de stores divergentes, Traefik path sem validação,
.gitignoretraps. Cada uma custou ~30min de debug em campo. - Verificar:
runner -V # esperado: runner 2.23.7+ (ou superior) - Corrigir:
runner self-update.
0.2 — `mkey` existe e tem permissão `0600`
- Por que: a master key encripta todos os ckeys + GitHub tokens em todos os state files. Se vazada, todos os secrets de todas as apps são decifráveis.
- Verificar:
ls -la /opt/runner/.keys/master.key # esperado: -rw------- 1 root root - Corrigir:
chmod 600 /opt/runner/.keys/master.key && chown root:root /opt/runner/.keys/master.key. - Backup: salve
runner mkey export --sealed --jsonnum password manager. Sem mkey o servidor é inrecuperável.
0.3 — Diretório `.keys/` em `0700 root`
- Por que: evita listagem do dir mesmo se a permissão do file individual for relaxada por engano.
- Verificar:
ls -ld /opt/runner/.keys # esperado: drwx------ root - Corrigir:
chmod 700 /opt/runner/.keys && chown root:root /opt/runner/.keys.
0.4 — Traefik container reachable
- Por que: runner valida o path do dynamic config contra os mounts do container. Sem container
traefikrodando ou com nome diferente, a validação é skipada e deploys podem retornar success com 404 silencioso. - Verificar:
docker ps --filter "name=traefik" --format "{{.Names}}\t{{.Status}}" docker inspect traefik --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' - Corrigir: se o container tem outro nome, alinhe (
traefiké hardcoded como nome esperado). Se o mount real é diferente dopaths.traefik_dynamic, configure:runner settings set paths.traefik_dynamic <path-dentro-do-mount>
0.5 — Group `devs` existe e está nos perms
- Por que: state files (
0640 root:devs) só são legíveis por root + membros do groupdevs. Sem o group, operadores não-root precisam de sudo pra qualquer leitura. - Verificar:
getent group devs # esperado: devs:x:GID:user1,user2,... - Corrigir:
groupadd devs && usermod -aG devs <operador>.
0.6 — Logs com perm correta
- Por que: audit logs (
secrets-access.jsonl,insecure-events.jsonl) precisam ser graváveis por root e visíveis pelo group devs. - Verificar:
ls -la /opt/runner/logs/ # esperado: drwxr-x--- root:devs em /opt/runner/logs (ou similar)
Fase 1 — Pré-registro (repositório)
Antes de rodar runner add.
1.1 — `.gitignore` exclui plaintext env files
- Por que: previne
git add .de outro contribuidor vazar.env/.secretsplain pro repo público. - Verificar:
grep -E "^\.env|^\.env\.\*|^\.secrets|^\.secrets\.\*" .gitignore # esperado: as 4 linhas presentes (ou padrões equivalentes) - Corrigir:
cat >> .gitignore <<'EOF' # Plaintext env files (encrypted bundle is in .runner/secrets.enc) .env .env.* .secrets .secrets.* EOF - Bypass:
runner secrets push(v2.23.2+) auto-injeta esses 4 patterns se estiverem ausentes — mas é mais limpo já vir do template.
1.2 — `.gitignore` não bloqueia `.runner/secrets.enc`
- Por que:
.runner/literal no.gitignore(template comum) impede git de descer no diretório, e a negação!.runner/secrets.encnão funciona quando o parent está ignorado. O encrypted bundle nunca chega ao repo → recovery zero-touch quebrado. - Verificar:
grep -E "^\.runner/?$" .gitignore && echo "PROBLEMA" || echo "OK" # esperado: OK (sem matches) - Corrigir: trocar
.runner/por.runner/*+ adicionar negação:- .runner/ + .runner/* + !.runner/secrets.enc - Bypass: v2.23.2+ reescreve isso automaticamente em
runner secrets pushouadd, mas já vir do template evita ruído no commit history.
1.3 — `.deploy.yml` declara secrets explicitamente
- Por que: auto-detect por nome ainda pode falhar pra keys com padrões não-cobertos. Declarar em
secrets:block garante. - Verificar: abra
.deploy.yml. Qualquer key sensível (DATABASE_URL, *_TOKEN, *_KEY, *_PASSWORD, *_SECRET) deve estar emsecrets:block, nãoenvironment:. - Corrigir:
environment: APP_NAME: "{{::AppName?my-app}}" # público OK aqui BASE_URL: "{{::Domain?example.com}}" secrets: DATABASE_URL: "{{::DATABASE_URL}}" # sensível JWT_SECRET: "${GENERATE:hex:64}" # auto-gerado OPENAI_API_KEY: "" # placeholder, preencher via `runner secrets set`
1.4 — `build.context` aponta pro diretório certo
- Por que: se o Dockerfile está em
backend/Dockerfilee o contexto deve ser sóbackend/, declare. Antes da v2.23.3 o campo era ignorado; agora é respeitado. Mas configs comdockerfile: backend/Dockerfilesemcontext:mandavam o repo inteiro pro daemon — desperdício de banda e potencial vazamento de files irrelevantes pro build. - Verificar:
build: context: backend # raiz do build context (não repo root) dockerfile: Dockerfile # relativo ao context
1.5 — `version.source` resolvível antes do primeiro deploy
- Por que: se você setou
version.source: fileapontando prapackage.jsonouversion.json, o file precisa existir no commit que vai ser deployado. Caso contrário, primeiro deploy falha com erro mostrando os 4 paths tentados. - Verificar:
ls $(grep "^ file:" .deploy.yml | awk '{print $2}') - Alternativa:
version.source: git-commitevita o problema (usa o hash do commit como versão).
Fase 2 — Registro (runner add)
2.1 — `runner add` com `--ckey` explícito
- Por que: sem ckey, secrets ficam só em plaintext em disk (
.env/.secrets), sem backup encriptado pro repo e sem recovery zero-touch em outro server. - Verificar:
runner add --repo owner/app --branch main --ckey "<senha-forte-min-8-chars>" ... - Backup do ckey: salve em password manager. Sem ckey é impossível decifrar
.runner/secrets.enc. - Anti-órfão: se você rodou
runner ckeys add <project>antes dorunner add, o runner herda automaticamente desde v2.23.0. Se quer reciclar uma ckey, passerunner addsem--ckey. - App já registrada sem ckey (v2.43.3+): não é preciso remover e re-adicionar.
runner deploy <app> --ckeyanexa um ckey a um app existente que ainda não tem um —--ckeysozinho gera um novo,--ckey <valor>usa o informado. No-op se o app já tiver ckey.
2.2 — Token via `--token-from` quando possível
- Por que: evita re-colar PAT na linha de comando (vaza no shell history).
--token-from <other-app>reusa o token já encriptado de outra app do mesmo owner. - Verificar:
runner tokens list # mostra apps que já têm token registrado runner add --token-from outra-app-do-mesmo-owner ...
2.3 — `runner secrets push` confirmou bootstrap
- Por que: auto-bootstrap em
runner addé best-effort; se falhar (.gitignoreexótico, token sem push perm),.runner/secrets.encfica só local — recovery em outro server impossível. - Verificar:
gh api repos/owner/app/contents/.runner/secrets.enc --jq .size # esperado: número (bytes do bundle) - Corrigir:
runner secrets push <app>manual.
2.4 — State file está em `0640 root:devs`
- Por que: estado da app contém token e ckey encriptados + metadata sensível (repo URL, deploy history). World-readable vaza sem motivo.
- Verificar:
stat -c "%a %U:%G %n" /opt/runner/state/<app>.yml # esperado: 640 root:devs - Migração: v2.23.7+ migra
0644 → 0640automaticamente em qualquer load. Pra forçar em massa:runner status --json >/dev/null.
Fase 3 — Primeiro Deploy
3.1 — Pre-flight `secrets:` resolve
- Por que: pre-flight check refuse deploy se algum secret declarado em
.deploy.ymlestá vazio. Falha cedo é melhor que container subindo com env vazio. - Verificar:
runner manifest <app> --resolved | grep -E "<UNRESOLVED|^secrets:" -A 20 # esperado: zero <UNRESOLVED:KEY> - Corrigir: preencher via
runner secrets set <app> <KEY> <VALUE>ou via wizard interativo dorunner add.
3.2 — Traefik path validado pelo runner
- Por que: o runner inspeciona o container
traefikantes do deploy. Se inalcançável, refuse com hint. Em modoExternalou ausência de container, valida skipa silenciosamente. - Verificar:
runner deploy <app> -V 2>&1 | grep -i "Traefik path" # esperado: "Traefik path validated: traefik reads from ..."
3.3 — Health check passa
- Por que: se health falha, runner reverte automaticamente pra versão anterior. Mas se é o primeiro deploy, não há "anterior" — runner bail explicitamente com instrução pra inspecionar
docker logs <container>. - Verificar:
runner deploy <app> # esperado: terminar com "✓ DEPLOY CONCLUÍDO" + healthy status curl -sk https://<dominio>/health # esperado: HTTP 200
3.4 — Cert SSL válido (não default)
- Por que: Traefik serve cert default quando não encontra dynamic config — sintoma claro de Traefik path inalcançável.
- Verificar:
echo | openssl s_client -servername <dominio> -connect <dominio>:443 2>/dev/null | openssl x509 -noout -subject # esperado: subject=CN=<dominio> ou wildcard apropriado
Fase 4 — Ongoing (operação)
4.1 — Audit log de secrets ligado
- Por que:
secrets showlegítimo precisa de trail pra revisão pós-incidente. Sem isso, "quem viu o quê" é não-respondível. - Verificar:
ls -la /opt/runner/logs/secrets-access.jsonl # esperado: existe, 0640 root:devs tail -1 /opt/runner/logs/secrets-access.jsonl | jq . # esperado: JSON com timestamp, action, app, keys (nomes), user, uid - Análise sugerida:
jq -c 'select(.action == "show")' /opt/runner/logs/secrets-access.jsonl | tail -50
4.2 — Backups regulares de mkey + ckeys
- Por que: server pode pifar fisicamente. Sem mkey, todos os tokens + ckeys são inrecuperáveis. Sem ckey, secrets de cada app idem.
- Verificar: alguém em algum lugar guarda o output sealed atual de:
runner mkey export --sealed --json # único pro servidor runner ckeys export <app> --json # uma por app que importa - Periodicidade: após cada
runner mkey rotateou ao adicionar app importante.
4.3 — `runner self-update` quando tag estável sai
- Por que: cada release fecha bug encontrado em campo. Atrasar atualização é colecionar dívida técnica de segurança.
- Verificar:
curl -s https://runner.ccs.systems/version.txt | head -1 runner -V # se diferentes: runner self-update
4.4 — Cron `runner fetch --deploy` rodando
- Por que: sem cron, deploys novos esperam intervenção manual e podem ser esquecidos.
- Verificar:
crontab -l | grep "runner fetch" # esperado: */5 * * * * /usr/local/bin/runner fetch --deploy
Anti-padrões a evitar
| Anti-padrão | Por que evitar |
|---|---|
--ckey "12345" ou ckey curta |
Trivial brute force. Use --ckey "$(openssl rand -base64 24)". |
Commitar .env/.secrets plain "só pra testar" |
Vai pro history do git. Use runner secrets set. |
Editar /opt/runner/state/*.yml à mão |
Quebra ID hash do bundle, próximo load fica inconsistente. Use runner edit. |
Rodar runner --insecure em prod sem motivo |
Vira audit event vermelho. Não use como atalho. |
Ignorar warnings de runner deploy -V |
Phase 1.6 e Phase 8 emitem info crucial pra debug futuro. |
Adicionar usuário ao group docker sem MFA |
Acesso ao socket Docker = root no host. Trate como root. |
Checklist resumida (copie pro seu doc interno)
Servidor (uma vez):
[ ] runner v2.23.7+ instalado
[ ] /opt/runner/.keys/master.key em 0600 root:root
[ ] /opt/runner/.keys em 0700 root
[ ] traefik container rodando, mounts cross-checked com paths.traefik_dynamic
[ ] group devs existe e operadores são membros
[ ] mkey backup salvo em password manager
Repositório (por app):
[ ] .gitignore exclui .env / .env.* / .secrets / .secrets.*
[ ] .gitignore NÃO tem .runner/ literal (use .runner/* + !.runner/secrets.enc)
[ ] .deploy.yml declara secrets sensíveis em secrets: block (não environment:)
[ ] build.context aponta pra dir correto
[ ] version.source resolvível no primeiro deploy
Registro:
[ ] runner add --ckey "<senha-forte>" passado
[ ] ckey salvo em password manager
[ ] --token-from usado quando possível
[ ] runner secrets push confirmou .runner/secrets.enc no repo
[ ] state file em 0640 root:devs
Primeiro deploy:
[ ] runner manifest --resolved sem <UNRESOLVED:KEY>
[ ] Traefik path validated no log do deploy
[ ] /health responde 200
[ ] Cert SSL é o domínio correto (não default)
Ongoing:
[ ] /opt/runner/logs/secrets-access.jsonl recebe escritas
[ ] runner self-update rodado quando nova tag stable sai
[ ] cron */5 runner fetch --deploy ativoVeja também
- Secrets lifecycle — modelo de duas camadas mkey + ckey
- `runner secrets` — CRUD completo, audit log, push
- `runner ckeys` — gerenciamento da content key
- `runner tokens` — gerenciamento do GitHub token
- Primeiro deploy (5min) — passo-a-passo end-to-end
By Borlot.com.br on 05/07/2026