Governança de IA · · 13 min de leitura
Quem roda código de PR não pode tocar produção
A máquina que roda os testes de um PR executa código que ninguém revisou; com IA, isso é a regra. Se ela alcança produção, quem abre PR alcança produção. Por isso separamos os dois: o runner não faz deploy, não guarda credencial de produção, não alcança a rede interna e prova isso todo dia. Produção não tem porta de entrada.
Por que a máquina de testes é a mais exposta da empresa?
Porque é a única que executa, por desenho, código que ninguém revisou. Num pull request, o que roda no CI não é o código da `main`: é o código de quem abriu o PR, incluindo o script de build, o arquivo de dependências, os testes e o próprio arquivo de workflow, quando ele muda. A revisão humana vem depois. O runner vem antes.
O risco não depende de um atacante sofisticado. A equipe de segurança do GitHub descreveu, em 2021, como um PR executa código no runner por um script de build, por uma mudança no arquivo de pacotes ou por um gancho de instalação de dependência, como `postinstall`. Nada disso exige vulnerabilidade. É o funcionamento normal do build.
Com IA no time, isso deixa de ser exceção. O assistente sugere uma biblioteca, e a biblioteca traz um script de instalação. Um agente abre PRs sozinho, à noite, e o CI executa cada um. Uma instrução escondida numa página que a IA leu vira um comando num arquivo de teste. Em todos os casos, o código chega ao runner antes de chegar a uma pessoa. Por isso, na terceira camada da série, a pergunta não é se o runner vai executar algo indevido. É o que esse algo alcança.
Daí a regra que dá nome a este texto: o runner é tratado como máquina não confiável, no mesmo nível de um notebook de visitante. Ele lê o repositório que testa e fala com a internet para baixar dependências. Não alcança produção, não guarda credencial de produção e nada em produção o consulta.
Por que não dá para rodar o CI no servidor de produção?
Porque direito de abrir PR viraria direito de rodar comando em produção. É a tentação mais comum numa empresa pequena: o servidor existe, tem folga, e instalar o runner ali resolve o deploy de uma vez, porque o runner conecta para fora e recebe o job sem abrir porta.
O problema está no que o deploy exige. Para subir contêineres, o runner precisa falar com o Docker do servidor, e quem fala com o Docker de um servidor consegue montar o disco inteiro num contêiner e agir como administrador. Na prática, "quem pode abrir um PR" passa a ser "quem pode executar qualquer coisa como administrador em produção", por meio de um arquivo de workflow que mora no próprio repositório. A retirada do `sudo` e o segundo fator ficam contornados por um caminho mais fácil.
O desenho que usamos separa as duas funções em duas máquinas e em duas direções. O CI constrói, testa e publica uma imagem, só com o pipeline verde. Produção não recebe nada empurrado: ela puxa a imagem publicada quando um PR em outro repositório troca a versão, como descrevemos em produção nunca constrói. O CI não tem a chave de produção, e produção não lê o código das aplicações. Ela recebe imagem, não código.
O erro comum é achar que um rótulo resolve: "este runner só roda o workflow de deploy". O rótulo escolhe em que máquina um job roda; não impede que um PR altere o workflow para pedir aquele rótulo. Se a máquina que alcança produção aceita job de um repositório em que todo o time abre PR, o acesso a produção é de todo o time.
Como se isola um runner?
Com cinco regras que valem ao mesmo tempo, porque cada uma fecha um caminho diferente pelo qual o job chegaria ao resto da máquina.
- Uma conta por runner. Cada runner roda com usuário próprio, sem senha, sem acesso administrativo e sem dividir pasta, grupo ou soquete com outra conta.
- Contêiner sem root. O Docker do runner roda no modo sem privilégio (rootless), suportado oficialmente desde a versão 20.10, de 2020, com um daemon por conta. Escapar do contêiner dá a conta do runner, não a máquina. O daemon do administrador fica desativado.
- Teto de memória, processador e disco. O runner e seus contêineres vivem sob um limite fixo, e o espaço de trabalho fica num disco com teto próprio. Um job em laço ou que enche o disco derruba o próprio job, não o servidor.
- Serviço escrito pelo administrador. A definição do serviço e as dependências do sistema são instaladas pelo administrador, nunca pelos scripts que vêm no pacote do runner. O administrador não executa arquivo que uma conta menos privilegiada consegue editar.
- Nenhum caminho para elevar privilégio. O serviço sobe proibido de ganhar privilégios novos: nenhum programa com permissão especial de elevação funciona a partir do job, nem o que o job plantar.
Do lado do GitHub, quatro escolhas completam o isolamento. O runner pertence a um grupo restrito a repositórios escolhidos, sem nenhum repositório público. Workflows de PR vindos de fork ficam desligados. O token de cada workflow começa só de leitura. E cada job pede o runner pelo rótulo do grupo, nunca pelo genérico "auto-hospedado", que aceitaria qualquer máquina. As duas do meio voltam no texto sobre o gate que bloqueia, que protege os oito checks do PR.
O que fazer amanhã: liste as máquinas que executam CI e, para cada uma, responda duas perguntas. Com que usuário o job roda? Esse usuário consegue falar com o Docker do administrador ou virar administrador? Se a resposta for "não sei", o isolamento ainda não existe.
Como provar que o runner não alcança a rede interna?
Com um teste que roda todo dia e falha se o runner alcançar qualquer servidor de produção. No nosso desenho, o isolamento não é uma afirmação na documentação. É um workflow agendado, com saída verde ou vermelha, como qualquer outro check.
O teste verifica quatro coisas a partir de um job comum. O job roda com a conta do runner. O Docker é o sem privilégio. A troca para administrador falha. E nem o runner nem um contêiner lançado por ele abre conexão com os servidores de produção pela rede interna. A última é a que importa, e dois detalhes separam um teste sério de um que passa por acaso.
O primeiro detalhe: só o tempo esgotado conta como bloqueio. Quando a conexão volta "recusada", o pacote chegou ao outro servidor e só não havia nada escutando ali. Isso prova que o servidor está fechado naquele ponto, não que a rede separa as duas máquinas. Amanhã alguém sobe um serviço novo, e o runner passa a alcançá-lo. Só vale como prova a tentativa que fica sem resposta até estourar o tempo, sinal de que a rede descartou o pacote no caminho.
O segundo detalhe: o teste exige um controle positivo. Ele precisa conseguir falar com o GitHub, o destino que o runner tem de alcançar para trabalhar. Sem isso, um runner sem rede nenhuma passaria no teste, e o verde não significaria nada.
O teste olha também para o contêiner, porque, conforme a rede dele é montada, o tráfego não passa pelas mesmas regras que o do processo. E roda todo dia porque rede muda sem avisar: uma regra de firewall refeita, um servidor novo, uma rota de manutenção que ficou. O vão fechado na segunda-feira não está provado na quinta.
É a pergunta que fazemos a todo controle da série: o que barra, como se prova e o que acontece quando falha. Aqui, a rede barra, o teste diário prova, e a falha chega como check vermelho, com dono, antes do incidente.
Como chegar a produção sem nenhuma porta aberta?
Invertendo a direção de todas as conexões. Produção não escuta nada na rede: nenhum serviço publicado, nem o acesso remoto de administração. Tudo o que precisa entrar chega por um túnel de saída, que o próprio servidor abre até um serviço de borda, e é a borda que recebe os usuários.
Isso muda o que um atacante encontra: não há porta para varrer nem senha sendo testada no servidor. Na frente de cada aplicação e do acesso administrativo fica uma camada de identidade, que exige login no provedor de identidade da empresa, com segundo fator, antes de qualquer pacote chegar ao servidor. A força bruta morre na borda, com registro, e não no disco da máquina.
A camada de identidade decide por quem é e de onde vem, as duas coisas juntas. Só o endereço de origem, como "qualquer máquina na VPN", aceita também o notebook comprometido e o convidado. Só a identidade aceita a pessoa certa num lugar inesperado. Com as duas, o registro guarda quem entrou e de onde. É o princípio da arquitetura de confiança zero descrita pelo NIST: estar dentro da rede não concede confiança nenhuma.
O mesmo caminho serve à máquina de CI. A telemetria e os alertas do runner saem como os de qualquer cliente externo, com credencial própria e de escopo estreito, sem exceção na rede interna para mandar log.
O erro comum é abrir "só uma porta" para uma conveniência: o painel de monitoramento, o banco para um relatório, o acesso de um fornecedor. Cada porta é um caminho a mais para o teste cobrir e alguém lembrar de fechar. Quando a conveniência é inevitável, ela entra como desvio registrado, com dono e data para rever.
Por que trocar sudo por su com segundo fator?
Porque escalar para administrador precisa ser raro, explícito e atribuído a uma pessoa. No nosso desenho, a máquina não tem `sudo`. Quem administra entra com a própria conta, por chave, e para virar administrador usa `su`, restrito a um grupo e protegido por um código do aplicativo autenticador. As contas de serviço, como as dos runners, nem chegam a ver o pedido de código.
Três razões sustentam a troca. A primeira é atribuição: a trilha de auditoria do sistema guarda a conta real que escalou, então cada comando como administrador aponta para uma pessoa. A segunda é explicitação: escalar vira um ato separado, que gera alerta, em vez de um prefixo que se digita sem pensar e que uma ferramenta, ou uma IA, repete num script. A terceira é superfície: a lista do `sudo` acumula exceções, e cada uma, como "este usuário pode reiniciar aquele serviço", é mais um caminho até o administrador.
O custo é admitido por escrito. Entre a conta pessoal e o administrador fica só um código de seis dígitos, e não um código mais uma senha. O que segura isso é o conjunto: só um grupo pequeno pode tentar, as tentativas são limitadas por janela de tempo e cada uma fica na trilha. A senha do administrador sai do caminho remoto e fica só no console da máquina, como acesso de emergência que não depende de celular.
O que fazer amanhã: procure, nos servidores, quem está no grupo do `sudo` e no grupo do Docker. Os dois são administrador com outro nome. Se uma conta de serviço aparece num deles, é por ali que um job ou uma sessão de IA chegaria ao topo.
E quando é o assistente de IA que roda no servidor?
Vale tudo o que vale para o runner, com mais rigor, porque a sessão de IA também executa código não revisado: o que ela mesma acabou de escrever. Um agente que roda tarefas num servidor da empresa, abre PRs ou investiga um erro numa máquina remota é, para o desenho, mais um runner. Ele recebe conta própria, teto de recursos, a rede da máquina de CI e nenhuma rota para produção.
Três regras se somam às do runner.
- Sessão descartável. Cada sessão começa com a pasta pessoal e a de trabalho vazias, e nada sobrevive para a seguinte, nem gancho de configuração, nem processo em segundo plano. A próxima sessão pode ser de outra pessoa.
- Nenhuma credencial do Git no servidor. O acesso aos repositórios passa por um intermediário que age em nome de quem abriu a sessão, e não por um token no disco que qualquer sessão leria.
- Atualização só pelo administrador. O programa do assistente pertence ao administrador, e nenhuma sessão consegue trocá-lo. Se pudesse, uma sessão contaminaria todas as seguintes, inclusive o processo que guarda a chave do serviço.
A configuração gerenciada vale aqui também: plugins e regras chegam montados pelo administrador, e a sessão não os desliga. E o segredo do serviço chega como arquivo que só ele lê na partida, nunca como variável que aparece numa inspeção, como em segredo não passa pelo terminal.
O que esse desenho não resolve?
Ele reduz o dano, não elimina o risco, e é melhor dizer isso antes do incidente. Um job comprometido ainda lê o repositório que testa e os segredos que aquele workflow recebe. Por isso segredo de CI é de CI, com valor que não serve fora dele, como a senha de um banco de teste que nasce e morre com o job.
Runner persistente pode ficar comprometido de forma persistente: um job deixa algo para trás e o seguinte herda. A resposta clássica é o runner efêmero, de um job só, que o GitHub oferece desde 2021. O custo pouco falado: para recriar runners sozinha, a automação precisa de uma credencial que registra runners na organização. Se um job alcançar essa credencial, registra um runner falso e passa a receber jobs, e segredos, de outros repositórios. Nossa regra: efêmero é o alvo; persistente com grupo restrito é aceitável enquanto o grupo tiver só repositórios de um time e nenhum segredo de produção, com a decisão escrita e o gatilho para rever.
E máquinas de confiança diferente não dividem runner. Um teste que precisa falar com um sistema sensível, como a homologação de um parceiro, ganha runner próprio, num grupo com só aquele repositório. Pôr a máquina de CI comum na lista de acesso desse sistema daria o sistema a qualquer workflow do grupo.
Como saber, hoje, se o seu CI está separado de produção?
Com seis perguntas, para responder numa tarde com quem administra os servidores. Cada "não sei" pesa tanto quanto uma resposta ruim.
- Alguma máquina que executa CI também roda produção, ou guarda chave, token ou acesso ao Docker de produção?
- De dentro de um job, uma conexão a um servidor de produção pela rede interna fica sem resposta até estourar o tempo?
- Existe um teste agendado que prova isso, com controle positivo, e alguém recebe o vermelho?
- Com que conta o job roda, e essa conta está no grupo do `sudo` ou do Docker?
- Os runners próprios aceitam job só de repositórios escolhidos, sem fork e sem repositório público?
- Produção tem alguma porta aberta na rede, e quem entra para administrar passa por identidade com segundo fator?
Com boas respostas, o código escrito com IA pode quebrar um teste à vontade: o estrago fica no runner. Falta a última parte da série, que atravessa as três camadas: provar que tudo isso funciona e saber desligar quando for preciso.
Perguntas frequentes
Posso usar o runner hospedado pelo GitHub em vez de ter o meu?
Pode, e para muita empresa pequena é o melhor começo: cada job roda numa máquina nova, fora da sua rede, que some no fim. Runner próprio faz sentido quando o build precisa de algo que só existe dentro de casa ou quando custo e desempenho pesam. Aí valem as regras deste texto: conta própria, contêiner sem root, teto de recursos, nenhuma rota para produção e um teste diário que prova o isolamento.
Por que o CI não pode fazer o deploy, se ele já tem a imagem pronta?
Porque quem faz deploy precisa de credencial de produção, e o CI executa código de qualquer PR antes da revisão. Juntar as duas coisas dá produção a quem abre PR. No nosso desenho, o CI publica a imagem com o pipeline verde; a troca de versão em produção é outro PR, num repositório próprio, e é produção que puxa a imagem.
Repositório privado também precisa desse isolamento?
Precisa. Repositório privado reduz quem abre PR, não o que o PR carrega. Uma dependência comprometida, a conta de um colaborador tomada ou um assistente de IA que sugere um pacote com script de instalação executam código no runner do mesmo jeito. O que muda no privado é desligar os workflows de PR vindos de fork, que de outra forma rodariam na sua máquina.
Como sei se o meu runner alcança produção hoje?
De dentro de um job, tente abrir conexão com um servidor de produção pela rede interna, com tempo limite curto. Se conectar, ou se voltar "conexão recusada", a rede não está bloqueando: só o tempo esgotado prova filtro. Faça a mesma tentativa a um destino que precisa funcionar, como o GitHub, para distinguir "bloqueado" de "sem rede". Depois transforme isso num teste agendado.
Runner efêmero resolve o problema?
Resolve a persistência: um job não deixa nada para o seguinte. Mas, para recriar runners sozinha, a automação precisa de uma credencial que registra runners na organização, e essa credencial não pode ficar ao alcance de um job. Efêmero é o alvo. Persistente com grupo restrito é aceitável enquanto o grupo tiver só repositórios de um time e nenhum segredo de produção, com a decisão escrita.
O que muda quando é um assistente de IA que roda tarefas no servidor?
Nada no princípio e mais rigor na prática. A sessão de IA também executa código não revisado, o que ela mesma acabou de escrever. Ela recebe conta própria, teto de recursos e a rede da máquina de CI, começa sempre do zero, não encontra credencial do Git no disco e não consegue atualizar o próprio programa.
Fontes
- Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests · GitHub Security Lab ·
- GitHub Actions: Ephemeral self-hosted runners & new webhooks for auto-scaling · GitHub Changelog ·
- Introducing Docker Engine 20.10 · Docker Blog ·
- Zero Trust Architecture (NIST SP 800-207) · NIST ·
Leia também
- Produção nunca constrói: o PR como único caminho do código com IA
- Gate que só alerta ensina o time a ignorar o vermelho
- Segredo não passa pelo terminal
- Governança de IA no código: o caminho estreito até produção
Por Victor Freitas · Consultor em governança de tecnologia e IA · Franklin Freitas Consultoria
Conduz diagnósticos e implantações de governança de tecnologia e IA em pequenas e médias empresas, com experiência acumulada em provedores de internet.