Pular para conteúdo

Segurança — o piso

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15

Aplica-se a: todo perfil e toda stack.

O mínimo que todo app da casa cumpre. Não é threat modeling formal: é o piso que evita as falhas baratas de evitar. A receita Micronaut de cada regra está em Java/Micronaut.

Authn / authz

  • Fail-closed por padrão: a regra de segurança nega, e o público é exceção explícita. Bypass de desenvolvimento só em dev/test, por SecurityRule nomeada — nunca um if solto no handler.
  • Micronaut: micronaut-security nativo com authentication: bearer; papel por claim consumido por @Secured("ROLE_…"); filtro HTTP é @ServerFilter.
  • O intercept-url-map libera por prefixo uniforme (/api/** Bearer, estáticos, infra). View pública se libera com @Secured(SecurityRule.IS_ANONYMOUS) no método da rota, ou numa isPublicView() de rota exata; pattern greedy (/*, /**) é proibido, porque o intercept-url-map (ordem −100) roda antes das regras próprias (ordem 0) e deixa a rota irmã pública sem sinal.
  • O @Secured decide antes de tudo (SecuredAnnotationRule, ordem −200), e a primeira regra que decide encerra: @Secured(IS_ANONYMOUS) na classe abre todas as rotas dela, inclusive as que a regra de view fecharia. Anônimo vai no método; controller com rota protegida não leva anônimo na classe, e o teste com a auth ligada espera 401.
  • REJECTED mapeia o status sozinho: 401 sem autenticação, 403 autenticado sem permissão.
  • Token social prova quem é, não onde entra: com um projeto de identidade para N apps (0030), a autorização por app vem sempre do grant (0029).
  • Negativa de acesso carrega o que o usuário precisa para agir: o 403 distingue pendente de rejeitado e traz o contato do admin no corpo — o usuário sem JWT não consegue consultá-lo.
  • CORS: superfície /api/** de app com UI em outra origem nasce com CORS, com as origens em config (<APP>_CORS_EXTRA_ORIGINS); nunca * junto de credencial ou cookie.
  • Tenancy: o servidor decide. O cliente_id vem da claim do JWT validada no servidor, nunca de parâmetro ou header que o cliente controla; restrição no cliente Flutter é UX, não segurança.
  • Validador de segredo, token ou HMAC — três invariantes numa classe: (1) compara em tempo constante (MessageDigest.isEqual), nunca equals; (2) segredo esperado vazio ou em branco nega o acesso (isEqual("".getBytes(), entrada) com entrada vazia é true); (3) a config segue a regra de segredo ausente. Classe nova ou extraída para lib cita esta regra no plano.
  • Endpoint público tem rate limit (login: o filtro da xadm-seguranca). Ação administrativa ou sensível gera log INFO de auditoria (quem, o quê, path).
  • Sessão de app com usuário (0037): a revogação e a troca de papel chegam ao app em até o TTL do access token, de no máximo 60 min, e a sessão tem teto absoluto de no máximo 7 dias desde o login. O app renova pelo refresh do central, trocando o token vigente, nunca guardando a senha para re-logar; 401 ou 403 com código de revogação faz logout.
  • Flutter: JWT em flutter_secure_storage; senha nunca é persistida. No web o pacote ofusca e não defende de XSS: a chave AES fica no mesmo localStorage, e o script injetado lê chave e valor. O token roubado renova pelo refresh, e quem limita o estrago é a revogação e o teto da sessão; a armadilha do pacote no web está no Flutter. Auditoria de segurança é do backend — evento client-side é visibilidade.

Validação de input

  • Todo record de request valida com jakarta.validation (@Body @Valid + @NotBlank/@Pattern/ @Size); endpoint novo sem @Valid é lacuna de review. O erro sai no RFC 7807 da casa, com code uniforme entre borda e service (Java/Micronaut).
  • Query nunca concatenada: Micronaut Data ou prepared statement com parâmetro.
  • Upload: valide extensão e conteúdo antes de processar; processador de planilha valida por linha e não falha o lote.

Segredos

  • Client-grade ≠ segredo. DSN do GlitchTip, app key de analytics e URL pública embarcam por desenho e moram no docs/app.json (Central de Apps). Segredo (key S3 de escrita, token de serviço, chave privada) nunca entra em repo, .ia/, log ou fixture.
  • Onde cada segredo vive: segredo de CI é secret do repo no GitHub; segredo compartilhado (ex. DOCS_S3_*) é mantido igual nos repos por um laço de gh secret set a partir de uma fonte única; segredo de runtime é env Secret do recurso Coolify. As outras páginas linkam para cá.
  • Segredo ausente: property de segredo não declarada → feature desligada (@Requires(property=…, pattern=".+")); declarada e vazia → boot recusado, com a env nomeada; bypass só em dev/test, por SecurityRule nomeada. No application.yml, ${ENV_VAR:} ou um default de dev inócuo; chave PEM aceita conteúdo ou caminho (AUTH_PRIVATE_KEY > AUTH_PRIVATE_KEY_PATH).
  • Credencial de repositório git: recurso que o Coolify builda do git (stack compose, site de docs) autentica por deploy key só-leitura do próprio repo, com URL SSH sem credencial. Nunca usuário:senha nem token na URL — o GET /api/v1/applications do Coolify devolve o git_repository em claro para qualquer token da API. Senha pessoal nunca é credencial de máquina.
  • Registro Maven da casa: ler é anônimo (sem credentials{} no app); publicar usa o secret do repo xadm-commons (Bibliotecas da casa).
  • Env de auth e URL inter-app se nomeia pelo destino: <APP>_URL + <APP>_API_TOKEN = "como chamar o <APP>" — o app valida o próprio <SELF>_API_TOKEN, e cada chamador seta o do destino, com o mesmo valor. Um token por app chamado; grep <APP>_API_TOKEN acha tudo o que gira numa rotação. Nome pelo papel (API_BEARER_TOKEN) é proibido. Banco (DATASOURCES_*), sessão (AUTH_*) e observabilidade (SENTRY_DSN) seguem o nome do framework.
  • Exceção nominal — dois tokens no central-backend, um por escopo de rota (dono: Gustavo Madruga): o CENTRAL_API_TOKEN, que fica em todo integrador-server, abre só /api/integrador/**; o CENTRAL_INTEGRATOR_TOKEN abre /api/admin/**. Com um token só, cada integrador-server ganharia o CRUD admin do central. A rotação tem os dois alvos, listados no README do repo (decisão do central-backend).
  • Fórmula de derivação sem key separada é o próprio segredo: derive por HMAC com a key em env, e quem opera com o fonte na mão pede o token, não o calcula.
  • Teste gera a própria chave (task Gradle); chave de produção nunca aparece em teste.
  • Vazou → rotaciona. Apagar o arquivo não basta: o Git guarda.
  • Log não carrega senha nem token, nem em breadcrumb.
  • PII no reporte de erro: sendDefaultPii ligado é o padrão da casa, em toda stack — o GlitchTip é self-hosted e o dado não sai da infra. Numa lib, o default alcança todo adotante e consta no CHANGELOG do módulo (receita).

Checklist do revisor (PR)

Ao revisar qualquer PR, os 60 segundos de segurança:

  1. Segredo no diff? (string de alta entropia, DSN com senha, PEM) → bloqueia e rotaciona.
  2. Endpoint novo tem @Secured/regra, e o público é público de propósito? View pública: @Secured(IS_ANONYMOUS) na rota, nunca intercept-url-map greedy.
  3. Request novo tem @Valid? Query tem parâmetro, não concatenação?
  4. Migração destrutiva segue expand/contract, com -- destrutivo-ok: contract; … no .sql? (banco)
  5. Fixture nova está anonimizada (ou justificada no javadoc)?
  6. Dado sensível novo em docs/? Só em privado/.
  7. Classe de segredo ou auth (nova ou extraída para lib) cumpre os três invariantes do validador? Extração é diff semântico contra a origem, não textual (Bibliotecas da casa).