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
SecurityRulenomeada — nunca umifsolto no handler. - Micronaut:
micronaut-securitynativo comauthentication: bearer; papel por claim consumido por@Secured("ROLE_…"); filtro HTTP é@ServerFilter. - O
intercept-url-maplibera por prefixo uniforme (/api/**Bearer, estáticos, infra). View pública se libera com@Secured(SecurityRule.IS_ANONYMOUS)no método da rota, ou numaisPublicView()de rota exata; pattern greedy (/*,/**) é proibido, porque ointercept-url-map(ordem −100) roda antes das regras próprias (ordem 0) e deixa a rota irmã pública sem sinal. - O
@Secureddecide 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. REJECTEDmapeia 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_idvem 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), nuncaequals; (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 mesmolocalStorage, 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, comcodeuniforme 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 degh secret seta 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, porSecurityRulenomeada. Noapplication.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/applicationsdo Coolify devolve ogit_repositoryem 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 repoxadm-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_TOKENacha 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/**; oCENTRAL_INTEGRATOR_TOKENabre/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:
sendDefaultPiiligado é 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:
- Segredo no diff? (string de alta entropia, DSN com senha, PEM) → bloqueia e rotaciona.
- Endpoint novo tem
@Secured/regra, e o público é público de propósito? View pública:@Secured(IS_ANONYMOUS)na rota, nuncaintercept-url-mapgreedy. - Request novo tem
@Valid? Query tem parâmetro, não concatenação? - Migração destrutiva segue expand/contract, com
-- destrutivo-ok: contract; …no.sql? (banco) - Fixture nova está anonimizada (ou justificada no javadoc)?
- Dado sensível novo em
docs/? Só emprivado/. - 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).