RootPilot Profiler
Métrica e log mostram sintoma: a latência subiu, o erro apareceu. Eles não dizem o que mudou dentro do processo entre a versão que funcionava e a que não funciona.
O RootPilot Profiler mede isso. Ele observa um processo por uma janela curta depois de cada deploy e guarda uma assinatura comportamental compacta — quanto o programa aloca e onde, que chamadas de sistema faz, como as esperas por lock se distribuem, quantos peers de rede ele tem. Comparando duas versões, dá para dizer “esta é a mudança” em vez de “algo piorou”.
Isso custa uma permissão que o resto do RootPilot não pede. Esta página existe para você decidir com a lista na mão.
Ele é um segundo agente, e isso é de propósito
Seção intitulada “Ele é um segundo agente, e isso é de propósito”Seria mais simples ligar uma flag no Agent. Não fazemos isso porque uma flag deixa o privilégio invisível: quem herda o ambiente meses depois não tem como saber, olhando o deployment, que aquele container passou a poder ler a memória de outros processos. Sendo dois artefatos, o privilégio aparece na porta de entrada.
A separação não depende de disciplina. O Agent recusa subir se detectar capability de kernel no próprio processo. E o Profiler não pode ser configurado para ler as suas integrações: a imagem dele não registra connector de dado nenhum, então não existe variável que o transforme num leitor da sua stack — uma que peça isso é ignorada, e ele diz no log de boot que ignorou. Nenhum dos dois vira o outro por engano de configuração.
Até a versão 1.15.0 esta página prometia mais do que o código entregava, na direção que menos convém: o Profiler subia servindo o conjunto de dados sintético que existe para desenvolvimento. Nada da sua infraestrutura era lido — o conjunto é fabricado —, mas ele ocupava lugar de fonte na frota. Corrigido pela raiz: a classe do artefato decide, e não a configuração do deploy.
Antes de tudo: este host serve?
Seção intitulada “Antes de tudo: este host serve?”O portão mais duro aqui é o kernel, e ele reprova hosts que ninguém espera ver reprovados — por isso ele é a primeira pergunta da qualificação, não a última. Um host que não passa daqui não chega à POC; descobrir isso durante a POC custa a POC.
Rode o preflight no host candidato. Ele só lê /proc e /sys, não muda nada e não precisa de root:
curl -O https://docs.rootpilot.sh/profiler-preflight.shsh profiler-preflight.shBaixe e leia antes de executar — o script inteiro está publicado logo abaixo, e ninguém deveria rodar
script de terceiro sem olhar. É por isso que não oferecemos um curl | sh.
Os portões que ele decide
Seção intitulada “Os portões que ele decide”| Portão | Por que ele reprova, e como ele falharia calado |
|---|---|
| Kernel ≥ 5.8 | BTF, ring buffer e CAP_BPF chegam nessa versão. O filtro é o kernel, não a distribuição: RHEL 7 / 3.10 está fora, e o fallback sem eBPF é só /proc. |
| BTF compilado | Versão ≥ 5.8 não implica BTF: um kernel construído sem CONFIG_DEBUG_INFO_BTF passa no portão anterior e reprova neste. É por isso que são dois portões e não um. |
| cgroup v2 | Sem a hierarquia unificada não há como escopar uma captura por cgroup. |
| tracefs montado | É onde a biblioteca de eBPF procura os tracepoints. Sem ele os coletores carregam, não anexam, e a captura volta com eixos faltando em vez de vazia — que se lê como “esse comportamento parou”. |
| Perfil de confinamento | O docker-default só permite ptrace entre containers do mesmo perfil. Herdá-lo custa dois coletores, em silêncio. |
“Não sei” não é aprovação
Seção intitulada ““Não sei” não é aprovação”O script sai com três códigos, e o do meio é o que costuma faltar num checklist de qualificação:
0 tudo passou · 1 algum portão duro reprovou · 2 nada reprovou e algo não pôde ser decidido dali.
O caso normal do 2 são as cinco capabilities: o shell de quem roda o script não é o workload do
Profiler, então o que aquele shell tem não responde o que o Profiler poderá ter — isso é política de quem
administra o cluster. Um checklist que contasse isso como “ok” entregaria uma aprovação que ninguém
verificou, e é exatamente a falha que o script existe para evitar. (A primeira versão dele cometeu essa
falha ao contrário: checava /sys/kernel/tracing/events com test -d, que falha para usuário sem
privilégio porque o diretório é 0700 root:root — reprovando um host perfeitamente montado. Ele agora lê
/proc/mounts, que é legível por todos.)
A pergunta que vem antes das técnicas
Seção intitulada “A pergunta que vem antes das técnicas”Anexar um coletor privilegiado a software de terceiro — de fornecedor, COTS, legado sem quem o mantenha
— pode violar ou invalidar o contrato de suporte que você tem com quem o vende. É pergunta jurídica, e
por isso ela vem antes das técnicas: nenhum resultado do script a substitui. O script a imprime junto das
outras duas que ele não pode responder (hostPID e a lista de alvos), para que ela não seja esquecida por
já estar “tudo verde”.
O script, na íntegra
Seção intitulada “O script, na íntegra”#!/bin/sh# RootPilot Profiler — host preflight.## Answers one question, before anyone installs anything or grants a capability:# CAN this host run the Profiler at all? It reads; it changes nothing.## Written in English on purpose: this is one canonical file served to readers of# both the Portuguese and the English documentation, and a single script cannot# be bilingual. Each check is explained, in your language, on the page that# publishes this script verbatim: https://docs.rootpilot.sh/security/profiler/## Exit codes — a check that could not run is NOT a pass:# 0 every gate passed, and nothing was left unknown# 1 at least one hard gate FAILED — this host is out until it is fixed# 2 no gate failed, but something could not be determined from here## Run it as the least privileged user you have. It needs no root: every gate it# can decide is decidable by reading /proc and /sys.
set -u
PASS=0FAIL=0UNKNOWN=0
pass() { printf ' PASS %s\n' "$1"; PASS=$((PASS + 1)); }fail() { printf ' FAIL %s\n ↳ %s\n' "$1" "$2"; FAIL=$((FAIL + 1)); }unknown() { printf ' UNKNOWN %s\n ↳ %s\n' "$1" "$2"; UNKNOWN=$((UNKNOWN + 1)); }
printf 'RootPilot Profiler — host preflight\n'printf 'host: %s kernel: %s arch: %s\n\n' "$(uname -n)" "$(uname -r)" "$(uname -m)"
# ── 1. Kernel ≥ 5.8 ───────────────────────────────────────────────────────────# BTF, the BPF ring buffer and CAP_BPF all land in 5.8. Below it the Profiler# refuses to boot rather than run with half its collectors.release=$(uname -r)major=$(echo "$release" | cut -d. -f1)minor=$(echo "$release" | cut -d. -f2 | cut -d- -f1)case "$major$minor" in *[!0-9]*|'') unknown "kernel >= 5.8" "could not parse a version out of '$release'" ;; *) if [ "$major" -gt 5 ] || { [ "$major" -eq 5 ] && [ "$minor" -ge 8 ]; }; then pass "kernel >= 5.8 ($release)" else fail "kernel >= 5.8 ($release)" \ "this host is out. The gate is the kernel, not the distribution: RHEL 7 / 3.10 cannot run this, and --no-ebpf is only /proc." fi ;;esac
# ── 2. BTF actually compiled in ───────────────────────────────────────────────# Version >= 5.8 does NOT imply BTF: a kernel built without CONFIG_DEBUG_INFO_BTF# passes check 1 and fails here. This is the gate that surprises people, so it is# separate from the version rather than folded into it.if [ -r /sys/kernel/btf/vmlinux ]; then pass "kernel BTF present (/sys/kernel/btf/vmlinux)"else fail "kernel BTF present" \ "no /sys/kernel/btf/vmlinux. The kernel was built without CONFIG_DEBUG_INFO_BTF; the version alone never proved it."fi
# ── 3. cgroup v2 ──────────────────────────────────────────────────────────────if [ -r /sys/fs/cgroup/cgroup.controllers ]; then pass "cgroup v2 unified hierarchy"else fail "cgroup v2 unified hierarchy" \ "no /sys/fs/cgroup/cgroup.controllers. A v1-only or hybrid host cannot scope a capture by cgroup."fi
# ── 4. tracefs mounted ────────────────────────────────────────────────────────# This one earns its own gate because of HOW it fails: without tracefs the eBPF# programs load and never attach, so the capture comes back with axes MISSING# rather than empty — which reads like "nothing changed in this deploy".## It is read from /proc/mounts, which is world-readable, and NOT with# `test -d /sys/kernel/tracing/events`. That was the first version, and it is# wrong in the direction that matters: tracefs is 0700 root:root, so descending# into it as an unprivileged user fails on a host where it is perfectly mounted.# A check that reports FAIL when it could not look is the failure this whole# script exists to prevent.if [ -r /proc/mounts ]; then mounts=$(cat /proc/mounts) if echo "$mounts" | grep -qE '^tracefs +/sys/kernel/tracing '; then pass "tracefs mounted (/sys/kernel/tracing)" elif echo "$mounts" | grep -qE '^(debugfs|tracefs) +/sys/kernel/debug '; then pass "tracefs reachable under /sys/kernel/debug/tracing" else fail "tracefs mounted" \ "no tracefs in /proc/mounts. Collectors would load, attach nothing, and the capture would come back missing axes instead of empty." fielse unknown "tracefs mounted" "could not read /proc/mounts from here"fi
# ── 5. The five capabilities ──────────────────────────────────────────────────# What this shell holds is NOT the answer — the Profiler runs as its own# workload, and whether the five can be granted to it is a policy question for# whoever owns the cluster (PodSecurity / PSP / the container runtime). So this# reports and does not decide.caps=$(grep -m1 '^CapEff:' /proc/self/status 2>/dev/null | awk '{print $2}')if [ -n "${caps:-}" ]; then unknown "CAP_BPF, CAP_PERFMON, CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, CAP_SYS_ADMIN grantable" \ "not decidable from this shell (it holds CapEff=$caps). Ask whoever owns the cluster whether a workload may be granted all five. All five are required — the Profiler fails boot naming the one that is missing, and there is no reduced mode."else unknown "the five capabilities grantable" \ "could not read /proc/self/status. Ask whoever owns the cluster whether a workload may be granted all five."fi
# ── 6. Confinement profile ────────────────────────────────────────────────────# Measured, not theorised: Docker's default seccomp/AppArmor profile denies# ptrace against an unconfined target, and that alone kills two collectors.lsm=$(cat /proc/self/attr/current 2>/dev/null | tr -d '\0')case "${lsm:-}" in ''|unconfined) pass "no restrictive LSM profile on this shell (${lsm:-none})" ;; *) unknown "confinement profile" \ "this shell runs under '$lsm'. Docker's default profile denies ptrace against an unconfined target, which silently costs two collectors. The Profiler needs its own profile decided, not inherited." ;;esac
# ── 7. Questions this script cannot answer ────────────────────────────────────printf '\nNot decidable from a host, and not optional:\n'printf ' • Does your support contract with the vendor of the observed software\n'printf ' ALLOW attaching a privileged collector to it? Doing so can void support.\n'printf ' This is a legal question, and it comes before the technical ones.\n'printf ' • Can the Profiler run with hostPID, so it can see the target process?\n'printf ' • Which processes go on the target list? It is required, it is yours,\n'printf ' and there is no default that means "everything on the node".\n'
printf '\n%d passed, %d failed, %d unknown\n' "$PASS" "$FAIL" "$UNKNOWN"
if [ "$FAIL" -gt 0 ]; then printf 'VERDICT: this host does NOT qualify yet.\n' exit 1fiif [ "$UNKNOWN" -gt 0 ]; then printf 'VERDICT: no gate failed, and %d item(s) could not be decided from here.\n' "$UNKNOWN" printf 'Not a pass. Answer them before the POC, not during it.\n' exit 2fiprintf 'VERDICT: this host qualifies.\n'exit 0O que ele precisa
Seção intitulada “O que ele precisa”| Permissão | Para quê |
|---|---|
CAP_BPF |
Carregar os programas eBPF que contam chamadas de sistema, esperas por lock, alocações e eventos de rede. |
CAP_PERFMON |
Amostrar CPU por perf_event, a 100 Hz por núcleo. |
CAP_SYS_PTRACE |
Anexar os programas acima a um processo que roda como outro usuário — ler /proc/<pid>/ns/* e /proc/<pid>/exe. Sem ela o eBPF carrega e nenhum coletor anexa: a captura sai vazia, que é indistinguível de “nada mudou neste deploy”. |
CAP_DAC_READ_SEARCH |
Ler /sys/kernel/tracing (que é 0700 root:root) e o próprio /proc/self/mem do coletor. A segunda parece estranha e não é: um binário que recebe capability por file capability — que é como este evita rodar como root — passa a ter o próprio /proc/self/* pertencendo ao root. Sem ela caem os tracepoints e a detecção de versão do kernel, ou seja sete dos doze coletores. |
CAP_SYS_ADMIN |
Criar o perf_uprobe do probe de heap — o eixo que carrega func e file:line. É root-equivalente num container com hostPID. Leia o bloco abaixo antes de aprovar. |
hostPID ou shareProcessNamespace |
Enxergar o processo-alvo. Sem isso ele só enxerga a si mesmo. |
/sys/kernel/tracing e /sys/kernel/debug montados |
Onde a biblioteca de eBPF procura os tracepoints. Sem eles o coletor carrega e não anexa, e a captura sai com eixos faltando em vez de vazia. |
| Perfil AppArmor próprio | O docker-default só permite ptrace entre containers do mesmo perfil, e o alvo normalmente não é um. Publicamos um perfil que é o docker-default mais uma linha (ptrace (read) peer=unconfined) — e não apparmor=unconfined, que desfaria o confinamento inteiro por causa de uma regra. |
get · list · watch em pods |
Ainda não pedida. Será como ele percebe um deploy quando o gatilho automático existir, e só nos namespaces da sua allowlist. Hoje o Profiler não precisa de permissão nenhuma no Kubernetes. |
| Kernel ≥ 5.8, cgroup v2 | BTF, ring buffer e CAP_BPF. Abaixo disso ele recusa subir em vez de rodar pela metade. |
| Saída de rede | Apenas o túnel do RootPilot. Nenhum outro destino. |
O que ele não pede vale tanto quanto o que pede: nenhuma permissão de escrita no Kubernetes. Em
particular, ele não pede create em pods/ephemeralcontainers — a permissão que a abordagem alternativa
exigiria, e que é genérica de um jeito desconfortável: quem pode anexar container efêmero pode anexar
qualquer imagem a qualquer pod do namespace.
Quais processos ele pode observar
Seção intitulada “Quais processos ele pode observar”Você declara a lista, e ela é obrigatória. Não existe um default que signifique “tudo o que estiver no nó”: um agente que enxerga todos os processos por omissão é privilégio acima da necessidade, e a omissão não deveria ser a forma de conceder o maior escopo possível.
A lista vive num arquivo que você implanta junto com o Profiler, por namespace e workload. Fora dela, ele não anexa — e a recusa aparece no log dele, nomeando o alvo, para que “não capturou” nunca se confunda com “capturou e não achou nada”.
Ela é diferente da denylist de tools num ponto que vale entender: a denylist é composta no control-plane e aplicada na borda; esta lista é inteiramente sua, nunca passa por nós. Ela não diz o que o RootPilot pode ler — diz em quais processos seus alguém pode encostar.
Quando ele captura
Seção intitulada “Quando ele captura”Por uma janela curta — 60 segundos por padrão — sobre um processo de cada vez. Ele não fica medindo o tempo todo.
Hoje o gatilho é explícito. Uma captura é pedida: pelo seu time durante uma investigação, ou pelo seu pipeline depois de um deploy. O Profiler recebe o pedido pela mesma conexão que já usa para tudo, valida o alvo contra a sua lista antes de olhar para qualquer processo, mede, e devolve o resultado.
As duas passam pela mesma fila, por portas diferentes. Pelo app, em Profiling, qualquer membro da
organização pede num formulário — o acesso é o mesmo que abre a página, e a decisão de owner/admin já
foi tomada antes, ao ligar o Profiler. Pelo pipeline, por uma chamada autenticada com um token
rpcap_ que um owner ou admin emite, e que só enfileira pedido de captura: ela não lê nada.
O passo a passo, os campos e os limites estão em Instalar o Profiler → Pedir uma captura. Nos dois casos o registro de capturas guarda quem pediu — a pessoa ou o pipeline, nomeados — ao lado do desfecho.
Em Docker não haverá gatilho automático, mesmo depois. Poderíamos detectar deploys ouvindo o socket do Docker, e decidimos não pedir isso: montar o socket do Docker é equivalente a root na máquina, e seria a maior permissão desta página inteira — por uma conveniência.
O que sai da sua infraestrutura
Seção intitulada “O que sai da sua infraestrutura”O que atravessa é a assinatura agregada — números e nomes, nunca o que o processo manipula:
- símbolo, arquivo e módulo do ponto do código que alocou memória;
- nome e contagem das chamadas de sistema, com latência média;
- a forma da disputa por locks, sem endereço de memória;
- contadores de rede: quantos peers, erros por tipo, latência média;
- distribuições de memória e CPU;
- nome do processo, versão e ambiente;
- qual instância foi medida — o UID do pod, o id do container, ou o PID quando não há nada mais estável. Uma captura é sempre de um processo; em Kubernetes o seu workload tem várias réplicas, e sem esse identificador duas capturas de réplicas diferentes entrariam no mesmo conjunto como se fossem a mesma coisa observada duas vezes.
Símbolo e caminho de arquivo do seu código atravessam a fronteira. É a mesma classe de dado que o connector de código já lê quando você o conecta, e dizemos isso explicitamente porque não dá para redigir: apagar o nome da função destrói a única coisa que torna a assinatura útil. Se essa classe de dado não pode sair da sua fronteira, o caminho certo é não instalar o Profiler — não instalá-lo e confiar numa máscara.
Nunca sai, em nenhuma configuração:
- conteúdo de requisição, resposta, log ou qualquer payload;
- texto em claro de conexões TLS — o coletor sabe capturá-lo, e a opção fica desligada e travada;
- endereço IP de peer; só identidade de serviço resolvida;
argve variáveis de ambiente do processo;- qualquer leitura de disco fora do
/procdo próprio alvo.
Onde a assinatura fica depois
Seção intitulada “Onde a assinatura fica depois”Ela é gravada no control-plane do RootPilot, isolada por organização como o resto do que guardamos: nenhum tenant lê a assinatura de outro, e apagar a organização apaga as assinaturas junto.
O Profiler não guarda nada. Ele mede, empurra e esquece — não há banco no artefato privilegiado, e é por isso que reiniciá-lo não perde nem acumula nada.
Quem interpreta a assinatura é um serviço nosso que roda ao lado do control-plane. Ele recebe os documentos na requisição, computa a comparação e responde: não tem banco, não tem credencial sua, e não guarda o que recebeu.
A assinatura não alimenta nenhum acervo compartilhado entre clientes. O que o RootPilot promove entre tenants é anonimizado e categórico (papéis de serviço, tipos de métrica); assinatura comportamental carrega símbolo e caminho do seu código, e por isso ela fica onde foi gravada.
Como você escreve a lista de alvos
Seção intitulada “Como você escreve a lista de alvos”Um arquivo JSON, apontado por RUNNER_PROFILE_TARGETS_FILE, montado no container:
{ "targets": [ { "workload": "checkout-api" }, { "namespace": "producao", "workload": "pricing-engine" } ]}namespace é do Kubernetes e é opcional — em Docker não existe.
Três coisas que o Profiler recusa no boot, em vez de subir e capturar o que não devia:
- arquivo ausente — não há default que signifique “tudo o que estiver no nó”;
- lista vazia (
"targets": []) — é um deployment que se acha configurado e não observa nada; "workload": "*"— curinga é a omissão com outro nome.
O schema é estrito: qualquer chave que ele não conheça derruba o boot. Isso inclui um campo de comentário bem-intencionado — se precisar explicar a lista, faça num arquivo ao lado.
Arquivo e não variável de ambiente de propósito: no seu repositório de infraestrutura ele é diffável,
entra numa revisão e alguém olha. Uma lista enterrada num values.yaml não é revisada por ninguém — e
esta é literalmente a lista de onde o container privilegiado pode encostar.
Quanto custa rodar
Seção intitulada “Quanto custa rodar”Duas coisas, e elas se comportam de formas muito diferentes.
O volume é o lado tranquilo. Cada captura produz ~30 KB — a assinatura é agregado, não evento, e o tamanho não cresce com a duração da janela.
O custo em CPU depende de uma escolha sua, e a escolha é o probe de alocação. Medimos, e a resposta honesta é uma tabela:
| Alocações por segundo do processo | Todos os probes | Sem o probe de heap |
|---|---|---|
| 11 mil | +14,8% | +0,1% |
| 114 mil | +109,9% | +0,2% |
| 1,2 milhão | +404,4% | −0,9% |
| 14,4 milhões | +3214% | +2,3% |
Tempo de CPU do processo observado por unidade de trabalho. 2 núcleos aarch64, kernel 7.0, piso de
ruído medido de ±4,9%. Dados e harness em bench/ no repositório do coletor.
Leia a segunda coluna primeiro: todo o resto é de graça, em qualquer taxa. Chamadas de sistema, contenção de lock, rede, CPU — nada disso aparece na medição. O probe de heap é o custo inteiro, e ele custa proporcionalmente ao quanto o seu processo aloca.
O probe de heap é uma compra deliberada
Seção intitulada “O probe de heap é uma compra deliberada”Ele é o eixo que carrega func e file:line — é o que torna possível apontar a linha que mudou de
comportamento, e não só que o comportamento mudou. É o mais caro porque é o único que dispara uma vez
por alocação.
Capturar com ele desligado mantém chamadas de sistema, forma da contenção, rede e CPU — você continua sabendo o que mudou, e abre mão de onde. Para um serviço que aloca muito, essa é provavelmente a troca certa; para um que aloca pouco, ligá-lo custa pouco e paga bem.
A recomendação prática: ligue por serviço, não globalmente, e comece por um não-crítico para medir no seu ambiente antes de decidir.
Uma distinção que vale fazer aqui, porque as duas coisas se parecem: o custo é escolha sua, o privilégio não. Desligar o probe de heap num serviço economiza CPU dele; não muda o que o Profiler precisa para subir. As cinco permissões da tabela são exigidas de qualquer forma — inclusive de quem nunca for ligar o heap —, porque é isso que mantém todas as suas capturas comparáveis entre si.
Como desligar — e como ele é ligado
Seção intitulada “Como desligar — e como ele é ligado”Ligar é uma decisão registrada, e ela não é nossa sozinha. Um Profiler só consegue um certificado da classe privilegiada se a sua organização tiver o Profiler habilitado no control-plane. Sem isso o enrollment é recusado, e a recusa é o padrão: nasce desligado. Ligar e desligar ficam no seu log de auditoria, com quem fez e quando.
A classe vive no certificado, decidida por nós contra essa política no momento do enrollment — não é um campo que o processo declara. Um Agent não consegue se anunciar como Profiler nem por erro de configuração, e o Profiler que perde a permissão para de renovar.
Para desligar:
Pelo control-plane — desligue o Profiler nas configurações da organização. Isso não derruba o container na hora: o certificado dele vale por minutos e a renovação passa pelo mesmo gate, então ele para no fim do certificado atual. A tela diz o prazo.
Desinstalando — remova o deployment. A coleta para junto com ele, e o RootPilot continua inteiro: nenhuma outra ferramenta é afetada. As capturas que você já tem seguem legíveis, porque quem as guarda é o control-plane e não o Profiler — as ferramentas de profiling continuam na superfície do agente e passam a responder que aquele processo não tem captura.
Tirando o processo da lista de alvos — o Profiler recusa o que não está na lista e nomeia no log o alvo recusado. É o controle mais fino dos três: desliga um processo sem desligar os outros, e a lista é sua, na sua infra.
Não há caminho pelo qual nós liguemos o Profiler sozinhos. Ele é um artefato que você implanta, com uma lista de alvos que você escreve, sob uma permissão que você concede.