Segurança e hardening do PostgreSQL

Um banco de dados desprotegido é o alvo preferido de ataques, por conter dados de usuários, credenciais e informações financeiras. Hardening do PostgreSQL começa pela configuração de rede, passa pela gestão de privilégios e termina em auditoria e monitoramento de acessos suspeitos.

Configurar pg_hba.conf com princípio do menor privilégio

O arquivo pg_hba.conf controla quem pode se conectar, de onde e como:

bash
# /etc/postgresql/17/main/pg_hba.conf
# Formato: TYPE  DATABASE  USER  ADDRESS  METHOD

# Conexão local do superusuário: apenas via socket Unix
local   all         postgres                    peer

# Conexão local de usuários da aplicação: senha
local   all         all                         scram-sha-256

# Conexão remota: apenas do IP específico da aplicação
host    minha_db    minha_app   10.0.0.2/32    scram-sha-256

# Replicação: apenas do IP do standby
host    replication  replicador  10.0.0.3/32  scram-sha-256

# NUNCA usar em produção:
# host all all 0.0.0.0/0 trust    (sem senha, qualquer IP)
# host all all 0.0.0.0/0 md5      (MD5 é vulnerável, use scram-sha-256)

# Recarregar sem restart:
sudo -u postgres psql -c "SELECT pg_reload_conf();"

Criar roles com privilégios mínimos

Separe usuários por função, aplicação, leitura, migrações:

sql
-- Conectar como superusuário
sudo -u postgres psql -d minha_db

-- Role para a aplicação (apenas operações CRUD)
CREATE ROLE app_user WITH LOGIN PASSWORD 'senha_forte';
GRANT CONNECT ON DATABASE minha_db TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;

-- Role apenas leitura (para dashboards, analytics)
CREATE ROLE readonly_user WITH LOGIN PASSWORD 'senha_readonly';
GRANT CONNECT ON DATABASE minha_db TO readonly_user;
GRANT USAGE ON SCHEMA public TO readonly_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO readonly_user;

-- Role para migrações (pode criar tabelas)
CREATE ROLE migrator WITH LOGIN PASSWORD 'senha_migrator';
GRANT CONNECT ON DATABASE minha_db TO migrator;
GRANT ALL ON SCHEMA public TO migrator;

-- NUNCA dar SUPERUSER para a aplicação
-- Verificar roles criadas:
\du

Criptografar dados sensíveis com pgcrypto

Criptografe colunas sensíveis diretamente no banco:

sql
-- Instalar extensão pgcrypto
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Tabela com coluna criptografada:
CREATE TABLE clientes (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  nome TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE,
  cpf_criptografado BYTEA,  -- armazenar CPF criptografado
  criado_em TIMESTAMPTZ DEFAULT now()
);

-- Inserir com criptografia simétrica (AES):
INSERT INTO clientes (nome, email, cpf_criptografado)
VALUES (
  'João Silva',
  'joao@example.com',
  pgp_sym_encrypt('123.456.789-00', current_setting('app.encryption_key'))
);

-- Ler com descriptografia:
SELECT nome, email,
       pgp_sym_decrypt(cpf_criptografado,
                       current_setting('app.encryption_key')) AS cpf
FROM clientes;

-- Configurar a chave de criptografia por sessão:
SET app.encryption_key = 'chave-aes-256-muito-longa-e-aleatoria';

-- Hash irreversível de senhas:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO usuarios (senha_hash) VALUES (crypt('senha123', gen_salt('bf', 12)));
-- Verificar senha:
SELECT * FROM usuarios WHERE senha_hash = crypt('senha123', senha_hash);

Auditoria com pgaudit

Registre todas as operações críticas no banco para compliance e investigação:

bash
# Instalar pgaudit
sudo apt install -y postgresql-17-pgaudit

# postgresql.conf:
shared_preload_libraries = 'pgaudit'
pgaudit.log = 'write, ddl, role, connection'
# write: INSERT, UPDATE, DELETE, TRUNCATE
# ddl: CREATE, ALTER, DROP
# role: GRANT, REVOKE, CREATE ROLE
# connection: login/logout

# Recarregar:
sudo systemctl restart postgresql

# Verificar logs de auditoria:
sudo tail -f /var/log/postgresql/postgresql-17-main.log | grep AUDIT

# Exemplo de log gerado:
# AUDIT: SESSION,1,1,DDL,CREATE TABLE,,,,
#   "CREATE TABLE dados_sensiveis (...)",<not logged>
# AUDIT: SESSION,2,1,WRITE,INSERT,TABLE,public.usuarios,
#   "INSERT INTO usuarios VALUES (...)",<not logged>

# Auditoria por objeto específico (apenas tabela critica):
-- Conectado ao banco:
SELECT pgaudit.set_config('write', 'true', true);
ALTER TABLE dados_financeiros ENABLE ROW LEVEL SECURITY;

Row-Level Security (RLS): isolamento por usuário

RLS permite que cada usuário veja apenas seus próprios dados:

sql
-- Habilitar RLS na tabela
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;
ALTER TABLE pedidos FORCE ROW LEVEL SECURITY;  -- aplica ao owner também

-- Política: usuário só vê seus próprios pedidos
CREATE POLICY pedidos_isolamento ON pedidos
  USING (usuario_id = current_setting('app.usuario_id')::uuid);

-- Política de escrita: usuário só pode criar/editar seus pedidos
CREATE POLICY pedidos_escrita ON pedidos
  FOR INSERT
  WITH CHECK (usuario_id = current_setting('app.usuario_id')::uuid);

-- Na aplicação Node.js — configurar por sessão:
// Antes de cada query de usuário:
await db.query("SET app.usuario_id = $1", [req.user.id])

// Agora todas as queries à tabela pedidos são automaticamente filtradas:
const pedidos = await db.query("SELECT * FROM pedidos")
// Retorna APENAS os pedidos do usuário logado

-- Verificar políticas criadas:
SELECT * FROM pg_policies WHERE tablename = 'pedidos';

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

SQL injection não é problema do banco, é da aplicação que constrói queries. Sempre use prepared statements ou queries parametrizadas: await db.query("SELECT * FROM users WHERE id = $1", [userId]). Nunca concatene input de usuário em SQL. ORM como Prisma, TypeORM e SQLAlchemy usam parametrização por padrão.

pg_hba.conf (Host-Based Authentication) define a política de controle de acesso do PostgreSQL: qual usuário, de qual IP, em qual banco, e com qual método de autenticação pode se conectar. É a primeira linha de defesa, configurado corretamente, bloqueia conexões não autorizadas antes mesmo de verificar a senha.

Criptografia de disco (LUKS) protege contra roubo físico do servidor ou acesso ao disco fora do PostgreSQL, irrelevante para ameaças de rede. Criptografia de coluna (pgcrypto) protege contra acesso indevido ao banco e vazamento de backups. Para compliance (LGPD, PCI-DSS): criptografia de coluna para dados sensíveis (CPF, cartão de crédito) é geralmente obrigatória.

ALTER USER minha_app WITH PASSWORD "nova_senha"; Atualiza imediatamente, conexões existentes no pool continuam funcionando com a sessão ativa, mas novas conexões precisam da nova senha. Com PgBouncer: atualize a senha no userlist.txt + reload (RELOAD; no pgbouncer), depois altere no banco e atualize a variável de ambiente da aplicação.

Não, o PostgreSQL nunca loga senhas. Com log_connections = on, loga "connection authorized: user=X database=Y", mas não a senha. Com pgaudit: loga o conteúdo das queries (cuidado se queries incluem dados sensíveis). Configure log_min_duration_statement com cuidado, queries com dados sensíveis podem aparecer nos logs.