Engenharia

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:

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:

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.

Comentários

Interações
Seu Perfil

Aguardando Login...