Skip to main content
Voltar para Insights
Arquitetura9 min de leitura

Isolamento multi-tenant: por que segurança em nível de linha não é suficiente

Quatro camadas de isolamento de inquilinos do gateway API ao banco de dados. Defesa em profundidade para infraestrutura financeira.

Isolamento Multi-Tenant em Sistemas Financeiros: Row-Level Security Não É Suficiente


Um desenvolvedor esquece uma cláusula WHERE. O ORM gera uma consulta sem escopo. Um endpoint admin contorna o filtro de tenant por "conveniência operacional." Um bug, uma consulta, e o Tenant A vê o histórico de transações do Tenant B.

Em um produto SaaS, isso é um vazamento de dados. Em um sistema financeiro, é também uma violação regulatória. PSD2 requer proteção de dados de pagamento. GDPR impõe multas por pessoa afetada. DORA requer que sistemas TIC previnam acesso não autorizado a dados financeiros. Um único vazamento de dados cross-tenant aciona os três.

Row-Level Security (RLS) deveria prevenir isso. O banco de dados impõe fronteiras de tenant independentemente do que o código de aplicação faça. Mas RLS é a última linha de defesa, não a única. Ele responde "esta sessão de banco de dados pode acessar esta linha?", mas alguém deve definir o contexto de sessão corretamente. Alguém deve garantir que o ID de tenant que flui pelo sistema é autêntico, não fornecido pelo chamador.

O isolamento multi-tenant em sistemas financeiros requer três camadas. Cada camada captura falhas que as outras deixam passar.

Camada 1: O Gateway

O API gateway fica entre a internet pública e a aplicação. Seu trabalho: autenticar o chamador, determinar a qual tenant pertence, e injetar essa identidade como um header confiável.

A propriedade crítica: a aplicação nunca lê a identidade do tenant do body do request do chamador, query parameters, nem JWT claims que o chamador controla. O gateway valida o token OAuth2 (ou API key), resolve o tenant associado, e define um header HTTP, X-Tenant-ID, no request interno. A aplicação lê o header. O chamador não pode falsificá-lo.

Isso previne uma categoria de ataque que a filtragem a nível de aplicação não pode: um chamador comprometido ou malicioso que envia um token de autenticação válido mas manipula o contexto de tenant. Se a aplicação lê tenant_id do body do request, um chamador com credenciais válidas pode se passar por qualquer tenant. Se a aplicação lê X-Tenant-ID de um header injetado pelo gateway, a afirmação é verificada antes que o request chegue ao código de aplicação.

Implementação: middleware ForwardAuth do Traefik, plugins do Kong, ou um gateway personalizado que chama um serviço de auth. O serviço de auth valida o token, resolve o tenant, e retorna o ID de tenant como header de resposta. O gateway o injeta. A aplicação confia nele.

Camada 2: A Aplicação

Cada método de serviço recebe contexto de tenant do header do gateway. O contexto é imutável pela duração do request. O serviço não pode construir uma consulta para um tenant diferente, não porque é proibido por convenção, mas porque a API não aceita ID de tenant como parâmetro. Ela o lê do header injetado.

Isso elimina o problema da cláusula WHERE. A aplicação não adiciona WHERE tenant_id = ? a cada consulta manualmente. O contexto de tenant é definido na conexão de banco de dados no início do request, e RLS (Camada 3) o impõe transparentemente.

Mas a camada de aplicação adiciona algo que o banco de dados não pode: validação com escopo de request. Antes de qualquer operação de escrita, o serviço verifica que os recursos sendo modificados pertencem ao tenant atual. Uma transferência da Conta A para a Conta B? Ambas as contas devem pertencer ao tenant solicitante. Uma atualização de cliente? O cliente deve pertencer ao tenant solicitante. Essas verificações ocorrem em código de aplicação, antes de tocar o banco de dados.

Por que não confiar apenas em RLS? Porque RLS opera no nível de linha. Pode prevenir ler linhas de outro tenant. Não pode prevenir operações semanticamente inválidas dentro de uma única consulta, por exemplo, construir uma transferência onde a conta de débito e a conta de crédito pertencem a tenants diferentes. Essa validação requer lógica de aplicação.

Camada 3: O Banco de Dados

Políticas de Row-Level Security do PostgreSQL em cada tabela que contém dados de tenant. A política:

CREATE POLICY tenant_isolation ON finance_transfer
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

A aplicação define app.current_tenant_id no início de cada conexão de banco de dados (do header injetado pelo gateway). Cada consulta é filtrada automaticamente. Esquecer uma cláusula WHERE é irrelevante, o banco de dados não retornará linhas de outros tenants independentemente da consulta.

O detalhe chave: o papel de banco de dados usado pela aplicação não pode contornar RLS. Apenas o papel de migração (usado para mudanças de esquema, nunca para consultas de aplicação) tem permissão BYPASSRLS. Se o papel de aplicação pudesse contornar RLS, um único bug de SET ROLE ou injeção SQL derrotaria todo o modelo de isolamento.

-- Papel de aplicação: RLS imposto
CREATE ROLE finance_api NOINHERIT NOBYPASSRLS;

-- Papel de migração: RLS contornado (apenas mudanças de esquema, nunca usado pela aplicação)
CREATE ROLE finance_admin BYPASSRLS;

Camada 4: O Ledger

Sistemas financeiros têm uma preocupação que plataformas SaaS genéricas não têm: o ledger deve impor isolamento de tenant independentemente do banco de dados relacional.

O motor de ledger opera sobre records de tamanho fixo com um campo user_data_128 que carrega informação de tenant codificada. Uma transferência entre duas contas só é válida se ambas compartilham a mesma codificação de tenant. O ledger rejeita transferências cross-tenant no nível do protocolo, antes que a transferência chegue ao armazenamento.

Isso não é redundante. É defesa em profundidade. Considere o modo de falha: um bug na camada de aplicação constrói uma transferência onde a conta de débito pertence ao Tenant A e a conta de crédito ao Tenant B. A verificação da camada de aplicação (Camada 2) deveria capturá-lo. Mas se não captura, uma validação faltante, uma race condition, um caminho de código adicionado por um novo desenvolvedor que não conhecia a verificação, o motor de ledger rejeita a transferência. O registro financeiro nunca é corrompido.

AplicaçãoBug: transfer(debit=TenantA:conta1, credit=TenantB:conta2)
Camada 2Verificação da aplicação: deveria capturarPassou (bug)
Camada 3BD RLS: ambas contas visíveis para TenantA?Bloqueado
Camada 4Motor de ledger: contas têm diferente codificação de tenantRejeitado

Duas redes de segurança independentes atrás da aplicação. Qualquer uma das duas é suficiente para prevenir a corrupção. Ambas juntas a tornam estruturalmente impossível.

Cinco Perguntas para Avaliar Seu Isolamento

Respostas binárias. Sem crédito parcial.

1. Um chamador pode definir sua própria identidade de tenant? Se a aplicação lê tenant_id do body do request ou de um JWT claim controlado pelo chamador: sim. Sua camada de gateway está faltando ou incompleta.

2. O código de aplicação pode construir uma consulta para um tenant diferente? Se algum caminho de código aceita tenant_id como parâmetro de função em vez de lê-lo do contexto do request: sim. Sua camada de aplicação tem uma lacuna.

3. Seu papel de banco de dados tem permissão para contornar RLS? Se a aplicação conecta com um papel que tem BYPASSRLS, SUPERUSER, ou é dono das tabelas: sim. Sua camada de banco de dados está quebrada.

4. Uma transferência pode mover fundos entre contas de tenants diferentes? Se o ledger não verifica independentemente a propriedade de tenant de ambas as contas em uma transferência: sim. Sua camada de ledger está faltando.

5. Seu motor de workflows propaga contexto de tenant através de fronteiras de serviço? Se chamadas cross-serviço dentro de um workflow não carregam contexto de tenant: não. Seus processos multi-passo podem vazar escopo.

Cada "sim" para perguntas 1-4 e "não" para pergunta 5 é uma lacuna. Em um sistema financeiro, cada lacuna é um achado regulatório potencial.


Leia mais: O Ledger | Segurança e Compliance


Fontes:

  • DORA, Regulamento (UE) 2022/2554, Art. 9 (Proteção e prevenção de acesso não autorizado)
  • PSD2, Diretiva 2015/2366, Art. 94 (Proteção de dados pessoais)
  • GDPR, Regulamento 2016/679, Art. 32 (Segurança do processamento)
  • Documentação do PostgreSQL: Row Security Policies (https://www.postgresql.org/docs/current/ddl-rowsecurity.html)