Rodamos uma probe cross-tenant que ninguém pediu, porque portas são encontradas.
"O isolamento está ligado" é uma frase; uma probe que lê os dados de outro tenant e falha é uma defesa.
Apollo Space Research
Apollo Space
A gente escreveu um teste cujo trabalho inteiro é roubar da gente. Ele faz login como uma empresa, então atravessa o muro e pede os dados de outra empresa, notas fiscais, agents, conversas, tudo. Toda vez que fazemos deploy, esse teste roda antes de um único cliente rodar. A gente está torcendo pra ele voltar de mãos vazias.
No dia em que ele não voltar de mãos vazias, ninguém entrega.
A maioria dos times vai te dizer que os tenants deles estão isolados. Eles falam sério. Eles também não conseguem te mostrar o teste que provaria que estão errados se estivessem mentindo pra si mesmos.
Este post é sobre a diferença entre acreditar que os seus muros estão de pé e ser dono de uma máquina que fica tentando atravessá-los. A versão curta: “o isolamento está ligado” é uma frase; uma probe que lê os dados de outro tenant e falha é uma defesa.
A versão ingênua: confie no WHERE
O jeito óbvio de manter os dados de dois clientes separados é marcar cada linha com de quem ela é, e então lembrar de filtrar por essa marca em todo lugar.
Você adiciona uma coluna org_id a cada tabela. Toda query que lê dados ganha uma condição: só linhas onde org_id é igual à empresa que está logada. O plano é sólido. Pras primeiras cinquenta queries, ele até segura.
Aí a codebase cresce, e o plano começa a depender da memória.
Um novo endpoint é entregue numa sexta e o desenvolvedor esquece o filtro numa de três queries. Um reporting job escrito pra um dashboard interno roda sem um escopo de tenant porque, na época, só havia um tenant. Um agent ganha uma nova ferramenta que roda uma query que o autor original nunca revisou. Nenhuma dessas é exótica. Elas são a coisa mais ordinária no software: um ser humano, sob prazo, esquecendo uma linha num lugar de dez mil.
E a falha é silenciosa. O filtro faltando não dispara um erro. A query roda bem, retorna linhas, a feature funciona na demo. Ela só também retorna linhas que pertencem a outra pessoa, e você não descobre até um cliente descobrir. A defesa inteira repousava em todo engenheiro lembrar de uma cláusula pra sempre, e isso não é uma defesa. É um desejo com boas intenções.
Um filtro que você tem de lembrar é um filtro que você vai eventualmente esquecer.
Dois muros, porque um muro confia na sua memória
A correção não é lembrar com mais força. É tornar o muro algo que o banco de dados impõe quer alguém tenha lembrado a cláusula ou não.
Bancos de dados modernos conseguem segurar a regra eles mesmos. Você diz ao banco, uma vez, no nível da tabela: uma linha só é visível pra empresa que a possui, ponto final. Depois disso, não importa se uma query lembrou de filtrar. O banco se recusa a entregar linhas que não pertencem ao chamador, do mesmo jeito que se recusa a dividir por zero. A regra mora abaixo da aplicação, onde um endpoint esquecido de sexta-feira não consegue contorná-la.
Esse é o primeiro muro, e ele é o forte. Ele transforma “todo engenheiro tem de lembrar a cláusula” em “o engine impõe a cláusula”, que é a diferença entre uma política e um hábito.
A gente mantém o filtro no nível da aplicação também. Não porque a gente não confia no primeiro muro, porque dois muros independentes falham independentemente. Se um erro de configuração algum dia enfraquecesse a regra do banco, o filtro explícito na query ainda está de pé, e vice-versa. Cinto e suspensório. A ideia-chave é simples: o custo de um segundo muro é alguns caracteres a mais por query, e o custo de um muro falhar sozinho é uma manchete.
Mas aqui está a armadilha que pega times cuidadosos. Você pode construir os dois muros corretamente na segunda e ter um buraco na sexta, porque muros são configuração, e configuração deriva. Um role de banco criado com um privilégio a mais silenciosamente contorna a regra que deveria obedecer. Uma migration altera uma tabela e a proteção não se transfere. Os muros são reais no dia em que você os constrói. A pergunta é se eles ainda são reais no dia que importa, e você não consegue responder essa pergunta lendo o código, porque o código parece ok. O buraco está na lacuna entre o que o código diz e o que o sistema rodando faz.
Então a defesa real não são os muros. É a coisa que fica checando os muros.
A probe: um teste que tenta te roubar
Aqui está a jogada que transforma isolamento de uma crença numa propriedade. A gente escreveu um adversário e o apontou pra nós mesmos.
A probe é um teste, mas ela não testa do jeito que a maioria dos testes testa. Um teste normal confirma o happy path: faça login como uma empresa, busque os seus próprios dados, afirme que estão lá. Útil, e completamente cego pra coisa que a gente de fato teme. Os nossos testes de happy path passaram no pior dia que um sistema multi-tenant pode ter, porque retornar os seus próprios dados corretamente e retornar os dados de outra pessoa por acidente não são mutuamente exclusivos. Um vazamento não quebra a feature. É isso que o torna um vazamento.
Então a probe inverte a pergunta. Ela se autentica como um tenant, então deliberadamente tenta ler as linhas de um segundo tenant, por id, por query, pelas ferramentas do agent, pelas rotas da API, em qualquer lugar que um atacante real ou um bug honesto pudessem alcançar. E ela afirma o oposto de um teste normal. Ela não afirma “eu peguei dados.” Ela afirma “eu peguei nada, e fui corretamente recusada.” Uma aprovação é uma porta trancada. Uma linha retornada é uma falha, alta e vermelha, antes daquele build ter permissão de chegar perto de um cliente.
Um detalhe importa mais do que parece. A probe se conecta ao banco como o mesmo tipo de conta restrita que a aplicação real usa, nunca como um role admin todo-poderoso. Isso soa como uma nota de rodapé e é na verdade o ponto inteiro. Uma conta superusuária em muitos bancos tem permissão de ignorar a regra de nível de linha inteiramente; ela vê tudo por design. Se você testa o seu isolamento conectado como aquela conta, toda probe passa, porque a conta com a qual você testou nunca esteve sujeita ao muro afinal. Você teria provado que o seu bypass funciona. A gente roda a probe como o role trancado com o qual o produto roda, pra que o teste sinta exatamente o que a sessão de um cliente sente. Teste o sistema que você entrega, não um primo mais amigável dele.
E a probe roda em todo deploy. Não uma vez durante uma auditoria, não trimestralmente quando alguém lembra. Todo build, antes do release, o adversário dá a corrida dele nos muros. Os muros não derivam sem ser notados, porque algo está sempre tentando atravessá-los.
A porta que ela vai encontrar
A gente não escreveu aquela linha de abertura como uma metáfora. A razão pela qual você roda uma probe assim é que, rodada contra um canto fresco de qualquer sistema em crescimento, ela eventualmente encontra uma porta.
Essa é a parte de que os times não falam, então a gente vai falar: construir os muros e construir a probe são dois dias diferentes, e a primeira vez que uma probe roda contra um canto do sistema que se assumiu que os muros cobriam, ela pode voltar com linhas que nunca deveria ter visto. Imagine como acontece. Existe um caminho, uma query específica alcançável por uma ferramenta específica, onde o escopo de tenant nunca foi aplicado, e o role de banco naquele caminho tem latitude suficiente pra retornar as linhas mesmo assim. Os dois muros têm uma lacuna combinando no mesmo ponto. A probe atravessa em linha reta.
Aqui está a coisa pra sentar e pensar. Esse tipo de lacuna já está no sistema rodando, no dia em que ela existe. A probe não cria o buraco; ela encontra um que estava lá, um que os testes de happy path estavam felizes com, que uma code review passou, que uma demo mostrou funcionando. A única diferença entre um time que pega isso e um time que entrega isso é se um adversário seu chegou lá antes da curiosidade de um cliente. A porta é real de qualquer jeito. Você ou é dono da coisa que bate em toda porta antes dos estranhos, ou não é.
Você não consegue descobrir que o seu isolamento funciona. Você só consegue descobrir onde ele não funciona, e você prefere descobrir isso você mesmo.
Você fecha uma porta encontrada do jeito chato, escope a query, aperte o role, rode a probe de novo até ela voltar recusada, e então você adiciona aquele caminho exato à lista de rotas permanente da probe, pra que ele nunca possa silenciosamente reabrir. Esse é o loop que importa. Toda porta que a probe encontra vira uma porta que ela checa pra sempre. A defesa fica mais forte cada vez que pega algo, que é o oposto de como confiar-no-filtro degrada ao longo do tempo.
Por que isto é uma defesa e não um checkbox
Existe uma versão mais branda de tudo isso que parece a mesma num slide e não é a mesma de jeito nenhum.
A versão branda é uma frase num questionário de segurança: Sim, os dados de tenant são isolados. Todo fornecedor marca aquele checkbox, inclusive os que estão prestes a vazar. O checkbox pergunta se você pretende isolar tenants. Ele não pergunta se uma máquina verifica essa intenção em todo deploy, contra o sistema rodando, como o role restrito que um cliente de fato usa. Essas são afirmações muito diferentes, e só uma delas sobrevive ao contato com uma sexta-feira descuidada.
A diferença é o que acontece quando alguém comete um erro, e alguém sempre comete um erro. Sob o checkbox, o erro é entregue, fica silenciosamente retornando as linhas erradas, e aparece como um incidente. Sob a probe, o erro encontra um adversário no mesmo dia em que é escrito, falha um deploy, e aparece como um teste vermelho na tela de um desenvolvedor sem ninguém fora do prédio ficar sabendo. Mesmo erro. A presença ou ausência da probe decide se ele vira uma brecha ou um bug.
Essa é a troca que a gente vai defender em qualquer sala. A probe nos custa deploys que ela para e engineer-minutes que ela gasta. O que ela compra é que a pior categoria de falha em software multi-tenant, um cliente vendo os dados de outro, tem de passar por algo construído especificamente pra pegá-la, toda santa vez, em vez de por um humano que estava cansado e lembrou nove cláusulas de dez.
A virada: paranoia é um serviço que você deve às pessoas
Os roles de banco e as listas de rotas são roupa recente em algo mais antigo que software.
Quando um cliente te entrega os dados dele, ele não está comprando as suas features. Ele está te estendendo uma confiança que ele mesmo não consegue verificar, ele não consegue ler o seu código, não consegue ver as suas tabelas, não consegue assistir os seus deploys. Ele está acreditando na sua palavra de que os livros dele e as conversas dele e os segredos dele não vão acabar na frente de um concorrente que por acaso usa o mesmo produto. A probe é o que a gente faz com essa confiança quando ninguém está olhando. É a gente, numa terça-feira ordinária sem auditoria agendada e nenhum cliente pedindo, pagando um agent pra tentar roubar os nossos próprios clientes pra que mais ninguém consiga.
Essa postura, assuma que o muro tem um buraco, vá encontrá-lo antes de qualquer outro, e nunca pare de procurar, não é uma feature que você instala. É um temperamento, e tem de estar lá nos dias chatos, os sem incidente e sem prazo, quando seria tão fácil confiar nos testes verdes e entregar. A probe é só esse temperamento tornado mecânico, pra que ele segure mesmo quando os humanos estão cansados. A coisa mais tranquilizadora que a gente consegue dizer a um cliente não é “o nosso isolamento funciona.” É “a gente tem uma máquina cujo trabalho inteiro é provar que ele não funciona, e ela roda antes de você fazer login.”
“O isolamento está ligado” é uma frase. Uma probe que lê os dados de outro tenant e falha é uma defesa. A gente prefere te entregar a segunda coisa.
É isso que estamos construindo na Apollo Space, um sistema operacional que não te pede pra confiar nos muros dele, mas te mostra o adversário que ele roda contra eles todo dia. Se você já assinou o questionário de um fornecedor e silenciosamente se perguntou quem de fato checa o checkbox, você já entende por que a gente prefere arrombar nós mesmos primeiro.
A Apollo cuida da operação repetitiva da sua empresa pro seu time não precisar.
Entre na lista de espera: acesso antecipado, preço de usuário fundador e um lugar na primeira fila enquanto a gente constrói.
Entrar na lista de esperaO imposto oculto dos agents em paralelo é um diamante de migrations
Seis agents escrevendo para um schema conflitam no banco de dados, não no código, e a CI morre em "multiple heads".
EngenhariaUm orchestrator que não sobrevive ao próprio crash não é um
Um crash que apaga o raciocínio do orchestrator perde a única coisa que você não consegue reconstruir.
EngenhariaColoque um portão determinístico na frente do seu revisor mais esperto
A pega-defeito mais barata é um script burro que checa se duas branches mergeadas ainda sobem antes de qualquer julgamento.