# ADR-021 — Rate limiting e anti-abuso como hardening day-zero

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-20) |
| **Data** | 2026-10-14 |
| **Plano de origem** | `wiki/plan/04-runtime-containers.md`, `wiki/plan/06-observabilidade-day-zero.md` |
| **Relacionadas** | ADR-005, ADR-020, ADR-019 |

## Contexto

O sistema é **single-tenant** e exposto à internet, com **formulários públicos** (ouvidoria,
contato), login do CMS e API. Está sujeito a **DDoS, força bruta, spam de formulário e
scraping**. O hardening precisa valer **desde o dia 0**, sem sacrificar **performance**,
**usabilidade** e **controle fino dos formulários**.

## Decisão

Hardening **em profundidade, em 3 camadas**, com estado em **Valkey** (nunca no banco no caminho quente):

1. **Borda/servidor (Apache WHM — ADR-005):** `mod_reqtimeout`, `mod_qos`/`mod_evasive`,
   `mod_security` (CRS curado), limites de conexão/requisição, tamanho de corpo, comprimento de
   URL/cabeçalho e timeouts. **`TrustProxies`** para IP real.
2. **Aplicação (Laravel nativo):** `RateLimiter` com store **Valkey** (contadores atômicos
   `INCR`+`EXPIRE`), via `throttle:` — **baseline global + por rota + por usuário/IP**.
3. **Formulários (controle fino):** **honeypot**, **time-trap** (tempo mínimo de preenchimento),
   janela deslizante **por formulário + IP + sessão**, **CSRF/idempotência nativos**. **Captcha
   só como último recurso** (custo de UX/privacidade) — preferir medidas nativas.

Regras transversais:

- **Identificação:** combinar **IP (via proxy confiável) + sessão + usuário + rota**; para
  formulários preferir **por sessão** (evita punir NAT/carrier-grade); para API, **por IP/chave**.
- **Resposta:** `429` **RFC 7807** + `Retry-After`; página acessível; **nunca** vaza interno;
  `X-Request-ID` (ADR-024) para correlação.
- **Performance:** estado em Valkey, checagens baratas, **zero query extra** no caminho quente;
  estáticos servidos pelo Apache; cache.
- **Degradação (fail strategy):** leitura/estático **fail-open**; **auth e formulários** mais
  conservadores; logar a condição de falha; fallback local.
- **Observabilidade:** log estruturado (`action=rate_limit`, motivo, chave) + métrica de `429`;
  alerta em pico.
- **LGPD:** anti-abuso **não** cria PII oculta; retenção curta desses logs; captcha (se usado)
  documentado (ADR-019).

## Justificativa (alternativas descartadas)

- **Só WAF** — descartada: não entende regra de negócio (formulário/limite por sessão).
- **Só app** — descartada: não protege contra DDoS volumétrico na borda.
- **Captcha primeiro** — descartada: piora UX/acessibilidade e levanta questão de privacidade.
- **Contadores no banco** — descartada: mata a performance no caminho quente.

## Consequências

**Positivas**
- Proteção baseline desde o dia 0 (DDoS, força bruta, spam) sem depender de CDN.

**Custos / negativas**
- Exige **tuning** (limites/rotas) e testes de carga; Valkey passa a ser caminho de limite.
- Risco de falso positivo — mitigado por janelas por sessão e monitoração.

## Exemplo mínimo (comparativo)

```php
// ❌ só throttle genérico por IP em tudo
Route::middleware('throttle:60,1')->group(...);

// ✅ camadas + limite por formulário/sessão + resposta amigável
RateLimiter::for('contato', fn ($r) => Limit::perMinute(3)->by($r->session()->getId()));
RateLimiter::for('api',     fn ($r) => Limit::perMinute(60)->by($r->ip()));
// honeypot + time-trap no FormRequest; 429 RFC 7807 com Retry-After
```

## Verificação

- Rajada de requisições → `429` com `Retry-After`, sem impactar usuário legítimo.
- Força bruta no login é estrangulada; formulário bloqueia spam sem punir sessão normal.
- Nenhum hit no banco para decidir limite (estado em Valkey); log/métrica de `429` visíveis.
- Apache aplica timeouts/limites de corpo; IP real correto atrás de proxy.

## Referências

- OWASP Automated Threats / API Security; Laravel `RateLimiter`/`throttle`; Apache `mod_reqtimeout`,
  `mod_qos`, `mod_security` (CRS); `wiki/plan/04`, `wiki/plan/06`.
