NubliVaultby Nublify
← Blog

· 8 min de leitura

Multi-tenant: como impedir que o backup de um cliente vaze no de outro

Validação em camada de aplicação falha pelo caminho que ninguém revisou. O isolamento que sobrevive a um time distraído está no banco, não no código.

Todo sistema de backup multi-tenant tem o mesmo ponto de ruína: uma consulta sem WHERE tenant_id = ?. Não é um ataque sofisticado. É um endpoint novo escrito na sexta-feira, um relatório gerencial, um job de limpeza, uma query rodada à mão para responder um chamado de suporte.

O estrago é desproporcional ao erro. Num sistema de backup, "vazar um registro" raramente significa vazar um registro. Significa expor a árvore de diretórios de outra empresa, os nomes dos arquivos, os horários de trabalho, o tamanho da base de dados do ERP — e, no pior caso, dar a um cliente a capacidade de restaurar o conteúdo de outro.

Por que validar na aplicação não basta

A verificação na camada de aplicação funciona. O problema é que ela funciona exatamente nos lugares onde alguém se lembrou de escrevê-la.

Os caminhos que escapam são sempre os mesmos:

  • Endpoints novos. O time copia um controller existente, muda a entidade e esquece o filtro. Code review pega a maioria. "A maioria" não é um controle de segurança.
  • Jobs assíncronos. Worker de expiração, recálculo de uso, coleta de métricas. Rodam sem usuário autenticado, então o desenvolvedor não tem o contexto de tenant na mão e escreve a query global.
  • Relatórios e exportações. Quase sempre SQL cru, quase sempre fora do ORM, quase sempre fora da camada que aplica a regra.
  • Migrações e scripts de correção. Alguém precisa consertar 4.000 linhas às duas da manhã. O script não passa pela aplicação.
  • Carregamento por ID. GET /snapshots/8142 carrega o snapshot e devolve. Se o ID é global e sequencial, enumerar é trivial.
  • Suporte. A ferramenta interna que lê qualquer tenant é legítima — até a credencial dela ser reutilizada por um serviço que não deveria tê-la.

A regra prática: se o isolamento depende de alguém lembrar, ele vai falhar na proporção do tamanho do time multiplicado pelo tempo. Isolamento precisa ser a condição padrão, não a ação consciente.

A superfície de vazamento de um sistema de backup

Vale listar, porque é maior do que parece:

  1. O índice. Caminho completo, nome, tamanho, data de modificação, hash. É o mapa da empresa. Vaza sozinho, sem nenhum byte de conteúdo.
  2. Os blocos de conteúdo. O dado em si.
  3. As chaves. Se existe uma chave por tenant, o controle de quem pode lê-la é tão crítico quanto o controle do dado — assunto tratado em quem consegue abrir o seu backup.
  4. A restauração. O caminho menos exercitado do sistema, e o que entrega conteúdo direto na mão de alguém. Falamos disso em por que o restore é a parte que ninguém testa.
  5. Logs, métricas e telemetria. Caminho de arquivo em mensagem de erro é vazamento com retenção longa e cópia em três sistemas de observabilidade.
  6. A deduplicação global. Este merece seção própria.

Deduplicação entre tenants vaza por construção

Deduplicação por hash de conteúdo compartilhada entre clientes economiza armazenamento e cria um oráculo de existência. Quem puder observar que um upload foi resolvido como "já existe" aprende que outro cliente do mesmo sistema tem aquele arquivo exato. Para um arquivo público isso é inofensivo. Para um contrato em PDF, uma planilha de folha ou um binário interno, não é.

A mitigação é chata e conhecida: escopo de deduplicação por tenant, ou chave de derivação distinta por tenant antes do hash. Você paga em espaço o que economiza em risco. É uma decisão explícita, não um detalhe de implementação.

O isolamento que sobrevive: restrição no banco

A ideia é inverter o padrão. Em vez de a query precisar pedir o filtro, ela precisa pedir permissão para não ter.

1. tenant_id em toda tabela de dados, NOT NULL. Sem exceção "porque essa tabela é de metadados". Metadado de backup é dado de cliente.

2. Chave primária composta: (tenant_id, id). E toda chave estrangeira carrega o tenant_id junto:

PRIMARY KEY (tenant_id, id),
FOREIGN KEY (tenant_id, snapshot_id)
  REFERENCES snapshots (tenant_id, id)

Isto é o que impede, em nível de banco, que um registro de um tenant aponte para o pai de outro. É a classe de bug que a revisão de código tem dificuldade de pegar, porque o código parece certo.

3. Row Level Security ligada e forçada. No PostgreSQL, a política vive na tabela e vale para qualquer caminho de acesso — ORM, SQL cru, job, psql do plantão. A documentação oficial descreve o comportamento em Row Security Policies.

Quatro detalhes que decidem se a proteção é real:

  • ALTER TABLE ... FORCE ROW LEVEL SECURITY. Sem FORCE, o dono da tabela não é submetido à política — e a aplicação costuma conectar como dono.
  • A role da aplicação não pode ter BYPASSRLS nem ser superusuário.
  • A gravação também precisa estar coberta, não só a leitura. Numa política FOR ALL ou FOR UPDATE sem WITH CHECK, o PostgreSQL usa a própria expressão USING para recusar linha nova com tenant_id alheio; uma política FOR INSERT só aceita WITH CHECK. Declarar WITH CHECK explicitamente não muda o efeito e deixa a intenção legível.
  • O tenant vem de um parâmetro de sessão, por exemplo current_setting('app.tenant_id'), definido no início de cada transação com SET LOCAL. Com pool em modo transação, SET sem LOCAL vaza o contexto para a próxima transação, que pode ser de outro cliente. Esse é o erro de configuração mais perigoso do conjunto, porque o sistema funciona perfeitamente em desenvolvimento, onde não há pool.

4. IDs opacos no exterior. UUID ou identificador aleatório na API. Não resolve autorização — só tira a enumeração trivial da mesa.

5. O mesmo princípio no armazenamento de objetos. Prefixo por tenant, derivado do contexto autenticado no servidor e nunca de um campo vindo do cliente. Onde o provedor permitir, credencial temporária restrita ao prefixo, para que o escopo seja imposto fora do seu processo. URLs pré-assinadas geradas por tenant, com validade curta.

Como testar: escreva o teste que tenta vazar

Teste que confirma "o cliente A vê os dados de A" passa mesmo com a RLS desligada. O teste que importa é o inverso.

  • Para cada endpoint, uma chamada autenticada como tenant B pedindo um ID de A. Resposta esperada: 404, não 403 — 403 confirma a existência do recurso.
  • Um teste de banco que abre conexão com a role da aplicação, define app.tenant_id para B e tenta SELECT, UPDATE e DELETE em linhas de A.
  • Uma verificação de esquema no CI que falha se existir tabela de dados sem tenant_id ou sem política ativa. Isso cobre a tabela que vai ser criada daqui a seis meses por alguém que não leu este documento.
  • Um teste de restauração completo com dois tenants povoados, conferindo a contagem e os hashes do conteúdo entregue.

O que este artigo não prova

Esta abordagem tem custo e tem limites, e omiti-los seria desonesto.

RLS afeta o planejador. A condição da política entra em toda consulta. Em tabelas grandes, isso muda planos e exige que os índices comecem por tenant_id. Medir no seu volume é obrigatório; não existe número genérico aqui.

Chave composta encarece. Índices maiores, joins mais verbosos, ORMs que reclamam. É trabalho real de refatoração num sistema que já está em produção.

Nem todo banco oferece isso. Em bancos sem RLS, o equivalente possível é usuário por tenant, schema por tenant ou visões restritas — cada um com uma lista própria de armadilhas. O princípio permanece; a implementação não se transporta.

Um banco por tenant isola melhor. É a opção mais forte e a mais cara: mais conexões, mais migrações, mais monitoramento, mais trabalho em cada deploy. Para poucos clientes grandes costuma compensar. Para muitos clientes pequenos, raramente.

Isolamento no banco não cobre tudo. Não protege contra credencial de administrador comprometida, bug no provedor de armazenamento, chave compartilhada entre tenants ou cache em memória indexado sem o tenant na chave. Cache é um ponto cego frequente: a política do banco não enxerga o Redis.

Nada disso substitui retenção e imutabilidade. Isolamento impede que um cliente leia o backup de outro. Não impede que o backup seja apagado — esse é outro controle, descrito em backup imutável com Object Lock.

O resumo é desanimador e verdadeiro: a parte eficaz da solução é burocrática. Constraint, política, teste negativo, verificação de esquema no CI. Não há nada elegante nisso. O que há é um sistema em que a consulta errada falha em vez de devolver os dados de outra empresa.


O NubliVault é o produto de backup da Nublify: empacota milhões de arquivos pequenos e os guarda numa nuvem diferente da de origem. Como funciona · Segurança