Segurança e identidade
Arquitetura de segurança de aplicações: threat modeling e princípios de design
Intermediário10 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Segurança adicionada no fim sai cara e fica incompleta. Este guia mostra como tratar a segurança como uma propriedade da arquitetura: representar o sistema como fluxos de dados e fronteiras de confiança, levantar ameaças com STRIDE, aplicar os princípios clássicos de proteção, decidir qual camada aplica cada controle e incorporar toda a prática ao pipeline de entrega.
O que você vai aprender
- Desenhar um diagrama de fluxo de dados com fronteiras de confiança e usá-lo para raciocinar sobre a superfície de ataque
- Levantar e priorizar ameaças com STRIDE e transformar cada uma em um controle concreto
- Aplicar menor privilégio, defesa em profundidade, mediação completa, falha segura e padrões seguros a designs reais
- Escolher a camada certa para autenticação, autorização e validação de entrada
- Gerenciar os segredos da aplicação sem nunca colocá-los no controle de versão
- Incorporar as atividades de segurança ao pipeline de entrega em vez de deixá-las para uma auditoria antes do release
Quase toda violação séria remonta a uma decisão, não a um erro de digitação: um serviço que recebeu credenciais de banco de dados de que nunca precisou, um endpoint interno que todo mundo supunha inacessível, uma integração que confiava em um identificador fornecido pelo chamador. São problemas de arquitetura, e nenhum cuidado com o código mais adiante os compensa por completo. Projetar a segurança desde o início significa perguntar, enquanto as caixas e setas ainda são baratas de mover, quem pode falar com o quê, o que cada um tem permissão de fazer e o que acontece quando uma das caixas fica sob o controle de um atacante.
Este guia trata desse trabalho de design. Ele mostra como modelar um sistema para que as ameaças fiquem visíveis, apresenta o pequeno conjunto de princípios que sobreviveram a cinquenta anos de prática, indica onde cada tipo de controle se encaixa em uma aplicação em camadas, explica como lidar com segredos e como manter tudo isso funcionando como parte da entrega normal, e não como uma correria antes do release. As categorias de risco específicas estão catalogadas no guia OWASP Top 10; aqui estamos um nível acima, decidindo a forma do sistema que torna esses riscos improváveis.
Segurança é uma propriedade do design
Requisitos funcionais descrevem o que o sistema deve fazer. Requisitos de segurança descrevem o que ele nunca deve fazer, não importa quem esteja pedindo, e requisitos negativos não podem ser testados de forma exaustiva; é por isso que precisam ser estruturais. "O serviço de relatórios não consegue ler os dados de pagamento dos clientes" é uma afirmação forte quando esse serviço não tem nenhuma credencial que alcance aquela tabela; é uma afirmação fraca quando é só uma convenção de code review.
Três perguntas de arquitetura carregam a maior parte do peso:
- Quais são os ativos? Dados pessoais, movimentação de dinheiro, credenciais, trilhas de auditoria, disponibilidade de um fluxo crítico. Priorize-os, porque você não consegue proteger tudo da mesma forma.
- Onde estão as fronteiras de confiança? Todo ponto em que os dados passam de um lugar que você controla para outro que não controla, ou entre dois componentes com privilégios diferentes, é uma fronteira que precisa de autenticação, autorização e validação.
- Qual é o raio de impacto? Se um componente for totalmente comprometido, até onde o atacante chega? Um design cuja resposta é "a tudo" precisa ser revisto, por melhor que seja o tratamento de entrada.
Threat modeling com fluxos de dados e STRIDE
Um threat model útil começa com um diagrama que qualquer pessoa do time conseguiria desenhar: atores externos, processos, armazenamentos de dados, os fluxos entre eles e linhas tracejadas para as fronteiras de confiança. Detalhe é o inimigo: uma página, no nível de "o navegador fala com o API gateway, que chama o serviço de pedidos, que grava no banco de pedidos e publica um evento".
[Browser] --https--> | TB1 | [API gateway] --mTLS--> [Order service] --TLS--> (Orders DB)
| |
v v
[Identity provider] | TB2 | [Payment provider]
TB1 internet / platform boundary — untrusted input, session establishment
TB2 organisation / third party — outbound secrets, response we do not control
Depois, passe cada elemento e cada fluxo pelo STRIDE, um mnemônico que associa cada categoria de ameaça à propriedade que ela viola:
| Ameaça | Propriedade perdida | Controle típico |
|---|---|---|
| Spoofing (falsificação de identidade) | Autenticidade | Autenticação forte de usuários e de serviços (TLS mútuo, tokens assinados) |
| Tampering (adulteração) | Integridade | Assinaturas, transporte com proteção de integridade, validação no servidor, registros de auditoria imutáveis |
| Repudiation (repúdio) | Não repúdio | Logs append-only com identidade confiável, transações assinadas |
| Information disclosure (divulgação de informações) | Confidencialidade | Criptografia em trânsito e em repouso, minimização, mensagens de erro cuidadosas |
| Denial of service (negação de serviço) | Disponibilidade | Cotas, rate limiting, timeouts, bulkheads, limites de recursos no parsing |
| Elevation of privilege (elevação de privilégio) | Autorização | Menor privilégio, mediação completa, verificações no nível do recurso |
Registre cada achado como um risco de uma linha, com o controle que o resolve, um responsável e uma decisão. Classificar por probabilidade e impacto é suficiente; pontuações elaboradas raramente mudam o que o time faz em seguida.
- id: "TM-014"
element: "Order service -> Payment provider"
threat: "Information disclosure"
description: "API key for the payment provider is read from an environment variable and printed by the outbound HTTP logger."
likelihood: "medium"
impact: "high"
control: "Redact Authorization headers in the client interceptor; move the key to the secret manager with 90-day rotation."
status: "accepted-fix"
owner: "payments"
Junto com o modelo, escreva abuse cases ao lado das user stories que eles invertem. Se a story é "um cliente baixa a própria fatura", o abuse case é "um cliente baixa a fatura de outra pessoa trocando o identificador na URL", e isso vira um teste. Times que acrescentam um abuse case para cada story relevante pegam a maior parte dos defeitos de controle de acesso antes mesmo de eles existirem.
Os princípios que continuam compensando
Menor privilégio. Cada identidade (pessoa, serviço, job batch) recebe o conjunto mais restrito de permissões que lhe permite fazer seu trabalho, e nada além disso. Na prática, isso significa usuários de banco de dados separados por serviço, com grants limitados às tabelas que eles usam, roles de nuvem restritos a recursos específicos e contêineres que não rodam como root. Significa também que privilégios expiram: uma elevação temporária que nunca é revogada é igual a uma permanente.
Defesa em profundidade. Parta do princípio de que qualquer controle isolado vai falhar. Uma query parametrizada, uma conta de banco de dados sem permissão de DROP e um alerta para volume incomum de queries defendem o mesmo ativo por mecanismos diferentes.
Mediação completa. Todo acesso a todo objeto protegido é verificado, sempre, no servidor. Guardar em cache uma decisão de autorização pela duração da sessão é o que faz "o usuário era admin quando fez login" sobreviver ao seu rebaixamento.
Falha segura. Quando algo dá errado (o serviço de políticas estoura o timeout, falta uma claim, um certificado não pode ser validado), o resultado seguro é negar. É fácil inverter isso em um código que trata uma exceção como "desconhecido, então permite".
public boolean canView(User user, Document doc) {
try {
return policyEngine.evaluate(user, "document:read", doc);
} catch (PolicyUnavailableException e) {
log.warn("policy engine unavailable, denying document {}", doc.id(), e);
return false; // deny on failure, never "return true" to keep the page working
}
}
Padrões seguros. O estado de fábrica deve ser o seguro: cadastro fechado, endpoints de debug desligados, TLS obrigatório, nenhuma conta de exemplo. Quem precisar de algo mais permissivo habilita explicitamente e deixa rastro.
Minimize a superfície de ataque. Cada endpoint exposto, feature flag ativada, pacote instalado e porta aberta é mais uma coisa a defender. Remover um console de administração que ninguém usa é um controle mais forte do que protegê-lo bem.
Segregação de funções. A pessoa ou o pipeline que escreve uma mudança não deve ser o único capaz de aprová-la e implantá-la, e o serviço que emite créditos não deve ser também o que os aprova.
Não confie na obscuridade. Parta do princípio de que o atacante leu seu código-fonte e sua documentação. Segredos são chaves e tokens, não URLs e nomes de schema.
Onde cada controle deve ficar
Um erro de design recorrente é aplicar uma regra em uma camada que pode ser contornada. Posicione os controles perguntando o que poderia alcançar a operação se a camada de cima fosse totalmente ignorada.
| Preocupação | Onde fica | Por quê |
|---|---|---|
| Criptografia do transporte, rate limiting, regras de roteamento amplas | Edge / gateway | Barato de aplicar de forma uniforme; veja API management e API gateways |
| Autenticação do chamador | No edge para a identidade, verificada de novo em cada serviço | Um chamador interno também precisa provar quem é |
| Autorização ampla (roles, scopes) | Filtro ou interceptor do framework | Vale para todas as rotas por padrão |
| Autorização no nível do recurso ("este pedido é seu?") | Serviço de domínio ou de aplicação | Só essa camada conhece a regra de propriedade |
| Validação sintática de entrada | Fronteira, no servidor | Rejeitar entrada impossível cedo e com baixo custo |
| Validação de regras de negócio ("o saldo não pode ficar negativo") | Modelo de domínio | É um invariante do domínio, não um problema de entrada |
| Integridade referencial e unicidade | Constraints do banco de dados | A última linha de defesa que nenhum caminho de código consegue pular |
| Codificação da saída | Camada de visão / renderização | A codificação depende do contexto em que o dado vai parar |
Validação no cliente é um recurso de usabilidade e nada mais. Em um sistema distribuído, esta tabela se aplica por serviço: em uma arquitetura de microsserviços, o gateway autentica o usuário final e cada serviço verifica o token de forma independente e aplica sua própria autorização, porque uma posição na rede não é uma permissão. Esse é o núcleo prático do zero trust conforme descrito no NIST SP 800-207.
Para a implementação dessas camadas baseada em servlets (filter chains, verificações no nível de método e validação stateless de tokens), veja os fundamentos do Spring Security.
Segredos também são arquitetura
Um segredo em um repositório está comprometido a partir do momento em que é feito o push, e reescrever o histórico não desfaz o vazamento; a credencial precisa ser rotacionada. O objetivo de design é que nenhum segredo de longa duração jamais seja anotado por uma pessoa.
- Prefira identidade de workload a segredos compartilhados. Plataformas de nuvem conseguem emitir credenciais de curta duração para uma instância em execução com base na identidade dela, eliminando por completo a chave estática.
- Guarde o que sobrar em um gerenciador de segredos, injetado em tempo de execução como variável de ambiente ou arquivo montado, nunca embutido em uma imagem ou em um arquivo de configuração que vai junto com o build.
- Rotacione periodicamente e após qualquer suspeita, e projete a aplicação para carregar um novo valor sem precisar de rebuild.
- Separe os segredos por ambiente, para que uma chave de teste vazada não sirva para nada em produção.
- Procure segredos no pipeline e em hooks de pre-commit, e trate cada ocorrência como um incidente, não como um detalhe de code review.
- Criptografe dados sensíveis em repouso com chaves que a aplicação não detém diretamente, usando um serviço de gerenciamento de chaves, para que um backup de banco de dados roubado não seja um conjunto de dados roubado.
Segurança no pipeline de entrega
Fazer shift left significa deixar o pipeline garantir o que o design decidiu, para que os achados de segurança cheguem enquanto a mudança ainda está fresca na cabeça de quem a fez. Um pipeline de entrega prático executa, mais ou menos nesta ordem: varredura de segredos e verificação de dependências a cada commit; análise estática ajustada para uma taxa baixa de falsos positivos, para que os desenvolvedores continuem confiando nela; varredura do contêiner e da imagem base no build; verificações de políticas sobre a infraestrutura como código; uma varredura dinâmica contra um ambiente de teste implantado; e artefatos assinados com proveniência, para que se possa demonstrar que o que roda em produção é exatamente o que foi construído.
stages:
- build # unit tests, SAST, secret scan, SBOM generation
- package # container build, image scan, sign the artefact
- verify # deploy to ephemeral env, DAST, authorization test suite
- release # policy gate: no critical findings, provenance verified
Dois hábitos importam mais do que a escolha de ferramentas. Primeiro, faça o build falhar apenas com os achados que o time concordou serem inaceitáveis e acompanhe o resto com prazo: um pipeline que falha o tempo todo ensina as pessoas a contorná-lo. Segundo, escreva testes de regressão para cada defeito de segurança corrigido, principalmente os de autorização; são testes comuns que verificam se uma requisição retorna 403, e são a proteção mais barata contra a volta do mesmo bug.
Erros comuns
- Modelar só o caminho feliz. O diagrama mostra o navegador e a API e omite a ferramenta de administração, a exportação noturna e o acesso de leitura do time de suporte, que é justamente onde estão os privilégios interessantes.
- Tratar a rede interna como confiável. Assim que um serviço é comprometido, uma malha interna plana e sem autenticação entrega todo o resto.
- Autorizar com base em identificadores enviados pelo cliente. Um identificador de tenant no corpo da requisição é uma alegação, não um fato; derive-o do principal autenticado.
- Uma única conta de banco de dados compartilhada por todos os serviços. Isso torna o menor privilégio impossível e o raio de impacto máximo.
- Logar o payload. Tokens, números de cartão e dados pessoais acabam em um armazenamento de logs com controle de acesso muito mais fraco do que o do banco de onde vieram.
- Aprovar um threat model e nunca mais reabri-lo. O modelo é um artefato vivo; uma nova integração o invalida.
- Comprar um scanner em vez de tomar uma decisão. Ferramentas encontram classes de bugs conhecidas; não conseguem dizer que um serviço nunca deveria ter tido aquela permissão.
Principais conclusões
- A segurança é decidida pela arquitetura: ativos, fronteiras de confiança e raio de impacto determinam o que qualquer esforço de código posterior consegue alcançar.
- Um diagrama de fluxo de dados de uma página mais uma passada de STRIDE produz controles concretos, com dono, mais rápido do que qualquer template de documento.
- Menor privilégio, defesa em profundidade, mediação completa, falha segura, padrões seguros e uma superfície de ataque pequena são os princípios que continuam compensando o esforço.
- Coloque cada controle na camada que não pode ser contornada: regras amplas no edge, autorização no nível do recurso no domínio, invariantes no banco de dados.
- Segredos ficam em um gerenciador ou, melhor ainda, são substituídos por uma identidade de workload de curta duração; qualquer coisa commitada em um repositório precisa ser rotacionada.
- Faça o pipeline garantir o design (varreduras, gates de políticas e testes de regressão de autorização) e mantenha o threat model vivo à medida que o sistema muda.
Fontes e leituras complementares
Perguntas frequentes
- Em que momento do projeto o threat modeling deve acontecer?
- Assim que houver um esboço dos componentes e dos dados que trafegam entre eles, e de novo sempre que esse esboço mudar de forma significativa: uma nova integração, um novo armazenamento de dados, um novo tipo de usuário. Um primeiro modelo feito no quadro branco em uma hora vale mais do que um documento perfeito produzido depois que o código já foi escrito, porque na fase de design a correção costuma ser uma escolha diferente, e não uma reescrita.
- Defesa em profundidade não é só trabalho duplicado?
- Não, porque cada camada falha de um jeito diferente. Uma regra do gateway pode ser contornada por um chamador interno, uma verificação do framework pode ser pulada por um novo caminho de código, uma constraint do banco de dados não pode ser pulada de forma alguma. As camadas são escolhidas para que o ponto cego de um controle seja o ponto forte de outro. Desperdício é copiar a mesma verificação duas vezes na mesma camada; o que tem valor é o mesmo invariante garantido por mecanismos independentes.
- Como convencer um time a dedicar tempo de design à segurança?
- Coloque a questão em termos de custo. Uma verificação de autorização ausente encontrada em um diagrama custa uma conversa; encontrada em produção, custa um incidente, uma comunicação pública e um patch feito às pressas. Leve um abuse case por user story para o refinamento, mantenha o modelo no mesmo quadro da arquitetura e registre as decisões tomadas, inclusive os riscos aceitos de forma deliberada, para que ninguém volte a discuti-las depois.
- Zero trust substitui a segurança de rede?
- Ele substitui a premissa de que estar dentro da rede implica confiança. Toda requisição continua precisando ser autenticada, autorizada e, sempre que possível, criptografada, seja atravessando a internet ou uma sub-rede. A segmentação de rede continua valiosa como mais uma camada que limita até onde um invasor consegue se mover, mas deixou de ser a fronteira sobre a qual todo o design se apoia.