Pular para o conteúdo

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.

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.

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:

Janela do terminal
curl -O https://docs.rootpilot.sh/profiler-preflight.sh
sh profiler-preflight.sh

Baixe 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.

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.

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.)

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”.

#!/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=0
FAIL=0
UNKNOWN=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."
fi
else
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 1
fi
if [ "$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 2
fi
printf 'VERDICT: this host qualifies.\n'
exit 0
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.

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.

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 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;
  • argv e variáveis de ambiente do processo;
  • qualquer leitura de disco fora do /proc do próprio alvo.

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.

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.

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.

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.

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.