eBPF na GitHub: Prevenindo Dependências Circulares em Deployments
Introdução à Segurança de Deployment com eBPF
Na AITY, assim como em grandes plataformas como o GitHub, a resiliência dos sistemas é primordial. O GitHub hospeda todo o seu código-fonte em github.com, uma prática que garante que a equipe seja seu maior cliente, testando mudanças internamente antes que cheguem aos usuários. Contudo, essa abordagem revela uma vulnerabilidade crítica: se github.com estiver indisponível, o acesso ao próprio código-fonte para corrigir a falha seria impossível. Esta é uma dependência circular simples, onde a implantação do GitHub necessita do próprio GitHub.
Para mitigar isso, o GitHub mantém um espelho do código para correções e assets compilados para rollbacks. No entanto, a complexidade aumenta com outras dependências circulares que podem ser introduzidas por scripts de deployment, como dependências em serviços internos ou downloads de binários do próprio GitHub.
Ao projetar um novo sistema de deployment baseado em host, novas abordagens para prevenir a criação de dependências circulares foram avaliadas. A solução encontrada foi o uso do eBPF (extended Berkeley Packet Filter) para monitorar e bloquear seletivamente essas chamadas problemáticas. Este artigo técnico explora como o GitHub utiliza eBPF para aprimorar a segurança de deployments, detectando e prevenindo dependências circulares.
Tipos de Dependências Circulares
Para ilustrar a importância da detecção, vamos considerar um cenário hipotético de falha do MySQL que impede o GitHub de servir dados de release. Para resolver o incidente, uma mudança de configuração precisa ser aplicada aos nós MySQL, executando um script de deployment em cada nó.
Nesse cenário, diferentes tipos de dependências circulares podem impactar o GitHub:
- Dependência Direta: O script de deployment do MySQL tenta baixar a versão mais recente de uma ferramenta de código aberto do GitHub. Como o GitHub não consegue servir os dados de release (devido à interrupção), o script falha.
- Dependências Ocultas: O script de deployment do MySQL utiliza uma ferramenta de serviço já presente no disco da máquina. No entanto, ao ser executada, a ferramenta verifica o GitHub para ver se uma atualização está disponível. Se não conseguir contatar o GitHub, o script pode falhar ou travar.
- Dependências Transitórias: O script de deployment do MySQL invoca, via API, outro serviço interno (por exemplo, um serviço de migrações), que por sua vez tenta buscar a última release de uma ferramenta de código aberto do GitHub. A falha se propaga de volta ao script de deployment.
Desafios na Resolução de Dependências
Até recentemente, a responsabilidade de revisar scripts de deployment e identificar dependências circulares recaía sobre cada equipe proprietária de hosts stateful. Na prática, muitas dependências não eram identificadas até que um incidente ocorresse, atrasando a recuperação.
Uma rota óbvia seria bloquear o acesso a github.com a partir das máquinas para validar se o sistema poderia ser implantado sem ele. No entanto, esses hosts são stateful e servem tráfego de clientes mesmo durante implantações contínuas, drains ou reinícios. Bloquear github.com inteiramente impactaria sua capacidade de lidar com requisições de produção.
A Solução eBPF: Monitoramento de Rede Seletivo
Foi nesse ponto que o eBPF entrou em cena. O eBPF permite carregar programas customizados no kernel Linux e se conectar a primitivas de sistema como a rede. Em particular, o tipo de programa BPF_PROG_TYPE_CGROUP_SKB possibilita conectar-se à saída de rede de um cGroup específico.
Um cGroup é uma primitiva Linux que impõe limites de recursos e isolamento para conjuntos de processos. É possível criar um cGroup, configurá-lo e mover processos para ele. Isso abriu a possibilidade de criar um cGroup, colocar apenas o script de deployment dentro dele e, então, limitar o acesso de rede de saída apenas daquele script.
Construindo Filtragem de Rede Condicional com eBPF
O GitHub iniciou um proof of concept em Go usando a biblioteca cilium/ebpf. A ebpf-go é uma biblioteca pura em Go para ler, modificar e carregar programas eBPF e anexá-los a vários hooks no kernel Linux, simplificando o processo de autoria, construção e execução de programas eBPF.
Para conectar-se ao tipo de programa BPF_PROG_TYPE_CGROUP_SKB, o código Go simplificado seria:
//go:generate go tool bpf2go -tags linux bpf cgroup_skb.c -- -I../headers
package main
import (
"log"
"time"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
)
func main() {
// Load pre-compiled programs and maps into the kernel.
objs := bpfObjects{}
if err := loadBpfObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// Link the count_egress_packets program to the cgroup.
l, err := link.AttachCgroup(link.CgroupOptions{
Path: "/sys/fs/cgroup/system.slice",
Attach: ebpf.AttachCGroupInetEgress,
Program: objs.CountEgressPackets,
})
if err != nil {
log.Fatal(err)
}
defer l.Close()
log.Println("Counting packets...")
// Read loop reporting the total amount of times the kernel
// function was entered, once per second.
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
var value uint64
if err := objs.PktCount.Lookup(uint32(0), &value); err != nil {
log.Fatalf("reading map: %v", err)
}
log.Printf("number of packets: %d\n", value)
}
}
Com o programa eBPF correspondente em C:
//go:build ignore
#include "common.h"
char __license[] SEC("license") = "Dual MIT/GPL";
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, u32);
__type(value, u64);
__uint(max_entries, 1);
} pkt_count SEC(".maps");
SEC("cgroup_skb/egress")
int count_egress_packets(struct __sk_buff *skb) {
u32 key = 0;
u64 init_val = 1;
u64 *count = bpf_map_lookup_elem(&pkt_count, &key);
if (!count) {
bpf_map_update_elem(&pkt_count, &key, &init_val, BPF_ANY);
return 1;
}
__sync_fetch_and_add(count, 1);
return 1;
}
A linha //go:generate compila o código eBPF C e gera automaticamente a struct bpfObjects, permitindo interagir com o programa.
Filtragem Baseada em DNS com eBPF
O tipo de programa CGROUP_SKB opera em endereços IP, mas manter uma lista de IPs de bloqueio atualizada para os sistemas do GitHub seria desafiador. A solução foi usar outro tipo de programa eBPF, BPF_PROG_TYPE_CGROUP_SOCK_ADDR, que permite conectar-se a chamadas de sistema para criar sockets e alterar o IP de destino.
Um exemplo simplificado reescreve qualquer syscall connect4 direcionada ao DNS (porta 53) para localhost:53:
cgroupLink, err := link.AttachCgroup(link.CgroupOptions{
Path: cgroup.Name(),
Attach: ebpf.AttachCGroupInet4Connect,
Program: obj.Connect4,
})
if err != nil {
return nil, fmt.Errorf("attaching eBPF program Connect4 to cgroup: %w", err)
}
/* This is the hexadecimal representation of 127.0.0.1 address */
const __u32 ADDRESS_LOCALHOST_NETBYTEORDER = bpf_htonl(0x7f000001);
SEC("cgroup/connect4")
int connect4(struct bpf_sock_addr *ctx) {
__be32 original_ip = ctx->user_ip4;
__u16 original_port = bpf_ntohs(ctx->user_port);
if (ctx->user_port == bpf_htons(53)) {
/* For DNS Query (*:53) rewire service to backend
* 127.0.0.1:const_dns_proxy_port */
ctx->user_ip4 = const_mitm_proxy_address;
ctx->user_port = bpf_htons(const_dns_proxy_port);
}
return 1;
}
Este mecanismo intercepta consultas DNS do cGroup e as encaminha para um proxy DNS em userspace. O proxy avalia cada domínio solicitado em relação a uma lista de bloqueio e usa eBPF Maps para se comunicar com o programa CGROUP_SKB, permitindo ou negando a requisição conforme necessário.
Rastreando a Origem das Solicitações Bloqueadas
Um avanço adicional foi correlacionar requisições DNS bloqueadas com o comando ou processo específico que as acionou, facilitando a depuração e correção de problemas pelas equipes.
Dentro do tipo de programa BPF_PROG_TYPE_CGROUP_SKB, é possível extrair o ID de transação DNS e o Process ID (PID) que iniciou a requisição do skb_buff. Essas informações são colocadas em outro eBPF Map, rastreando DNS Transaction ID -> Process ID.
Um exemplo simplificado do código eBPF:
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u16 skb_read_offset = sizeof(struct iphdr) + sizeof(struct udphdr);
__u16 dns_transaction_id =
get_transaction_id_from_dns_header(skb, skb_read_offset);
if (pid && dns_transaction_id != 0) {
bpf_map_update_elem(&dns_transaction_id_to_pid, &dns_transaction_id,
pid, BPF_ANY);
}
Ao redirecionar todas as chamadas DNS para o proxy DNS em userspace, é possível analisar o ID de transação de cada requisição, encontrar o domínio sendo resolvido e consultar o eBPF Map para identificar qual processo fez a requisição. Lendo /proc/{PID}/cmdline, a linha de comando completa que acionou a requisição pode ser extraída.
Isso permite gerar logs detalhados como:
> WARN DNS BLOCKED reason=FromDNSRequest blocked=true blockedAt=dns domain=github.com. pid=266767 cmd="curl github.com " firewallMethod=blocklist
Impacto Prático e Próximos Passos
Com essa implementação, o GitHub agora pode:
- Bloquear condicionalmente domínios que causariam dependências circulares em scripts de deployment.
- Informar a equipe responsável qual comando acionou a requisição bloqueada.
- Fornecer uma lista de auditoria de todos os domínios contatados durante um deployment.
- Usar cGroups para aplicar limites de CPU e memória em scripts de deployment, prevenindo o consumo excessivo de recursos que poderia impactar outras cargas de trabalho.
Este novo processo de detecção de dependências circulares, após um rollout de seis meses, já está em produção. Se uma equipe adicionar acidentalmente uma dependência problemática, ou se uma ferramenta binária existente adquirir uma nova dependência, a ferramenta detectará o problema e o sinalizará à equipe. O resultado líquido é um GitHub mais estável e um tempo médio de recuperação (MTTR) mais rápido durante incidentes, devido à remoção dessas dependências circulares.
A exploração do eBPF continua, e novas melhorias serão implementadas à medida que novas formas de dependências circulares forem descobertas. Para os interessados em aprofundar, a documentação em docs.ebpf.io e os exemplos em cilium/ebpf são excelentes pontos de partida. Ferramentas open source como bpftrace para tracing profundo ou ptcpdump para dumps TCP com metadados de contêiner também demonstram o poder do eBPF na prática.
Aguardando Login...