# UPDATE_INFRA — pipeline de infraestrutura (V7 × `wiki/backlog/concluido/_INFRA/`)

> **Dono:** Edge/Infra · **Status:** Em execução (2026-10-20) · **Data:** 2026-10-20
> **Fontes revisadas:** `wiki/backlog/concluido/_INFRA/README.md` + docs `01`–`10` (6.115 linhas) e
> `wiki/backlog/concluido/_INFRA/arquivos/` (109 cópias literais do V6, `manifest.sha256`).
> Nas seções abaixo, `_INFRA/` (curto) refere-se a esse diretório arquivado.
> **Alvo:** código deste repositório (V7), medido em 2026-10-20 no stack local
> (`concordia-web-1` em `127.0.0.1:8090`).
> **Relação com os planos:** complementa F5 (borda) e F6 (deploy/WHM); **não substitui**
> `wiki/specs/16-edge-apache-whm.md` nem `wiki/specs/17-up-prod.md` — todas as mudanças
> abaixo atualizam essas specs quando alteram o contrato.
>
> **Estado por fase:** I0–I2, I4, I5 e I8 **fechadas**; I3, I6 e I7 com código, gates e
> ensaios locais prontos, aguardando evidência externa (execução no WHM, deploy de homolog e
> endpoint S3 externo). JIT foi **medido** (`off` 5,8 ms × `tracing` 13,7 ms) e permanece `off`.
> `make check` verde 2× (**94 s** quente, ≤100 s) e `make check E2E=1` verde (**177 s**, ≤3 min).

---

## 0. Método

1. Cada afirmação de `_INFRA/` foi classificada em **superada por desenho** (a arquitetura do
   V7 eliminou a camada), **transferível** (o princípio continua válido e precisa de
   implementação equivalente) ou **proibida** (copiar reintroduz um defeito já resolvido).
2. Cada item transferível foi confrontado com o código **e com a resposta HTTP real**, não com
   a documentação do projeto. Onde a documentação e o runtime divergem, o runtime vence.
3. O pipeline é **estritamente sequencial**: `I0` instala os gates; cada fase seguinte só fecha
   com `make check` verde e a evidência do gate correspondente. Uma fase que exige host real
   (WHM) é marcada como **canário** e não pode ser declarada concluída em dev.
4. Nada aqui autoriza commit: a regra de ouro do repositório continua valendo (o humano commita).

### 0.1 Delta de stack que explica 80% do descarte

| Camada no V6 (`_INFRA`) | Camada no V7 | Consequência |
|---|---|---|
| Apache/EA4 por conta + `.htaccess` + `php.ini` de conta/sistema | Apache do WHM **só** `ProxyPass` → Caddy `:8080` → php-fpm 8.4 | `.htaccess`, `mod_headers`, `mod_expires`, `FilesMatch`, `Header edit`, `php.ini` por conta e APCu por `pecl` **não existem** |
| PHP 7.2 / Laravel 5.5 (`Kernel.php`, Mix, Intervention/Imagick) | PHP 8.4 / Laravel 12 (`bootstrap/app.php`, Vite 8, libvips) | Middlewares/FPC/minify do V6 não são portáveis; valores de OPcache continuam válidos |
| Compressão no vhost EA4 | Caddy `encode` + `.br/.gz` pré-gerados no build | `_INFRA` L0 (Optimize Website/mod_deflate) é **proibido** no V7 (dupla compressão/ETag) |
| CSP por nonce + FPC de origem | CSP estática no edge, sem nonce; FPC descartado (ADR-022 §6) | F-01/F-09/F-11/F-15 de `_INFRA/10` são **impossíveis** por construção |

---

## 1. Veredito

O `_INFRA/` descreve com precisão um ambiente que o V7 **abandonou**. Ele não é um runbook do V7
e não deve ser aplicado literalmente. O valor residual está em quatro pontos:

1. **Valores de runtime** (OPcache/realpath) que o V7 ainda não calibrou.
2. **Postura de borda** (HSTS, XFO, CSP para os dois terceiros permitidos, falha segura do proxy).
3. **Operação** (offsite real, retenção de WAL, permissões, evidência de deploy).
4. **Gates mecânicos** que o V7 ainda não tem (validação do Caddyfile no `make check`,
   verificação de compressão/304/HSTS no go-live).

O que **não** se aplica: `.htaccess`, `php.ini` de conta, APCu por `pecl`, FPC, `HtmlMinify`,
`PictureTransform`, `PageCache`, `CdnPurgeService`, CSP por nonce, `X-Hardening-Version`,
`db:seed` no deploy, reset de OPcache por CLI. Ver §5.

---

## 2. Matriz `_INFRA/` → V7

| Doc | Tema | Equivalente no V7 (evidência) | Veredito |
|---|---|---|---|
| `01` | WHM/EA4: gzip, OPcache, APCu, PHP Selector | `docker/php-fpm/php.ini` + `entrypoint.sh`; Caddy comprime; `composer` sem `--apcu` | **Superado**, exceto 3 valores de OPcache (§G-11) |
| `02` | `public/.htaccess` (H1–H9, L0.2–L0.4) | `docker/web/Caddyfile` (headers/cache/`@forbidden`) + `app.conf` (`AllowOverride None`) | **Superado**; regras H8/H9/`sw.js`/`Vary` viram gates (§I0) |
| `03` | `php.ini` de conta × sistema, escopos `PHP_INI_*` | um único `php.ini` montado + perfil gerado por `APP_ENV` | **Superado**; conceito de “SYSTEM ≠ conta” vira “imagem ≠ app” |
| `04` | Cache/perf L0–L6 (FPC, layout cache, índices, minify, WebP) | `PublicResponseCache`/`StripPublicCookies`, ETag SHA-256, `precompressed`, Media AVIF/WebP | **Equivalente ou superior**; FPC/minify rejeitados por ADR-022 |
| `05` | Headers, CSP, cookies, CSRF, HTTPS, SRI | Caddy headers/CSP, `config/session.php`, `@csrf` + páginas `private` | **Parcial**: HSTS/XFO/CSP dos terceiros faltam (§G-02/03/10) |
| `06` | Deploy, OPcache reset, permissões, rollback | `up_prod.sh` (lock, backup 5.9, migrate, optimize, USR2, rollback) | **Equivalente/superior**; faltam permissões e evidência (§G-06/19) |
| `07` | Inventário/rastreabilidade das 109 cópias | — | **Referência histórica**; único artefato reutilizável: `codigo/lighthouse/budget.json` |
| `08` | Middlewares/controllers/providers do V6 | `src/core/Internal/Http/Middleware/*`, `services/Seo`, Media | **Equivalente**; os corner cases continuam válidos (§2.3) |
| `09` | Borda Multi-CDN (não contratada) | `s-maxage`, `stale-while-revalidate`, `Vary` | **App-side pronto**; CDN exige decisão nova (ADR) |
| `10` | 17 achados F-01..F-17 | 14 impossíveis por arquitetura; F-02/F-09/F-13 têm análogos evitáveis | **Triagem concluída** (§2.2) |

### 2.1 O que o V7 já resolveu — e não pode regredir

| Invariante | Como o V7 cumpre hoje | Como é verificado |
|---|---|---|
| Compressão server-side, uma única vez | Caddy `encode zstd gzip` + `precompressed br gzip`; nenhum `mod_deflate` no proxy | e2e `11-performance`, `make check` |
| Assets `immutable` **só** com hash | `@viteAssets path /build/assets/*` | e2e `14` |
| `Vary` sem `User-Agent` | `Vary: Accept-Encoding[, Cookie]` | e2e `14` |
| ETag estável sob compressão | Caddy anexa `-gzip`/`-br`; Symfony normaliza no `304` | e2e `14` (regex `^"<64 hex>(-gzip)?\"$`) |
| CSRF fora de HTML cacheável | página com `<form method=post>` vira `private, no-store` e perde o ETag | `ResponseCacheTest` |
| `Set-Cookie` fora do cache público | `StripPublicCookies` remove após `StartSession` | `ResponseCacheTest`, e2e `14` |
| OPcache `validate_timestamps=0` + reload | `kill -USR2 1` na fase 8 do `up_prod.sh` | `DeployPipelineTest`, deploy 2× |
| Zero CDN | CSP `'self'`, `vendor-check` com allowlist | e2e `14`, `03-no-js` |
| CSP sem nonce (cacheável) | header estático no edge | e2e `14` |
| `.well-known`/`security.txt` com ETag | `SeoRootFilesController` | `SeoSitemapTest` |

### 2.2 Triagem dos achados F-01..F-17

| Achado | V7 | Observação |
|---|---|---|
| F-01 CSP nonce × ETag/FPC | **Impossível** | CSP é estática no edge; sem nonce/FPC |
| F-02 `immutable` amplo em uploads | **Evitado**, mas frágil | `@viteAssets` limita a `/build/assets/*`; qualquer ampliação para `/storage/*` reintroduz o bug |
| F-03 `SameSite` duplicado | **Impossível** | `SameSite` vem de `config/session.php` + `php.ini` |
| F-04 `composer.lock` ausente | **Resolvido** | lock versionado; deploy com `--classmap-authoritative` |
| F-05 APCu ambíguo | **Resolvido** | `pecl install apcu` na imagem; `ext-apcu` é `require` |
| F-06 reset de OPcache | **Resolvido** | USR2 no container |
| F-07 `.htaccess` não versionado | **Impossível** | artefato inerte |
| F-08 seed/`DB_HOST` no deploy | **Impossível** | ADR-041; sem `DB_HOST` |
| F-09 log de CSP sem teto | **Reabre** se §I2 adotar relatório | tratar teto/rotação antes |
| F-10/F-11/F-14/F-15 | **Impossível** | sem `CacheControl`/`PageCache`/`HtmlMinify` |
| F-12 sitemap estático | **Impossível** | `public/` sem `sitemap.xml`/`robots.txt` |
| F-13 headers em não-HTML | **Resolvido** | `PublicResponseCache` distingue HTML de asset/arquivo |
| F-16 blocklist incompleta | **Parcial** | `@forbidden` cobre dotfiles/`*.ini`/`*.log`; falha do proxy coberta em §G-04 |
| F-17 CI sem `composer prod` | **Resolvido** | CI chama `up_prod.sh` |

### 2.3 Corner cases de `_INFRA` que continuam válidos

1. **Soft-404** (`302 → /error/404` respondendo 200) — o V7 já responde 404 real; manter o teste.
2. **`route:cache` + Closure** — gate já existe; não regredir.
3. **`env()` em runtime após `config:cache`** — gate `optimize-check`.
4. **ETag forte + compressão** — funciona hoje porque o Caddy normaliza; **não** ligar
   `mod_deflate` no Apache (mangleria o ETag de novo).
5. **`Set-Cookie` sobrevive a `headers->remove()`** — `StripPublicCookies` remove o header
   inteiro; não trocar por `removeCookie()` isolado.
6. **`storage:link` mascara falha com `|| true`** — no V7 o `--force` não mascara, mas o
   symlink é um artefato sem uso (§G-16).
7. **`migrate:rollback --step=1`** com índice grande bloqueia — `down()` nunca apaga (ADR-007).
8. **Paginação fora da chave de cache** — sem FPC hoje; vira requisito se o FPC voltar.

---

## 3. Achados

> Severidade: **P0** = quebra função/segurança declarada; **P1** = risco operacional ou
> requisito de ADR não cumprido; **P2** = higiene/observabilidade. Linhas de arquivo são
> aproximadas (o conteúdo é a âncora).

### 3.1 P0

#### G-01 — `security.txt` fica inalcançável em produção · P0
- **Evidência:** `docker/web/app.conf:15` exclui **todo** `/.well-known` do proxy
  (`ProxyPass /.well-known !`). Não existe `public/.well-known/` (nem no repo, nem no
  docroot). A rota dinâmica existe (`src/services/Seo/Routes/portal.php:12` →
  `SeoRootFilesController::security`). O `tools/go-live-check.sh` exige
  `/.well-known/security.txt` com 200.
- **Impacto:** em produção o pedido vai ao filesystem do Apache e responde 404; o go-live
  falha e o `security.txt` (RFC 9116, ADR-027) não é publicado. Em dev o bug não aparece
  porque não há Apache na frente.
- **Ação:** excluir **somente** os desafios de validação de certificado:
  `ProxyPass /.well-known/acme-challenge !` e `ProxyPass /.well-known/pki-validation !`
  (antes de `ProxyPass /`; o cPanel usa um ou outro conforme o provedor DCV). O restante de
  `/.well-known` (incluindo `security.txt`) passa a ser proxiado.
- **Gate:** `tools/security-check.sh` exige o caminho estreito e proíbe o amplo; go-live
  valida 200 + `Content-Type: text/plain` no host real.

#### G-02 — a CSP bloqueia os dois únicos terceiros permitidos (VLibras e GA) · P0
- **Evidência:** `docker/web/Caddyfile:63` publica `script-src 'self'` (sem hosts externos).
  `src/layout/Assets/js/a11y.js:98` injeta
  `https://www.vlibras.gov.br/app/vlibras-plugin.js` e `:100` chama o widget; o portal CSP
  não lista `vlibras.gov.br` em `script-src`/`frame-src`/`connect-src`/`img-src`.
  `src/layout/Assets/js/analytics.js:24` injeta
  `https://www.googletagmanager.com/gtag/js?id=…`; o portal CSP não lista
  `googletagmanager.com`/`google-analytics.com`.
- **Impacto:** o botão **VLibras** (ADR-025/eMAG) e o **Google Analytics opt-in**
  (ADR-002/027) falham silenciosamente no browser (violação de CSP), embora o código exista.
  Nenhum e2e clica no VLibras nem configura o GA.
- **Ação:** permitir **explicitamente** os dois hosts no `@portal` (e no `@cms` apenas o
  necessário para o editor, se houver): `script-src` com `https://www.vlibras.gov.br` e
  `https://www.googletagmanager.com`; `connect-src`/`img-src` com
  `https://*.google-analytics.com`; `frame-src`/`style-src`/`img-src` com
  `https://www.vlibras.gov.br` **se** o widget os exigir (provar por e2e, não por suposição).
  A permissão não gera requisição: o GA continua desligado sem ID, e o VLibras só carrega
  no clique.
- **Gate:** e2e novo (ou extensão de `05-preferencias-a11y`) que clica em VLibras com
  `page.route` interceptando `vlibras.gov.br` e falha em qualquer `securitypolicyviolation`;
  teste da CSP do CMS; e2e `14` continua proibindo `cdn.jsdelivr.net`/`fonts.*` (não
  confundir allowlist de dois hosts com “CDN”).

#### G-03 — HSTS ausente, com dono contraditório · P0
- **Evidência:** o header não aparece em `curl -I http://localhost:8080/`;
  `docker/web/Caddyfile:50-51` mantém HSTS **comentado** e diz “habilitar via snippet no
  deploy”; não existe snippet no `up_prod.sh`. Ao mesmo tempo, ADR-005 §“Proxy chain” e a
  spec 16 §4 dizem que HSTS/HTTP2/3 são do **Apache** (termina TLS), não do Caddy.
- **Impacto:** requisito do ADR-020 (`Strict-Transport-Security` com preload) não cumprido;
  dois “donos” documentados, nenhum implementado.
- **Ação:** definir o Apache do WHM como dono (coerente com ADR-005/spec 16): no include
  **ssl**, `Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"`
  (sem `preload` na primeira janela); remover o comentário do Caddy para não sugerir um
  segundo dono. Promover a `preload` só depois de confirmar que **todos** os subdomínios
  (`www`, `cms`, `api`, `storage`) respondem HTTPS.
- **Gate:** `tools/security-check.sh` exige a diretiva no arquivo ssl; `go-live-check.sh`
  exige o header quando a URL é `https://` (flag `--require-hsts`, ligada por padrão em
  produção); canário antes de liberar.

### 3.2 P1

#### G-04 — falha do proxy expõe a raiz do repositório · P1
- **Evidência:** `docker/web/app.conf:21-24` aplica `AllowOverride None` e `Require all
  granted` em `/home/<acct>/public_html` (a raiz do clone, não `public/`). O
  `public/.htaccess` do Laravel fica **inerte**; se o include do `ProxyPass` não carregar
  (módulo ausente, rebuild do `httpd.conf`, erro de sintaxe), o Apache serve `.env`,
  `storage/`, `vendor/` e o código.
- **Impacto:** exposição total de segredos em um único erro de configuração — o pior modo de
  falha do layout ADR-005.
- **Ação:** negar por padrão e liberar só os desafios de validação:
  `<Directory /home/<acct>/public_html> Options -Indexes; AllowOverride None; Require all
  denied </Directory>` + `<Directory …/.well-known/acme-challenge> Require all granted
  </Directory>` e o mesmo para `/.well-known/pki-validation`. O `ProxyPass` continua vencendo
  para o app (é avaliado antes do filesystem); a negação só vale para o que **não** for
  proxiado.
- **Gate:** probe no host real `GET /.env`, `/composer.json`, `/artisan` → 403/404 (nunca
  200); estático no `security-check`; go-live com `--require-deny`.

#### G-05 — `X-Forwarded-Proto` não é definido no escopo SSL · P1
- **Evidência:** `docker/web/app.conf:56-58` — a diretiva
  `RequestHeader set X-Forwarded-Proto "https"` está **comentada** e o arquivo é copiado para
  os dois escopos. Caddy encaminha `HTTP_X_FORWARDED_PROTO` ao FPM; sem ele,
  `$request->isSecure()` é falso em produção (o `URL::forceScheme` mascara a geração de URLs,
  mas não o estado da request).
- **Impacto:** logs/auditoria com esquema errado, qualquer regra futura que dependa de
  `isSecure()` (signed URLs, cookies condicionais) falha; rate limiting por IP não é afetado.
- **Ação:** criar `docker/web/app-ssl.conf` (ou descomentar a seção no gêmeo ssl) com
  `RequestHeader set X-Forwarded-Proto "https"`; atualizar spec 16/runbook para deixar claro
  que o arquivo é **diferente** por escopo (hoje o texto manda copiar o mesmo arquivo).
- **Gate:** estático no `security-check`; teste de feature enviando
  `X-Forwarded-Proto: https` e afirmando `$request->isSecure()` (prova o lado Laravel);
  canário confirma o lado Apache.

#### G-06 — `.env` legível por todos e deploy não corrige · P1
- **Evidência:** `stat -c '%a' .env` → `644`; `up_prod.sh` não executa `chmod`/`umask`.
- **Impacto:** em host compartilhado (cPanel), outro usuário do sistema lê `APP_KEY`,
  `DB_PASSWORD`, `REDIS_PASSWORD` e chaves S3.
- **Ação:** `umask 077` no início do `up_prod.sh` (ou `chmod 600 .env` após validar a
  existência); documentar que o arquivo nunca entra no docroot (já garantido).
- **Gate:** `security-check` (host) exige `.env` 600 quando existir; `DeployPipelineTest`
  cobra o `chmod`/`umask` no script.

#### G-07 — `pg_stat_statements` pré-carregado, mas não instalado · P1
- **Evidência:** `docker/postgres/postgresql.conf:23-25` define
  `shared_preload_libraries = 'pg_stat_statements'`; `SELECT extname FROM pg_extension`
  retorna apenas `plpgsql`.
- **Impacto:** toda a observabilidade de query planejada (ADR-024, `_INFRA` L3.4) não
  existe; `log_min_duration_statement=500ms` é a única evidência de lentidão.
- **Ação:** migration idempotente `CREATE EXTENSION IF NOT EXISTS pg_stat_statements`
  (referência via migration, ADR-041), em `src/core/Internal/System/Database/Migrations/` com
  `loadMigrationsFrom` no `CoreServiceProvider`; `down()` no-op (não remove extensão).
  Registrar no runbook a consulta de top-N (`pg_stat_statements` por `total_exec_time`).
- **Gate:** teste que consulta `pg_extension` após `migrate`; `tools/migration-check` segue
  verde.

#### G-08 — Valkey sem `maxmemory` torna a política de evicção inerte · P1
- **Evidência:** `docker/valkey/valkey.conf:15` define `maxmemory-policy volatile-lru` sem
  `maxmemory`; runtime: `CONFIG GET maxmemory` → `0`.
- **Impacto:** sem teto, o Valkey cresce até o OOM do host; a política só evita evictar
  chaves sem TTL (sessões/filas), mas nunca é acionada.
- **Ação:** `maxmemory 256mb` (ajustar ao host; documentar a conta: sessões + cache +
  fila) e `maxmemory-samples 5`; monitorar `used_memory`/`evicted_keys`.
- **Gate:** `CONFIG GET maxmemory` no teste de runtime; runbook com o cálculo.

#### G-09 — API pública de dados abertos sem CORS · P1
- **Evidência:** `src/modules/Portal/DadosAbertos/Routes/api.php` usa só
  `throttle:open-data`; `curl -H 'Origin: …'` não devolve `Access-Control-Allow-Origin`.
  Existe o middleware `security.open-cors` (`OpenCors`), hoje usado apenas na rota demo de
  Notícias.
- **Impacto:** ADR-018 promete API pública; consumidores browser (dashboards, portais de
  transparência de terceiros) são bloqueados. `curl`/server-side funcionam, o que esconde o
  problema no go-live.
- **Ação:** aplicar `security.open-cors` nas rotas `v1/*` da API (somente `GET/HEAD`;
  `Access-Control-Allow-Origin: *` é adequado a dado aberto sem PII) e manter o
  `throttle:open-data`. Opcional: `Cross-Origin-Resource-Policy: cross-origin` no matcher
  `@api` do Caddy (CORP só afeta requisições `no-cors`; com ACAO o `fetch` CORS já passa).
- **Gate:** teste de feature `DadosAbertosTest` com `Origin` e asserção de ACAO; e2e
  `12-dados-abertos` (browser) consumindo a API.

#### G-10 — `X-Frame-Options` ausente (e o embed não pode recebê-lo) · P1
- **Evidência:** `curl -I` não traz o header; ADR-020 lista `x-frame` entre os headers
  estáticos. Existe `fact_01hf6gca`: o XFO já bloqueou o embed de notícia no passado; a
  solução foi mover `frame-ancestors` para o edge.
- **Impacto:** navegadores antigos sem `frame-ancestors` ficam sem defesa de clickjacking;
  a CSP cobre os modernos.
- **Ação:** `header @portal X-Frame-Options SAMEORIGIN` e `header @cms X-Frame-Options
  SAMEORIGIN`, **nunca** em `@embed`; como `@portal` inclui `/noticias/embed/*`, adicionar
  `header @embed -X-Frame-Options` (a remoção precisa vir depois, ou o matcher `@portal`
  precisa excluir o path). Provar o embed no e2e `14` já existente.
- **Gate:** e2e `14` (portal/CMS com XFO, embed sem); `security-check` estático.

#### G-11 — OPcache: spec e implementação divergem; valores não calibrados · P1
- **Evidência:** `docker/php-fpm/php.ini:17-27` → `memory_consumption=192`,
  `interned_strings_buffer=16`, `max_accelerated_files=20000`, `jit=off`;
  `realpath_cache_ttl` no runtime é **120** (default, não configurado). A spec 17 §62
  promete `opcache.jit=tracing` e `realpath_cache_size/ttl`. O `_INFRA/03` recomenda
  256/32/10000/600.
- **Impacto:** a spec mente; o `realpath_cache_ttl` curto custa `stat` em cada include do
  autoloader; JIT ligado sem medição pode piorar (ADR-008 manda medir).
- **Ação:** (a) `realpath_cache_size=4096K`, `realpath_cache_ttl=600` no `php.ini`;
  (b) medir `opcache_get_status(false)` (used/wasted/interned) e só então subir memória/strings;
  (c) decidir JIT por benchmark em homolog — se não houver ganho, **corrigir a spec 17** para
  `jit=off` (não deixar a promessa no papel).
- **Gate:** `php -i` no teste de runtime; evidência numérica no `close` da task; spec 17
  atualizada.

#### G-12 — Caddyfile não é validado no `make check`; edge efetivo pode divergir · P1
- **Evidência:** `tools/check.sh` não executa `caddy` (grep vazio); a validação só ocorre na
  fase 8 do `up_prod.sh` (`caddy validate`), depois do código publicado. Existe
  `risk_01qke8zj`: o container pode servir um Caddyfile antigo e o gate de texto passa.
- **Impacto:** um erro de sintaxe derruba o deploy no fim (rollback desnecessário); headers
  antigos passam no `make check` e só o e2e pega.
- **Ação:** passo `edge-config` no `check.sh`:
  `dc exec -T web caddy validate --config /etc/caddy/Caddyfile` (medido: **0,05 s**).
  `caddy fmt --diff` **não** entra como gate enquanto o Caddyfile não for normalizado uma vez
  (`caddy fmt --overwrite`); depois disso, pode virar gate barato.
- **Gate:** `CheckPipelineTest` cobra o passo; `gates-selftest` com fixture inválida.

### 3.3 P2

#### G-13 — CSP enforce sem canal de relatório · P2
- **Sintoma:** a política é enforce e não há `report-uri`/`report-to`; violações (como G-02)
  só aparecem no console de quem abre o DevTools.
- **Ação (decisão em I2):** adotar `report-uri /csp-report` no `@portal`/`@cms` com endpoint
  same-origin, `throttle`, corpo limitado (≤16 KB), log JSON com rotação e sem PII; **ou**
  aceitar explicitamente a ausência e registrar a decisão. Se adotar, herdar a mitigação de
  F-09 (teto de bytes/rotação) — nunca `file_put_contents` sem limite como no V6.
- **Gate:** teste do controller (JSON inválido/grande → 204/413) + e2e de violação forçada.

#### G-14 — WAL arquivado dentro do `PGDATA`, sem retenção · P2
- **Evidência:** `docker/postgres/postgresql.conf` (bloco PITR):
  `archive_command` copia para `${PGDATA}/wal_archive`, sem poda.
- **Impacto:** crescimento contínuo do disco até encher o volume; PITR existe, mas o custo
  operacional é invisível.
- **Ação:** em produção, enviar o WAL para o S3 (ADR-030; `wal-g`/`archive_command` externo)
  ou definir retenção/poda por idade; monitorar `du -sh` do arquivo; ensaiar restore.
- **Gate:** `pitr-drill.sh` continua verde; runbook com o limite e o alerta.

#### G-15 — “offsite” é o RustFS do mesmo host · P2
- **Evidência:** `BACKUP_OFFSITE=1` envia para o disco `s3`; em dev/prod o `s3` é o serviço
  `storage` (RustFS) do mesmo Compose (`docker-compose.yml`), no mesmo host.
- **Impacto:** protege contra corrupção do banco, **não** contra perda do host — o DR
  prometido em F6 não se sustenta com o host perdido.
- **Ação:** documentar o limite; para produção, apontar `AWS_*` para um S3 externo (a troca é
  só `.env`, ADR-030) ou copiar o dump para fora do host; ensaiar o restore a partir dessa
  cópia.
- **Gate:** `db-restore-drill.sh` a partir da cópia externa; checklist no runbook.

#### G-16 — `storage:link` contradiz o S3-only e cria superfície de bypass · P2
- **Evidência:** `up_prod.sh:369` e `composer.json` (`prod`) executam
  `storage:link --force`; `public/storage` existe (symlink). Nenhum código usa
  `asset('storage/…')` nem o disco `public` (grep vazio); arquivos de domínio vivem no `s3`.
- **Impacto:** se algum arquivo local aparecer em `storage/app/public`, passa a ser servido
  pelo `file_server` **sem** os headers do `MediaStreamController` (cache/auth/alt), furando
  ADR-026/030.
- **Ação:** remover `storage:link` do deploy e do script `prod`; apagar o symlink; manter o
  disco `public` apenas se houver uso real (hoje não há).
- **Gate:** teste/estrutural que proíbe `asset('storage/` e o symlink; `StorageBoundaryTest`
  permanece verde.

#### G-17 — headers menores incompletos (`Permissions-Policy`, `X-XSS-Protection`) · P2
- **Evidência:** `docker/web/Caddyfile:47` lista `geolocation`, `camera`, `microphone`;
  `_INFRA/02` lista também `payment`, `usb`; não há `X-XSS-Protection: 0` (recomendação atual
  para desligar o filtro legado — a CSP é o controle real).
- **Ação:** acrescentar `payment=(), usb=()` e `X-XSS-Protection "0"` (sem impacto funcional).
- **Gate:** estático/e2e.

#### G-18 — `Vary: Cookie` e ausência de `stale-if-error` (decisão documentada) · P2
- **Evidência:** `StripPublicCookies.php:35` emite `Vary: Accept-Encoding, Cookie`;
  `:34` não inclui `stale-if-error`.
- **Impacto:** `Vary: Cookie` é **correto** (preferências de a11y são renderizadas no SSR,
  ADR-016) mas fragmenta qualquer cache compartilhado por variante de cookie; sem `stale-if-error`,
  uma falha de origem não é mascarada na borda.
- **Ação:** **não mudar agora** (correção > hit ratio); documentar no ADR-022/spec 16 que um
  CDN futuro precisa de chave normalizada (ignorar cookies de tracking, manter o cookie de
  a11y) e que `stale-if-error` só entra quando houver borda contratada. Se o CDN for
  contratado, abrir ADR novo.
- **Gate:** nenhum (decisão registrada).

#### G-19 — evidência de deploy incompleta (`migrate:status`, reset de OPcache) · P2
- **Evidência:** o `up_prod.sh` roda `migrate --force --isolated` sem `migrate:status`
  prévio; o USR2 não deixa prova no log além da linha de fase.
- **Ação:** `migrate:status` antes do `migrate` (falha se houver pendência inesperada? não —
  apenas registra a lista no log/JSON); registrar `pid` do master do FPM antes/depois do
  USR2 para provar o reload; `about --only=environment,cache,drivers` já existe.
- **Gate:** `DeployPipelineTest` cobra a ordem e a evidência; `up_prod.sh --dry-run` mostra os
  comandos.

#### G-20 — go-live não verifica compressão, 304, HSTS nem caminhos proibidos · P2
- **Evidência:** `tools/go-live-check.sh` cobre 200/Content-Type/CSP/nosniff/Referrer/cache;
  não cobre `Content-Encoding`, `If-None-Match`, `immutable` de asset, `/.env`, `TRACE`,
  HSTS (G-03) e cookies.
- **Ação:** estender o script (sem criar um segundo caminho) com esses checks e um modo
  `--require-hsts`/`--require-deny` para produção.
- **Gate:** o próprio script (falha antes/depois) + `gates-selftest`.

---

## 4. Pipeline sequencial

```mermaid
flowchart LR
  I0[I0 · gates e contrato] --> I1[I1 · headers do edge]
  I1 --> I2[I2 · CSP operável]
  I2 --> I3[I3 · Apache/WHM canário]
  I3 --> I4[I4 · runtime PHP/DB/Valkey]
  I4 --> I5[I5 · API dados abertos CORS]
  I5 --> I6[I6 · higiene de deploy]
  I6 --> I7[I7 · DR e retenção]
  I7 --> I8[I8 · decisões registradas]
```

Regras do pipeline:

- **Nenhuma fase começa sem a anterior verde** (`make check` 2×; E2E=1 ao fechar).
- **Vermelho→verde:** o gate de cada item entra no mesmo commit da correção; um gate que
  nunca falhou não prova nada.
- **Canário:** I3, I4 e I7 tocam o host real; executar em homolog/canário antes de produção e
  manter o rollback descrito em cada fase.
- **Nada de código novo de produto:** as mudanças são de configuração do edge, runtime,
  gates e documentação. Endpoint novo só em I2 (relatório CSP), se aprovado.

### I0 — Congelar o contrato (gates que já passam)

**Objetivo:** instalar o **harness** de gates e cobrir o que já é verdade hoje. Os gates dos
achados entram **na fase que corrige o achado** (vermelho→verde); I0 não pode deixar
`make check` vermelho.

**Passos**
1. `tools/security-check.sh` (aceita `ROOT` para fixtures): manter os gates existentes e
   adicionar o **modo efetivo** (`SECURITY_EDGE_URL`) com as asserções que já passam hoje —
   CSP sem `unsafe-inline`, Trusted Types no CMS e assets `immutable`. As asserções de
   `X-Frame-Options` e HSTS entram nas fases I1 e I3, junto com a correção.
2. `tools/go-live-check.sh`: adicionar o que já passa em dev — compressão
   (`Content-Encoding` com `Accept-Encoding: gzip`), `304` via `If-None-Match` (GET, incluindo
   sufixo `-gzip`), `immutable` em asset descoberto no HTML, `Set-Cookie` ausente no HTML
   público, `/.env`/`/composer.json`/`/artisan` → 403/404, `TRACE` → 405/403. O modo
   `--require-hsts`/`--require-deny` só vira default em I3.
3. `tools/check.sh`: passo `edge-config` (**só** `caddy validate`; 0,05 s medido) + prep gated
   por staleness que reinicia `web` quando o Caddyfile é mais novo que o `StartedAt` do
   container. `caddy fmt` fica de fora até uma normalização única do arquivo.
4. `tools/gates-selftest.sh`: fixtures inválidas para cada novo gate (uma por regra),
   provando que o gate **falha** quando a regra some.
5. `CheckPipelineTest`: cobrar o passo `edge-config` e o restart gated.
6. `DeployPipelineTest`: fixar o baseline atual (lock, `trap` de rollback, ordem
   backup→migrate); as asserções novas de `umask`/`migrate:status` entram em I6.

**Aceite:** `make check` verde; para cada gate adicionado, remover a regra do fixture e ver o
check falhar (evidência no `close`); `make check E2E=1` verde.

**Rollback:** reverter os gates (são aditivos), sem tocar em runtime.

**kd:** issue `I0` + 6 tasks. Cada fase seguinte acrescenta o gate do seu achado junto com a
correção.

### I1 — Contrato HTTP do edge (Caddy)

**Objetivo:** completar os headers estáticos do ADR-020 sem quebrar cache/embed.

**Passos**
1. `docker/web/Caddyfile`: `X-Frame-Options: SAMEORIGIN` em `@portal`/`@cms`; remoção
   explícita em `@embed`; `Permissions-Policy` com `payment`/`usb`; `X-XSS-Protection: 0`.
2. Remover o comentário de HSTS do Caddy (dono é o Apache — G-03); a implementação real
   entra em I3.
3. `@api`: `Cross-Origin-Resource-Policy: cross-origin` (se I5 exigir `no-cors`; caso
   contrário, manter e documentar).
4. Atualizar `wiki/specs/16-edge-apache-whm.md` (dono de cada header, matcher e ordem).

**Aceite**
- `curl -sI /` → `X-Frame-Options: SAMEORIGIN`, `Permissions-Policy` completa;
  `curl -sI /noticias/embed/...` → **sem** XFO e com `frame-ancestors *`;
  `curl -sI` no CMS → XFO + Trusted Types.
- e2e `14` estendido (XFO/embed/Permissions); `make check E2E=1` verde 2×.

**Rollback:** reverter o Caddyfile e `restart web` (o passo gated de I0 mostra o drift).

**kd:** issue `I1` + 3 tasks (portal/CMS, embed, spec).

### I2 — CSP operável (VLibras, GA e relatório)

**Objetivo:** fazer a CSP permitir **apenas** os dois terceiros da allowlist (ADR-002) e
tornar violações observáveis.

**Passos**
1. `docker/web/Caddyfile` `@portal`:
   - `script-src 'self' https://www.vlibras.gov.br https://www.googletagmanager.com`;
   - `connect-src 'self' https://*.google-analytics.com https://www.googletagmanager.com
     https://www.vlibras.gov.br`;
   - `img-src 'self' data: https://*.google-analytics.com https://www.vlibras.gov.br`;
   - `frame-src 'self' https://www.vlibras.gov.br` (provar; se o widget não usa iframe,
     remover);
   - `style-src` com `https://www.vlibras.gov.br` **somente** se o widget injetar `<link>`
     externo (provar).
   Repetir no `@cms` apenas o que o editor realmente usa (não ampliar sem prova).
2. **Provar** o VLibras: e2e com `page.route('https://www.vlibras.gov.br/**', fulfill)` +
   clique no botão + `securitypolicyviolation` capturado = vazio; o widget precisa montar
   (assertar o elemento criado).
3. **Provar** o GA: teste com `gtag-id` configurado e `page.route` no
   `googletagmanager.com`; assertar que a requisição **seria** permitida (nenhuma violação) e
   que sem ID não há requisição.
4. Decisão de relatório (G-13): adotar `report-uri /csp-report` + endpoint same-origin
   (`throttle`, corpo ≤16 KB, log JSON com rotação) **ou** registrar a recusa no ADR-020.
   Se adotar, o endpoint é `POST` isento de CSRF, responde 204 e nunca é cacheado.

**Aceite**
- e2e novo (VLibras e GA) verde com CSP enforce; e2e `03-no-js` continua sem requisições
  externas na navegação normal (VLibras só no clique; GA só com ID).
- e2e `14` continua sem `unsafe-inline`/`unsafe-eval` e sem CDNs de fonte/JS.
- Se o relatório for adotado: teste do controller + tamanho/teto; nenhum log sem rotação.

**Rollback:** remover os hosts da CSP (volta ao estado atual, funcionalmente pior, mas
seguro); desligar `report-uri`.

**kd:** issue `I2` + 4 tasks (CSP portal/CMS, VLibras, GA, relatório).

### I3 — Apache/WHM (canário, host real)

**Objetivo:** corrigir o contrato do proxy burro e o modo de falha, com prova no host.

**Passos**
1. `docker/web/app.conf`: `ProxyPass /.well-known/acme-challenge !` +
   `ProxyPass /.well-known/pki-validation !`; `<Directory>` com `Require all denied` +
   `Options -Indexes` + `AllowOverride None`; grant só nos dois diretórios de validação.
2. `docker/web/app-ssl.conf` (novo): `RequestHeader set X-Forwarded-Proto "https"` +
   `Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"`
   (sem `preload` na primeira janela). Atualizar spec 16/runbook (o gêmeo ssl **não** é
   cópia idêntica).
3. `TraceEnable Off` no vhost/include; `ServerTokens Prod`/`ServerSignature Off` no WHM
   (server-wide, via Include Editor do WHM — não no `userdata`).
4. Confirmar no cPanel/WHM que **não** há `mod_deflate`/`mod_brotli` atuando no vhost do app
   (compressão é do Caddy; dupla compressão mangla ETag/`Content-Encoding`).
5. Publicar em canário (`infra-*`/homolog), medir e só então liberar.

**Aceite (host real, `tools/go-live-check.sh --require-hsts --require-deny`)**
- `curl -I https://<host>/` → HSTS presente; `Content-Encoding` só uma vez.
- `GET https://<host>/.well-known/security.txt` → 200 `text/plain` (G-01).
- `GET http(s)://<host>/.env`, `/composer.json`, `/artisan` → 403/404.
- `TRACE` → 405/403; `Server` não revela versão.
- `X-Forwarded-Proto` chega (log de teste em homolog com `$request->isSecure()`).
- Rollback ensaiado: reverter o include e recarregar o Apache; app continua no ar.

**Rollback:** manter o include anterior versionado no runbook; `apachectl configtest` antes do
reload; AutoSSL intocado (`acme-challenge` continua fora do proxy).

**kd:** issue `I3` + 5 tasks (proxy/deny, ssl/XFP, HSTS, TraceEnable/server tokens, canário).

### I4 — Runtime: PHP, PostgreSQL e Valkey

**Objetivo:** calibrar o runtime com números, não com cópia do V6.

**Passos**
1. `docker/php-fpm/php.ini`: `realpath_cache_size=4096K`, `realpath_cache_ttl=600`.
2. Medir `opcache_get_status(false)` em homolog (used/wasted/interned); se `interned` saturar,
   subir para 32; memória para 256 só com evidência. Registrar os números no `close`.
3. JIT: ligar `tracing` em homolog por uma janela, medir TTFB/CPU com `curl -w` (3 amostras,
   mediana) e **decidir**; se não houver ganho, manter `off` e corrigir a spec 17
   (`jit=tracing` → `off`).
4. Migration `pg_stat_statements` (G-07) + consulta de top-N no runbook.
5. `docker/valkey/valkey.conf`: `maxmemory 256mb`, `maxmemory-samples 5` (G-08); documentar o
   cálculo com o número de sessões/filas.
6. `up_prod.sh`/`entrypoint.sh`: nada muda no mecanismo (USR2 já correto); só a evidência
   (G-19) quando I6 rodar.

**Aceite**
- `php -i` mostra `realpath_cache_ttl=600`; `opcache.validate_timestamps=Off` preservado.
- `SELECT count(*) FROM pg_stat_statements` > 0 após tráfego de teste.
- `valkey-cli CONFIG GET maxmemory` → `256mb`; `evicted_keys` observável.
- `make check E2E=1` verde 2×; nenhum teste flaky (ADR-001).

**Rollback:** reverter o `php.ini`/`valkey.conf` e recarregar FPM/Valkey; a migration do
`pg_stat_statements` é aditiva (não remover a extensão).

**kd:** issue `I4` + 4 tasks (PHP, OPcache/JIT, PG, Valkey).

### I5 — API de dados abertos: CORS

**Objetivo:** cumprir ADR-018 para consumidores browser sem abrir o resto da plataforma.

**Passos**
1. Aplicar `security.open-cors` nas rotas `v1/*` da API (DadosAbertos) mantendo
   `throttle:open-data`; `Access-Control-Allow-Methods: GET, HEAD` e preflight `OPTIONS`
   respondendo 204 (hoje o middleware só anexa headers ao GET; um `Authorization`/header
   customizado dispararia preflight e receberia 405). Sem `Access-Control-Allow-Credentials`
   — a API é anônima.
2. Revisar `config/cors.php` (default vazio) — a exceção é declarada na rota, não global.
3. Opcional: matcher `@api` no Caddy com CORP `cross-origin` (só se necessário).
4. Atualizar `wiki/specs/05-seguranca-lgpd.md`/ADR-018 com a decisão (dado aberto sem PII).

**Aceite**
- `curl -H 'Origin: https://exemplo.org' -D - https://api.<host>/v1/datasets` → ACAO `*`.
- `DadosAbertosTest` cobre o header; e2e `12` consome a API via browser.
- Rate limit continua respondendo 429 RFC 7807.

**Rollback:** remover o middleware da rota (volta ao comportamento server-side only).

**kd:** issue `I5` + 2 tasks (middleware/teste, doc).

### I6 — Higiene de deploy

**Objetivo:** fechar os gaps de operação que não mudam comportamento de página.

**Passos**
1. `up_prod.sh`: `umask 077`/`chmod 600 .env`; `migrate:status` antes do `migrate`; registrar
   `pid` do master do FPM antes/depois do USR2 (G-06/G-19).
2. Remover `storage:link` do `up_prod.sh` e do `composer.json` (`prod`); apagar
   `public/storage` (G-16).
3. `tools/check.sh`: nada novo (I0 já cobre); confirmar que o `edge-config` roda antes do
   deploy.
4. Atualizar `wiki/specs/17-up-prod.md` e o runbook (fases, evidências, `chmod`).

**Aceite**
- `up_prod.sh --dry-run` mostra `umask`/`chmod`/`migrate:status`;
- após deploy real de homolog: `stat -c %a .env` = 600; `ls public/storage` = ausente;
- `DeployPipelineTest` verde; `make check` verde.

**Rollback:** reverter o script; o symlink pode ser recriado manualmente se algum cliente
depender dele (não há uso no código).

**kd:** issue `I6` + 3 tasks.

### I7 — DR: offsite real e retenção de WAL

**Objetivo:** fazer o DR prometido em F6 valer para perda de host, não só de banco.

**Passos**
1. Documentar que `BACKUP_OFFSITE=1` com o RustFS local **não** é offsite (G-15); em produção,
   apontar o disco `s3` para um endpoint externo (`.env` apenas, ADR-030) ou copiar o dump
   para fora do host.
2. Definir retenção/poda do `wal_archive` (G-14) ou enviar o WAL ao S3 (`wal-g`); monitorar
   `du -sh`.
3. Ensaiar `tools/db-restore-drill.sh` a partir da cópia externa e `tools/pitr-drill.sh`;
   registrar RTO.
4. Atualizar `wiki/runbooks/deploy-e-recuperacao.md` com os limites e o alerta.

**Aceite**
- Restore de uma cópia **fora do host** verificado (RTO medido);
- PITR verde; `wal_archive` com retenção definida;
- go-live + `make check E2E=1` verdes.

**Rollback:** voltar ao destino local (perde a propriedade de offsite, mas mantém o backup).

**kd:** issue `I7` + 3 tasks.

### I8 — Decisões registradas (sem código)

Fechar com um registro curto (kd `decision` + ADR quando mudar contrato):

1. **FPC de origem:** não adotar (ADR-022 §6); se adotar, a chave precisa normalizar tracking
   e incluir locale/página, e a invalidação precisa de eventos de modelo + purge.
2. **Multi-CDN:** não contratado; pré-requisitos para contratar (chave normalizada,
   `Vary: Cookie` tratado, purge, bypass de `/cms`/`/api`, failover <60 s). Exige ADR.
3. **APCu autoloader:** só com medição; `classmap-authoritative` já cobre o classmap.
4. **Minificação de HTML:** rejeitada (ADR-022 §7).
5. **PWA/service worker:** o V7 tem manifest, não tem `sw.js`. Quando existir, servir
   `sw.js`/`manifest.webmanifest` com `max-age=0, must-revalidate` (lição `_INFRA` F1.1) e
   nunca `immutable`.
6. **`Vary: Cookie`:** manter (correção > hit ratio) e documentar o requisito de CDN (G-18).
7. **ETag forte + compressão:** manter; o Caddy normaliza (`-gzip`/`-br`) e o e2e `14` prova.
   Proibido ligar `mod_deflate`/`mod_brotli` no Apache.

---

## 5. Fora de escopo / proibido copiar

| Item de `_INFRA` | Por que não |
|---|---|
| `.htaccess` (H1–H9, L0.x) e `php.ini.recomended`/`whm.php.ini.recomended` | Caddy + `AllowOverride None`; chaves `PHP_INI_SYSTEM` por conta são inertes. Só os **valores** de OPcache foram considerados (I4) |
| `mod_deflate`/`mod_brotli` no vhost do app | Dupla compressão e mangling de ETag; o Caddy é o dono |
| `CspNonce`/nonce por request | Quebra ETag/cache (F-01); a CSP é estática no edge |
| `PageCache`/FPC, `cache:page:clear`, `CdnPurgeService` | ADR-022 §6 descarta sem ganho demonstrado; CDN não contratado |
| `HtmlMinify`, `PreloadCritical`, `PictureTransform` | ADR-022 §7 rejeita minify; Vite/`<x-media::picture>` cobrem preload/picture |
| `deploy.sh`/`composer prod` do V6 (seed sempre, `--apcu`, `images:redigest-webp`) | ADR-041; sem acervo legado; `up_prod.sh` é superior |
| `config/csp.php`, `config/cdn.php`, `config.json` | O V7 não lê config de runtime; CSP/CDN não existem |
| `X-Hardening-Version`, `Server: MaximaTecnologia` | Canário do V6; substituído pela borda efetiva (`security-check`/go-live) |
| `db:seed` no deploy, `DB_HOST=127.0.0.1`, `PAGE_CACHE_TTL` | Sem ETL/seed de produção (ADR-041/042); Postgres é serviço do Compose |
| APCu por `pecl` no host, `selectorctl`, alt-php | A imagem do V7 já instala APCu; o host não roda PHP de app |
| Reset de OPcache por `php -r 'opcache_reset();'` | CLI ≠ FPM; o USR2 do container é o mecanismo correto |

---

## 6. DoD do UPDATE_INFRA

- [ ] `I0`–`I7` fechadas no `kd` com evidência (`--outcome success --note`), cada uma com
      `make check` verde **2×**; `I2`/`I3`/`I4` com `make check E2E=1`.
- [ ] Nenhum gate novo é decorativo: cada um falhou ao menos uma vez com a regressão injetada
      (`gates-selftest`/fixture).
- [ ] `tools/security-check.sh` + `tools/go-live-check.sh` cobrem: HSTS, XFO/embed, CSP dos
      dois terceiros, compressão, 304 com `-gzip`, immutable de asset, `/.env` negado,
      `security.txt` 200, `TRACE` negado.
- [ ] `make check` quente ≤100 s e `E2E=1` ≤3 min preservados (ADR-001/038); o passo
      `edge-config` custa **0,05 s** (medido) e não ameaça o orçamento.
- [ ] Specs 16/17 e o runbook atualizados com o contrato real (dono de HSTS, arquivo ssl,
      `edge-config`, offsite/WAL, `storage:link` removido).
- [ ] Nenhuma cópia de `_INFRA/arquivos/` entra no runtime; se algum artefato for reutilizado
      (ex.: `budget.json`), validar `sha256sum -c manifest.sha256` e citar a origem.
- [ ] `kd` sem tarefas `blocked` órfãs; riscos novos registrados (`kd write --type risk`).

---

## 7. Comandos de evidência (reproduzíveis)

```bash
# 1) estado atual da borda (dev)
curl -sI http://localhost:8080/ | tr -d '\r' | grep -iE '^(HTTP|cache-control|etag|vary|csp|content-security|x-frame|strict)'
curl -sI -H 'Accept-Encoding: gzip' http://localhost:8080/ | tr -d '\r' | grep -iE 'content-encoding|etag'
E=$(curl -sS -D - -o /dev/null http://localhost:8080/ | tr -d '\r' | awk 'tolower($1)=="etag:"{print $2}')
curl -s -o /dev/null -w '%{http_code}\n' -H "If-None-Match: $E" http://localhost:8080/            # 304
EG=$(curl -sS -D - -o /dev/null -H 'Accept-Encoding: gzip' http://localhost:8080/ | tr -d '\r' | awk 'tolower($1)=="etag:"{print $2}')
curl -s -o /dev/null -w '%{http_code}\n' -H "If-None-Match: $EG" -H 'Accept-Encoding: gzip' http://localhost:8080/  # 304
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/.env                                # 404/403
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/.well-known/security.txt            # 200
curl -s -o /dev/null -w '%{http_code}\n' -X TRACE http://localhost:8080/                           # 405/403

# 2) runtime
docker compose -p concordia exec -T php-fpm php -i | grep -E '^(opcache\.|realpath_cache|zend\.assertions)'
docker compose -p concordia exec -T postgres psql -U concordia -d concordia -tAc 'select extname from pg_extension'
docker compose -p concordia exec -T valkey sh -c 'VALKEYCLI_AUTH="$REDIS_PASSWORD" valkey-cli CONFIG GET maxmemory maxmemory-policy'

# 3) gates
tools/security-check.sh
tools/go-live-check.sh --url https://<host> --json
dc() { docker compose -p concordia "$@"; }
dc exec -T web caddy validate --config /etc/caddy/Caddyfile
```

> Medido em 2026-10-20 (dev): HTML público `Cache-Control: public, max-age=300, s-maxage=300,
> stale-while-revalidate=86400`, `Vary: Accept-Encoding, Cookie`, ETag SHA-256 forte
> (`-gzip` sob compressão), 304 estável nos dois formatos; asset do Vite `immutable`; `/.env`
> 404; `security.txt` 200; TRACE 405; HSTS/XFO **ausentes**; API sem ACAO;
> `pg_extension` só `plpgsql`; Valkey `maxmemory=0`; `.env` 644.

---

## 8. Referências

- `wiki/backlog/concluido/_INFRA/README.md` (resumo executivo e critérios), `01`–`10`.
- `wiki/backlog/concluido/_INFRA/arquivos/` + `manifest.sha256` (cópias literais; validar antes de reutilizar).
- ADR-001/002/005/008/020/021/022/024/027/030/031/032/038/040/041/042/043.
- `wiki/specs/16-edge-apache-whm.md`, `wiki/specs/17-up-prod.md`,
  `wiki/runbooks/deploy-e-recuperacao.md`, `wiki/implementation/05-borda.md`,
  `wiki/implementation/06-deploy-migracao.md`.
- Código: `docker/web/{Caddyfile,app.conf}`, `docker/php-fpm/{php.ini,entrypoint.sh,zz-custom.conf}`,
  `docker/{postgres/postgresql.conf,valkey/valkey.conf}`, `docker-compose.yml`,
  `up_prod.sh`, `tools/{check.sh,security-check.sh,go-live-check.sh}`,
  `src/core/Internal/Http/Middleware/{PublicResponseCache,StripPublicCookies,PrivateResponse,EnforceSecurityHeaders}.php`.

---

## 9. Rastreabilidade no `kd`

Épico: `epic_00dw7kyx` — `kd task graph --root epic_00dw7kyx`.

| Fase | Issue | Tasks |
|---|---|---|
| I0 — gates | `task_01y0yyqv` | `task_00am9h6v` · `task_00guogie` · `task_01sxf4q1` · `task_01ypqrnl` · `task_00lfxsxc` · `task_003tcqph` |
| I1 — headers | `task_01wz7idz` | `task_00pddcz8` · `task_002u7g5p` · `task_019bnnof` |
| I2 — CSP | `task_007h396o` | `task_000xxivm` · `task_00qxsayu` · `task_01c2zq4i` · `task_01oesydb` |
| I3 — Apache/WHM | `task_0111yud9` | `task_00zfb54q` · `task_01s9edv2` · `task_01ydzptx` · `task_00sofqx5` · `task_00up69qa` |
| I4 — runtime | `task_01h4d7yp` | `task_011nn5br` · `task_00sn1qda` · `task_013pvtvb` · `task_00w408f4` |
| I5 — CORS | `task_013jw0g7` | `task_011yo060` · `task_01104ffq` |
| I6 — deploy | `task_01yb5cev` | `task_00988aa3` · `task_00x8l8rf` · `task_01mc4cbt` |
| I7 — DR | `task_01chfqya` | `task_01nr1a9z` · `task_00fnht86` · `task_0194ct81` |
| I8 — decisões | `task_01y2l345` | `task_00ys3cci` |

Notas de evidência (uma asserção por nota, ancoradas no código):

| Achado | Nota |
|---|---|
| G-01 `security.txt` inalcançável | `fact_01ab4gv8` |
| G-02 CSP × VLibras/GA | `risk_01uegq2a` |
| G-03 HSTS ausente | `risk_00tpf6e2` |
| G-04 falha do proxy expõe o docroot | `risk_00mefikr` |
| G-06 `.env` 644 | `fact_01bhla4n` |
| G-07 `pg_stat_statements` | `fact_00499iry` |
| G-08 Valkey `maxmemory=0` | `risk_00c3860z` |
| G-09 API sem CORS | `fact_00kicahd` |
| G-11 OPcache/spec 17 | `fact_00j0rrhq` |
| G-14 WAL sem retenção | `risk_00ltlp3p` |
| G-15 “offsite” no mesmo host | `risk_01b7zbhw` |

Ao fechar cada task: `kd task close --id <ID> --outcome success --note "<evidência>"`. O
`UPDATE_INFRA` só é considerado concluído quando `kd task graph --root epic_00dw7kyx` não tiver
folhas abertas e o §6 estiver inteiro.
