Istio AuthorizationPolicy: controle fino de acesso no mesh
Se você já tentou restringir qual serviço pode chamar qual no seu cluster Kubernetes usando só NetworkPolicy, sabe a limitação: dá pra controlar IP e porta, mas não dá pra dizer "só o service account sleep pode fazer GET em /info". É exatamente essa lacuna que o AuthorizationPolicy do Istio resolve, operando na camada de aplicação (L7) com identidade de workload, método HTTP, path e até claims de JWT.
Este post cobre a mecânica de avaliação do AuthorizationPolicy, os quatro tipos de ação disponíveis, uma pegadinha real com tráfego TCP e o recurso de dry-run para testar política sem derrubar produção.
Premissas
Os exemplos usam apiVersion: security.istio.io/v1 e foram adaptados da documentação oficial do Istio. Nenhum manifesto deste post foi aplicado ou validado em um cluster real neste ambiente — trate-os como ponto de partida, não como YAML pronto para produção. Este resumo também se baseia em uma única fonte (a referência oficial), sem cruzamento com benchmarks de performance, relatos de produção ou comparação com outros service meshes.
Como a decisão de acesso é tomada
Um workload pode ter várias AuthorizationPolicy aplicadas a ele, com ações diferentes. O Istio avalia isso em ordem fixa:
- CUSTOM — se houver política CUSTOM que casa com a requisição e o resultado for deny, nega.
- DENY — se houver política DENY que casa, nega.
- Se não existir nenhuma política ALLOW para o workload, permite por padrão.
- Se existir ALLOW e alguma casar, permite.
- Caso contrário, nega.
Na prática, isso significa que DENY sempre vence sobre ALLOW, e a ausência de qualquer ALLOW não bloqueia nada — só vira restritivo quando você define pelo menos uma regra ALLOW. A documentação recomenda declarar o campo action explicitamente, porque o default é ALLOW e deixar implícito dificulta auditoria.
ALLOW: liberando acesso com identidade e contexto
O caso mais comum é permitir uma origem específica para um destino específico, com condições extras:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: httpbin
namespace: foo
spec:
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/default/sa/sleep"]
- source:
namespaces: ["test"]
to:
- operation:
methods: ["GET"]
paths: ["/info*"]
- operation:
methods: ["POST"]
paths: ["/data"]
when:
- key: request.auth.claims[iss]
values: ["https://accounts.google.com"]
O ponto a entender aqui é que from usa OR entre itens da lista — a requisição passa se vier do service account sleep OU do namespace test. O mesmo vale para to: GET em /info* OU POST em /data. O when adiciona uma condição extra sobre claims de um JWT já validado pelo mesh (nesse exemplo, o issuer precisa ser o Google).
Isso é controle de acesso baseado em identidade de serviço, não em IP. Faz sentido quando seus workloads rodam com mTLS e service accounts bem definidos — se seu ambiente ainda não isolou identidades por serviço, esse é o pré-requisito antes de escrever qualquer principals.
DENY: bloqueando explicitamente, e o risco em TCP
DENY serve para expressar exceções que seriam estranhas de escrever como ALLOW. Dois exemplos da documentação:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: deny-dev-post
namespace: foo
spec:
action: DENY
rules:
- from:
- source:
namespaces: ["dev"]
to:
- operation:
methods: ["POST"]
Isso nega POST vindo do namespace dev para qualquer workload em foo. E um exemplo por porta:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: deny-port-8080-post
namespace: foo
spec:
action: DENY
rules:
- to:
- operation:
methods: ["POST"]
ports: ["8080"]
Aqui entra o ponto que mais causa incidente em produção: em tráfego TCP puro, o atributo methods não existe — não há como o Istio inspecionar método HTTP em uma conexão TCP crua. Para regras DENY, a documentação define que atributo ausente conta como match positivo. Resultado prático: a regra acima, aplicada a tráfego TCP na porta 8080, nega todo o tráfego TCP naquela porta, não só POST, porque o filtro de método simplesmente não se aplica e é tratado como "bateu".
Se você remover o ports dessa regra, o efeito é pior: nega todo tráfego TCP, de qualquer porta, para o workload. A recomendação explícita da documentação é sempre escopar DENY a uma porta específica — e redobrar a atenção quando a regra mistura atributos HTTP com destinos que também recebem tráfego TCP puro (bancos de dados, filas, qualquer coisa fora de HTTP/gRPC).
CUSTOM e AUDIT: o que cada um resolve
CUSTOM delega a decisão de autorização a um serviço externo de autorização configurado separadamente — é o ponto de extensão para lógica que não cabe em from/to/when nativos do Istio (ex.: checagem contra um serviço de políticas corporativo). Ele é avaliado antes de DENY e ALLOW, então uma negativa de CUSTOM bloqueia a requisição independente do resto.
AUDIT não decide nada sobre permitir ou negar — ele só marca a requisição para fins de log:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: audit-user-profile
namespace: foo
spec:
selector:
matchLabels:
app: myapi
action: AUDIT
rules:
- to:
- operation:
methods: ["GET"]
paths: ["/user/profile/*"]
Importante: AUDIT por si só não gera nenhum registro visível se você não tiver um plugin de auditoria separado configurado e habilitado no mesh. Se seu objetivo é só observar quem acessa um path sensível antes de decidir bloquear, confirme primeiro que esse plugin existe no seu ambiente — caso contrário a política existe, mas não produz efeito prático.
Escopo: onde a política se aplica
O alcance de uma AuthorizationPolicy é definido pela combinação de metadata.namespace com um selector opcional:
- Aplicada em um namespace normal, afeta só os workloads daquele namespace (ou os que casam com o
selector, se houver). - Aplicada no namespace raiz do mesh (tipicamente
istio-system), se propaga para todos os namespaces — útil para baseline de segurança, perigoso se aplicado sem cuidado. spec: {}(semselector, semrules) em um namespace nega tudo para os workloads daquele namespace — é a configuração de "deny all" explícito.- Uma regra vazia (
rules: - {}) faz o oposto: permite tudo.
Essa diferença entre "sem rules" e "rules com item vazio" é sutil no YAML e fácil de errar ao copiar exemplo — vale revisar com atenção antes de aplicar, principalmente no namespace raiz.
Testando antes de aplicar: dry-run
O Istio expõe uma anotação experimental para simular o efeito de uma política sem de fato impor o bloqueio:
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: deny-dryrun
namespace: foo
annotations:
istio.io/dry-run: "true"
spec:
action: DENY
selector:
matchLabels:
app: httpbin
rules:
- to:
- operation:
methods: ["POST"]
Com essa anotação, a política é avaliada e o resultado é registrado (para análise), mas o tráfego real não é afetado. Isso é especialmente valioso para regras DENY, dado o risco de bloqueio amplo em TCP descrito acima — você consegue confirmar o impacto antes de o bloqueio valer de fato. A documentação referencia uma página específica sobre tarefas de dry-run com mais detalhes operacionais, que não fazia parte do material consultado para este post; vale revisar a documentação oficial antes de depender desse fluxo em produção.
Próximo passo
Antes de escrever qualquer AuthorizationPolicy de produção, rode a versão com istio.io/dry-run: "true" primeiro — principalmente se a regra for DENY e tocar qualquer destino que recebe tráfego TCP puro além de HTTP. O custo de testar é baixo; o custo de um DENY mal escopado bloqueando tudo em uma porta de banco de dados, não.