O OPA no Node.js permite separar decisões de política do código da aplicação. Em vez de manter regras de autorização, compliance e segurança espalhadas em condicionais, a API envia um documento JSON ao Open Policy Agent e recebe uma decisão estruturada.
OPA, sigla de Open Policy Agent, usa a linguagem declarativa Rego. Ele pode decidir se uma requisição é permitida, quais campos precisam ser ocultados, quais limites se aplicam a um tenant ou se uma configuração de infraestrutura viola uma regra. A aplicação Node.js continua responsável por autenticar, coletar contexto e aplicar a decisão.
Neste guia, você aprenderá a executar OPA como sidecar ou serviço, escrever políticas Rego, consultar a API HTTP, integrar com Express, testar policies, distribuir bundles, aplicar fail-closed e evitar que dados controlados pelo cliente sejam tratados como fatos confiáveis.
O que é Open Policy Agent?
A documentação oficial do Open Policy Agent define OPA como um policy engine geral que separa decisão de enforcement. O projeto é graduado pela CNCF e pode ser usado em microserviços, Kubernetes, CI/CD, gateways e infraestrutura.
As políticas são escritas em Rego, linguagem projetada para avaliar dados estruturados. O repositório oficial está em open-policy-agent/opa.
Decisão versus enforcement
OPA decide:
{
"allow": true,
"reason": "project_editor",
"fields": ["id", "name", "status"]
}A aplicação aplica:
if (!decision.allow) {
throw new ForbiddenError(decision.reason);
}
return pick(document, decision.fields);OPA não intercepta automaticamente a requisição quando usado diretamente pela aplicação. O middleware precisa respeitar o resultado.
Executando OPA com Docker
docker run --rm \
--name opa \
-p 8181:8181 \
-v "$PWD/policy:/policy:ro" \
openpolicyagent/opa:1.8.0 \
run --server /policyFixe uma versão específica. Uma atualização de Rego ou built-ins deve passar por testes antes de chegar à produção.
Primeira policy
Crie policy/authz.rego:
package codigofacil.authz
import rego.v1
default allow := false
allow if {
input.user.authenticated
input.action == "read"
input.resource.tenant_id == input.user.tenant_id
}
allow if {
"admin" in input.user.roles
input.resource.tenant_id == input.user.tenant_id
}A regra nega por padrão. Acesso é concedido somente quando alguma condição explícita é satisfeita.
Consultando pela linha de comando
opa eval \
--data policy/authz.rego \
--input input.json \
'data.codigofacil.authz.allow'Use --fail ou --fail-defined em scripts de validação conforme o resultado esperado.
API HTTP
Com OPA em modo servidor:
POST http://localhost:8181/v1/data/codigofacil/authzCorpo:
{
"input": {
"user": {
"id": "user-42",
"authenticated": true,
"tenant_id": "tenant-a",
"roles": ["editor"]
},
"action": "read",
"resource": {
"type": "document",
"id": "doc-7",
"tenant_id": "tenant-a"
}
}
}Resposta:
{
"result": {
"allow": true
}
}Cliente Node.js simples
async function evaluatePolicy(input, signal) {
const response = await fetch(
'http://opa:8181/v1/data/codigofacil/authz',
{
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ input }),
signal
}
);
if (!response.ok) {
throw new Error(`OPA respondeu ${response.status}`);
}
const body = await response.json();
if (!body.result) {
throw new Error('Decisão OPA ausente');
}
return body.result;
}Timeout
const decision = await evaluatePolicy(
input,
AbortSignal.timeout(500)
);Política de autorização não pode bloquear indefinidamente. Defina timeout menor que o orçamento total da requisição.
Fail-closed
let decision;
try {
decision = await evaluatePolicy(input, AbortSignal.timeout(500));
} catch (error) {
logger.error({ error }, 'OPA indisponível');
throw new ServiceUnavailableError();
}
if (decision.allow !== true) {
throw new ForbiddenError();
}Em operações sensíveis, erro, resposta ausente ou tipo inesperado devem negar. Não converta indisponibilidade em acesso permitido.
Middleware Express
function requirePolicy(action, resourceLoader) {
return async function policyMiddleware(req, res, next) {
try {
const resource = await resourceLoader(req);
const input = {
user: {
id: req.auth.userId,
tenant_id: req.auth.tenantId,
roles: req.auth.roles,
authenticated: true
},
action,
resource
};
const decision = await evaluatePolicy(
input,
AbortSignal.timeout(500)
);
if (decision.allow !== true) {
return res.status(403).json({
type: 'https://api.example.com/problems/forbidden',
title: 'Acesso negado',
status: 403
});
}
req.policy = decision;
next();
} catch (error) {
next(error);
}
};
}Consulte Problem Details no Node.js.
Não confie no corpo da requisição
Este input é perigoso:
{
"user": {
"roles": req.body.roles,
"tenant_id": req.body.tenantId
}
}Roles e tenant precisam vir de sessão validada, token verificado ou banco. O recurso deve ser carregado pelo backend, não descrito apenas pelo cliente.
Decisões estruturadas
OPA pode retornar mais que boolean:
package codigofacil.documents
import rego.v1
default decision := {
"allow": false,
"reason": "denied",
"fields": []
}
decision := {
"allow": true,
"reason": "owner",
"fields": ["id", "name", "content", "created_at"]
} if {
input.user.id == input.resource.owner_id
}A aplicação precisa validar o formato do resultado antes de usar campos ou filtros.
ABAC com OPA
Rego expressa regras baseadas em atributos:
allow if {
input.user.department == input.resource.department
input.resource.classification != "restricted"
input.context.hour >= 8
input.context.hour < 18
}Veja ABAC no Node.js.
RBAC com OPA
role_permissions := {
"viewer": {"document:read"},
"editor": {"document:read", "document:update"},
"admin": {"document:read", "document:update", "document:delete"}
}
allow if {
some role in input.user.roles
sprintf("%s:%s", [input.resource.type, input.action]) in role_permissions[role]
}Consulte RBAC no Node.js.
OPA ou OpenFGA?
OPA é um policy engine geral para atributos, contexto, infraestrutura e decisões arbitrárias. OpenFGA é especializado em autorização baseada em relações e navegação por objetos.
Os dois podem ser combinados: OpenFGA resolve relações; OPA aplica condições adicionais, como tenant, risco, horário ou classificação. Veja OpenFGA no Node.js.
Dados externos
OPA pode carregar data documents, mas não deve fazer chamadas HTTP arbitrárias em cada decisão. Prefira:
- input fornecido pela aplicação;
- bundles distribuídos pelo control plane;
- dados replicados e versionados;
- cache local;
- decisões pequenas e determinísticas.
Bundles
Um bundle contém policies e data:
bundle/
├── policies/
│ └── authz.rego
└── data.jsonEmpacote:
tar -czf bundle.tar.gz -C bundle .OPA pode buscar bundles periodicamente de um servidor configurado. Assine e proteja o canal de distribuição.
Configuração de bundle
services:
policy_control_plane:
url: https://policies.example.com
credentials:
bearer:
token: ${POLICY_BUNDLE_TOKEN}
bundles:
authz:
service: policy_control_plane
resource: bundles/authz.tar.gz
polling:
min_delay_seconds: 10
max_delay_seconds: 30Não inclua tokens diretamente no arquivo versionado.
Versão da política
Inclua metadata no bundle ou data:
{
"policy_version": "authz-2026-09-18.1"
}Retorne a versão na decisão e registre nos logs. Isso ajuda a explicar por que um acesso foi permitido ou negado.
Testes Rego
Crie authz_test.rego:
package codigofacil.authz_test
import rego.v1
import data.codigofacil.authz
test_same_tenant_reader if {
authz.allow with input as {
"user": {
"authenticated": true,
"tenant_id": "tenant-a",
"roles": []
},
"action": "read",
"resource": {
"tenant_id": "tenant-a"
}
}
}
test_cross_tenant_denied if {
not authz.allow with input as {
"user": {
"authenticated": true,
"tenant_id": "tenant-a",
"roles": ["admin"]
},
"action": "read",
"resource": {
"tenant_id": "tenant-b"
}
}
}Execute:
opa test policy -vCoverage
opa test policy --coverage --format=jsonCobertura ajuda a encontrar regras não exercitadas, mas não garante que todas as combinações de privilégio estão corretas.
Lint com Regal
Regal é um linter do ecossistema OPA:
regal lint policyFixe a versão no CI e trate warnings conforme a política do projeto.
CI
- run: opa fmt --fail policy
- run: opa check --strict policy
- run: opa test policy -v
- run: regal lint policyVeja CI para Node.js com GitHub Actions.
Policy diff
Uma alteração de policy pode ampliar acesso sem mudar o código. Pull requests precisam mostrar:
- regras alteradas;
- casos permitidos novos;
- casos negados removidos;
- versão do bundle;
- testes adicionados;
- impacto por tenant ou recurso.
Performance
Decisões locais por sidecar tendem a ser rápidas, mas policies complexas podem degradar. Meça:
- tempo de avaliação;
- tamanho do input;
- número de regras;
- uso de built-ins custosos;
- latência de rede;
- tempo de atualização do bundle.
Partial evaluation
OPA pode pré-computar partes da policy quando alguns dados são conhecidos. Isso é útil para gerar filtros ou reduzir trabalho repetido. A técnica exige modelagem cuidadosa e testes do resultado.
Data filtering
Em vez de checar cada registro, a policy pode produzir uma condição usada na consulta. Nunca concatene SQL retornado diretamente. Traduza uma estrutura validada para parâmetros e operadores permitidos.
Multi-tenancy
Inclua tenant no input e aplique proteção no banco. OPA não substitui Row-Level Security nem filtros de query.
Consulte Multi-Tenancy no Node.js.
Cache de decisões
Cache pode reduzir latência, mas a chave precisa incluir todos os atributos relevantes e a versão da policy. Uma decisão baseada em horário, risco ou estado mutável pode não ser segura para cache longo.
Auditoria
logger.info({
userId: req.auth.userId,
action,
resourceType: resource.type,
resourceId: resource.id,
allowed: decision.allow,
reason: decision.reason,
policyVersion: decision.policy_version
}, 'Decisão de política');Não registre input completo quando contém dados pessoais.
Observabilidade do OPA
Monitore:
- latência de decisão;
- erros e respostas indefinidas;
- bundle ativo;
- falhas de download;
- uso de memória;
- taxa de allow e deny;
- timeouts na aplicação;
- decisões por versão.
Undefined não é allow
Quando uma query não produz resultado, trate como erro ou deny:
if (typeof body.result?.allow !== 'boolean') {
throw new Error('Decisão OPA inválida');
}Segurança da API do OPA
- Não exponha OPA diretamente à internet.
- Use rede privada ou sidecar.
- Proteja management APIs.
- Use TLS quando atravessa hosts.
- Restrinja quem publica bundles.
- Valide tamanho do input.
- Não permita escrita de policies pela aplicação comum.
Erros comuns
- Dados do cliente como verdade: atacante inventa roles.
- Fail-open: indisponibilidade concede acesso.
- Policy sem testes: privilégio aumenta silenciosamente.
- Input gigante: decisão fica lenta.
- OPA exposto: management API pode ser abusada.
- Bundle sem versão: auditoria não explica decisões.
- Cache sem contexto: decisão de outro tenant é reutilizada.
- Confundir decisão com enforcement: código ignora o resultado.
Conclusão
O OPA no Node.js centraliza policies e separa decisão de aplicação. Rego expressa regras de autorização, compliance e segurança sobre documentos JSON, enquanto a API Node.js coleta contexto e aplica a resposta.
Negue por padrão, use input derivado de fontes confiáveis, distribua bundles versionados e teste policies no CI. Execute OPA próximo da aplicação, defina timeout e falha fechada. Assim, políticas podem evoluir com revisão e auditoria sem ficarem escondidas em dezenas de condicionais.



