`runner service`

Instala e gerencia o serviço systemd do runner (scheduler, webhook e API remota). Todos os subcomandos de mutação exigem root e systemd.

Subcomando O que faz
init Gera service.yml, a unit e habilita (nasce com a API desligada)
start / stop / restart Controla o serviço
status Estado do serviço (-o json: installed, running, scheduler, api)
config Altera porta, bind, chave, autenticação e restrição por IP
expose / unexpose Publica a API num domínio do Traefik ou remove a rota
uninstall Remove unit, service.yml, a chave e o segredo do header

Com -o json, um erro vira UM objeto {"error":"<codigo>","message":"..."} no stdout, com exit diferente de zero. O texto humano de expose, unexpose e uninstall vai para o stderr. Veja a tabela de códigos.

Autenticação da API

Por padrão a API autentica com o header X-API-Key e uma chave rk_... (entregue em /opt/runner/service-api-key, modo 600). Para usar um header próprio:

# o segredo vem por stdin: nunca na linha de comando
runner service config --auth-header X-Meu-Header --secret-stdin < /caminho/segredo
Flag Efeito
--auth-header <NOME> Liga o header próprio. O modo é exclusivo: o X-API-Key passa a dar 401. Authorization é aceito, e o valor enviado é o segredo puro, sem Bearer. Sem --secret-stdin, troca só o nome (exige segredo já gravado)
--secret-stdin Lê o segredo do stdin: 32 a 4096 bytes de ASCII visível, o \n final sai. Sozinho, rotaciona o segredo e mantém o header. Com o stdin num terminal, é recusado
--auth-padrao Volta para X-API-Key + rk_, gera uma rk_ NOVA e apaga o segredo do header

O segredo fica em /opt/runner/service-auth.secret (modo 600, dono root, arquivo comum). O service.yml guarda só o nome do header e a impressão digital (12 hex). A API não sobe se o arquivo sumir, tiver outra permissão ou divergir da impressão. A resposta 401 é a mesma nos dois modos e não revela o header esperado.

Ao entrar no modo header, a rk_ é zerada: api.api_key fica vazio no service.yml e /opt/runner/service-api-key é apagado. O status passa a trazer key_configured: false.

Restrição por IP

runner service config --ip-permitidos 192.0.2.10/32,198.51.100.0/24
runner service config --limpar-ip-permitidos
runner service config --ip-permitidos 192.0.2.10 --atras-de-proxy cloudflare
Flag Efeito
--ip-permitidos <CIDR,...> Substitui a lista (v4/v6). IP sem / vale /32 ou /128. Mínimo /8 (v4) e /16 (v6), até 64 entradas. Vale para a API e os hooks; o /health fica livre
--limpar-ip-permitidos Remove a restrição
--atras-de-proxy cloudflare|nenhum Informa o proxy à frente do Traefik. Exige a lista

service expose aceita --ip-permitidos e --atras-de-proxy. Com a API exposta, config --ip-permitidos reescreve a rota runner-api.yml: o vpn-only@file sai da rota e a faixa da VPN entra na lista (a combinação seria E, não OU). Ao limpar, ele volta. API exposta por versão anterior à 2.175.0 pede reexpor_necessario: rode expose de novo.

Fora da lista, a resposta é 403 com ip_nao_permitido. A verificação roda antes da autenticação. O mTLS não pode ser combinado com a lista (ip_permitidos_com_mtls).

O IP do client só vem do X-Forwarded-For quando a conexão chega de um proxy confiável (trusted_proxies). Um par de fora é sempre julgado pelo próprio IP.

Downgrade: com --ip-permitidos ativo, rode runner service config --limpar-ip-permitidos ANTES de voltar para versão < 2.175.0; senão a API fica sem filtro de IP e sem VPN. No modo header, a versão antiga recusa subir (api_key_ausente); rode --auth-padrao antes se for usar a API nela.

Status em JSON

runner -o json service status traz o bloco api:

{
  "enabled": true, "bind": "127.0.0.1", "port": 9090, "mtls": false,
  "exposed": true, "domain": "runner.exemplo.com", "key_configured": false,
  "auth": {"modo": "header", "header": "X-Meu-Header", "fingerprint": "3f9a1c2b7d4e"},
  "expose": {"domain": "runner.exemplo.com", "vpn_only": true, "hooks_publicos": false, "middlewares": []},
  "ip_permitidos": {
    "cidrs": ["192.0.2.10/32"], "incluir_vpn": true, "faixa_vpn": ["10.0.1.0/24"],
    "proxy": "cloudflare", "cloudflare_faixas_data": "2026-10-29"
  }
}
  • auth.modo é api_key ou header. No modo api_key, header é X-API-Key. O fingerprint é null se não há chave.
  • ip_permitidos só aparece quando configurado. Traz aviso_modo_host: true com o Traefik em modo host e sem trusted_proxies, e aviso quando as faixas embutidas da Cloudflare têm mais de 180 dias.
  • expose só aparece com a API exposta. Se ela foi exposta por versão anterior à 2.175.0, sem o registro gravado, traz só domain e "vpn_only": null.
  • Sem service.yml: {"installed": false, ...} com exit 0.

Códigos de erro

Código Quando
servico_nao_instalado Sem service.yml ou unit
servico_ja_instalado init com serviço existente
systemctl_falhou systemctl saiu com erro
systemd_ausente Host sem systemd
permissao_negada Não é root
porta_ocupada / bind_invalido / bind_falhou Porta em uso, bind inexistente, falha ao escutar
api_desligada, mtls_atras_do_traefik Pré-condições do expose
api_key_ausente, api_key_formato Chave da API
dominio_em_uso Domínio já tem router
traefik_nao_encontrado Sem Traefik ou Docker inacessível
expose_sem_resposta A rota não respondeu em 60 s
mtls_incompleto, tls_cert_ausente, tls_chave_ausente TLS
auth_header_invalido Nome de header fora da regra (token RFC 9110, até 64, sem hop-by-hop, sem X-Forwarded-*)
auth_header_ausente --secret-stdin sem header configurado
auth_modo_header --api-key com o modo header ligado
segredo_ausente Header sem segredo gravado ou informado
segredo_stdin_vazio, segredo_stdin_terminal Stdin vazio ou num terminal
segredo_fraco, segredo_grande, segredo_invalido Fora de 32 a 4096 bytes ou com caractere não visível (inclui espaço)
segredo_permissao Arquivo do segredo não é comum, 600 e do root
segredo_divergente Arquivo não bate com a impressão do service.yml
ip_permitidos_invalido CIDR inválido, largo demais ou lista acima de 64. CIDR duplicado não é erro: a duplicata é removida
ip_permitidos_com_mtls Lista combinada com mTLS
ip_permitidos_nao_aplicado Restart ou rota falhou; o estado anterior foi restaurado
vpn_faixa_desconhecida Não foi possível ler a faixa da VPN do Traefik
vpn_only_ausente expose --vpn-only num host sem o middleware vpn-only no file provider do Traefik (falha na hora, nada é gravado)
atras_de_proxy_invalido Valor desconhecido ou sem lista
reexpor_necessario API exposta por versão antiga: rode expose de novo
runner_erro Erro sem código próprio

Na API HTTP, a recusa por IP responde 403 com ip_nao_permitido ou ip_nao_determinado.

Veja também

By Borlot.com.br on 29/10/2026