Segurança no PostgreSQL: conformidade, LGPD e auditoria
scram-sha-256, TLS obrigatório, o perigo do schema public, Row Level Security, auditoria com pgaudit e a pergunta que a LGPD faz: quem leu esse dado, quando e por quê. Com SQL pronto para usar.

Auditoria de segurança em banco de dados costuma ter dois tempos. O primeiro é o time dizendo que está tudo certo. O segundo é eu rodando cinco consultas e a sala ficando em silêncio. Vamos direto às cinco.
1. Autenticação: md5 ainda está lá?
O método md5 é obsoleto e quebrável. O padrão é scram-sha-256 desde o PostgreSQL 10, e mesmo assim eu ainda encontro produção com md5 porque "veio assim da migração".
SHOW password_encryption; -- tem que ser scram-sha-256
-- Quem ainda tem senha em md5
SELECT rolname FROM pg_authid WHERE rolpassword LIKE 'md5%';
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
SELECT pg_reload_conf();
-- Atencao: cada usuario listado precisa redefinir a senha para migrar o hash
ALTER ROLE app_web PASSWORD 'nova-senha-forte';
No pg_hba.conf, a ordem das linhas é lei — a primeira que casa vence. Use hostssl, nunca trust, e faixas de rede estreitas:
# TIPO BANCO USUARIO ORIGEM METODO
hostssl app app_web 10.20.0.0/24 scram-sha-256
hostssl all dba_admin 10.99.0.5/32 scram-sha-256
host all all 0.0.0.0/0 reject
2. TLS: criptografado mesmo, ou só configurado?
-- Quem esta conectado sem TLS agora
SELECT a.usename, a.client_addr, a.application_name, s.ssl, s.version, s.cipher
FROM pg_stat_activity a
LEFT JOIN pg_stat_ssl s USING (pid)
WHERE a.backend_type = 'client backend'
ORDER BY s.ssl NULLS FIRST;
Configurar ssl = on não obriga ninguém. A obrigação vem do hostssl no pg_hba. E no lado da aplicação, sslmode=require criptografa mas não valida o certificado — para proteger contra man-in-the-middle é verify-full.
3. O schema public: a porta que fica aberta
Até o PostgreSQL 14, qualquer usuário conectado podia criar objetos no schema public. O 15 corrigiu o padrão para bancos novos, mas banco migrado carrega a herança. Confira:
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
REVOKE ALL ON DATABASE app FROM PUBLIC;
-- Privilegio minimo de verdade, com roles de grupo
CREATE ROLE app_leitura NOLOGIN;
GRANT USAGE ON SCHEMA vendas TO app_leitura;
GRANT SELECT ON ALL TABLES IN SCHEMA vendas TO app_leitura;
ALTER DEFAULT PRIVILEGES IN SCHEMA vendas
GRANT SELECT ON TABLES TO app_leitura;
CREATE ROLE relatorio LOGIN PASSWORD 'xxxx' IN ROLE app_leitura;
Sem o ALTER DEFAULT PRIVILEGES, toda tabela nova nasce invisível e alguém "resolve" com um GRANT amplo. É assim que permissão apodrece.
E o clássico: a aplicação conecta como superusuário. Descubra quem:
SELECT rolname, rolsuper, rolcreaterole, rolcreatedb, rolbypassrls
FROM pg_roles WHERE rolcanlogin AND (rolsuper OR rolcreaterole OR rolbypassrls);
4. Row Level Security: isolamento que o ORM não burla
RLS aplica o filtro no banco. Se o desenvolvedor esquecer o WHERE tenant_id, o banco lembra por ele.
ALTER TABLE contratos ENABLE ROW LEVEL SECURITY;
ALTER TABLE contratos FORCE ROW LEVEL SECURITY; -- vale ate para o dono
CREATE POLICY p_contratos_tenant ON contratos
FOR ALL TO app_web
USING (tenant_id = current_setting('app.tenant_id')::bigint)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::bigint);
A aplicação define o contexto no início da transação:
BEGIN;
SELECT set_config('app.tenant_id', '42', true); -- true = escopo da transacao
SELECT * FROM contratos; -- so ve o tenant 42
COMMIT;
Dois avisos que custam caro: USING controla leitura e WITH CHECK controla escrita — sem o segundo, dá para inserir linha de outro tenant. E roles com BYPASSRLS ou superusuário ignoram tudo; é por isso que FORCE existe. Vale lembrar que RLS tem custo no plano: a policy vira predicado, então indexe a coluna do tenant.
5. Auditoria: a pergunta da LGPD
A LGPD não pergunta se o dado está seguro. Ela pergunta quem acessou, quando, para qual finalidade e por quanto tempo você guarda. Log de erro não responde nada disso. O caminho é pgaudit:
-- shared_preload_libraries = 'pgaudit' (exige restart)
CREATE EXTENSION IF NOT EXISTS pgaudit;
ALTER SYSTEM SET pgaudit.log = 'ddl, role, write';
ALTER SYSTEM SET pgaudit.log_parameter = on;
ALTER SYSTEM SET pgaudit.log_relation = on;
-- Auditoria dirigida: so o que toca dado pessoal
CREATE ROLE auditor NOLOGIN;
ALTER SYSTEM SET pgaudit.role = 'auditor';
GRANT SELECT, UPDATE, DELETE ON clientes, contratos TO auditor;
SELECT pg_reload_conf();
Auditar read em tudo gera volume absurdo de log e ninguém lê. Auditar SELECT só nas tabelas com dado pessoal, via pgaudit.role, é o equilíbrio que sobrevive ao primeiro mês.
Anonimização e minimização
-- Visao com mascaramento para ambiente de analise
CREATE VIEW clientes_mascarado AS
SELECT id,
left(nome, 1) || repeat('*', 6) AS nome,
regexp_replace(cpf, '^(\d{3})\d{6}(\d{2})