# 08 — RustFS: revisão da skill `rustfs` e ajustes de storage

> Revisão em profundidade da skill `public-v7/.agents/skills/rustfs/` (RustFS **1.0.2**, commit
> `6153769`) contra o serviço `storage` do V7. O foco é o que muda **container, configuração e
> operação**; não é um resumo da skill. Nada foi aplicado.
>
> Convenção: `arquivo:linha` **sem** prefixo (`03:33-34`, `11:25-34`) é a skill
> `.agents/skills/rustfs/`; `docker-compose.yml`, `up_prod.sh`, `config/...` e
> `src/...` são do V7.

## 1. Estado atual

| Item | Hoje | Evidência |
|---|---|---|
| Imagem | `rustfs/rustfs:1.0.1-glibc@sha256:8776…` | `docker-compose.yml:126` |
| Topologia | **single-node, single-drive** (`/data`) | `docker-compose.yml:133,146` |
| Paridade | `EC:0` implícita (single-drive); warning `storage_class_zero_redundancy` | skill `04:102-124`; `15:3-15` |
| Durabilidade | default `strict`, mas não explícita | skill `03:60-66`; `15:126` |
| Filesystem guard | default `warn` | skill `03:55`; `15:127` |
| Healthcheck | `GET /health` (**liveness**) | `docker-compose.yml:148-153` |
| Logs | `RUSTFS_OBS_LOGGER_LEVEL=error`; `/logs` em **tmpfs**; sem stdout | `docker-compose.yml:131-132,143` |
| Console | desligado; porta 9001 não publicada | `docker-compose.yml:138` |
| Credenciais | as **mesmas** `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` do app | `docker-compose.yml:140-141`; `config/filesystems.php:54-55` |
| Bucket | `storage:ensure-bucket` só cria; sem versioning/lifecycle/PAB | `EnsureStorageBucketCommand.php:20-54` |
| Backup | `up_prod.sh` faz `pg_dump`; **não** copia mídia | `up_prod.sh:42-47,260-297` |
| Rede | bridge `internal` única, compartilhada com web/PG/Valkey | `docker-compose.yml:176-177` |
| Limites/hardening | sem `mem_limit`, `pids_limit`, `security_opt`, `cap_drop`, `stop_grace_period` | `docker-compose.yml:125-153` |
| Observabilidade | sem OTLP; `jaeger` só sob profile e não é coletor de métricas | `docker-compose.yml:164-173` |

O ponto crítico: o bucket `s3` é a **fonte de verdade da mídia** (`GET /midia/{path}` lê o objeto
pelo app; o endpoint RustFS não é exposto), mas está em um único drive sem backup de objetos e sem
redundância. O backup do `up_prod.sh` cobre **apenas o banco**.

### Cobertura da skill

| Bloco | O que foi confrontado | Disposição |
|---|---|---|
| `00`–`01` | Topologia, quórum, readiness, `FormatV3` | RS-02, RS-05, RS-10; não mudar topologia in place |
| `02` | Docker, filesystem, TLS, checklist de produção | RS-01, RS-04, RS-05, RS-14 |
| `03` | `RUSTFS_*`, durabilidade, HTTP, runtime, logs | RS-03, RS-05, RS-09 |
| `04` | Reed-Solomon, `EC:0`, fsync, bitrot, heal | RS-05, RS-10; single-drive não tem heal útil |
| `05` | Cobertura/limites S3, path-style, assinatura | Path-style já correto (`config/filesystems.php:62`); sem ação |
| `06` | Versioning, lifecycle, PAB, quota, SSE default | RS-08; quota adiada |
| `07` | IAM/policies, TLS, hardening, egress | RS-07, RS-11, RS-14 |
| `08` | SSE-S3/KMS/C, rotação, ETag, compliance | RS-12 (condicional) |
| `09` | Bucket/site replication, ODM, DR | RS-06 como alternativa; sem target agora |
| `10` | Tiering, notificações, audit | Não adotar agora (sem target/sink) |
| `11` | Health, logs, métricas, alertas | RS-02, RS-03, RS-13 |
| `12` | Expansão, rebalance, decommission, heal, restart | RS-10; runbook de não apagar metadata |
| `13` | Swift/SFTP/FTPS/WebDAV/S3 Tables | Não adotar; superfície desnecessária |
| `14` | Troubleshooting de proxy/quórum/storage | Base da verificação; proxy não se aplica (sem CDN) |
| `15`/`99` | Cheatsheet, exemplo completo, anti-patterns | RS-04, RS-06, RS-14; exemplo não copiado (TLS/OIDC/console) |

### Achados na própria skill

| Ponto | O que a skill diz | Leitura para o V7 |
|---|---|---|
| Versão × commit | `SKILL.md:20-22` diz 1.0.2, source commit `6153769` (14/10); a tag oficial 1.0.2 aponta `2b243d8` (16/10) | O conteúdo é útil, mas valide a versão no binário (`rustfs --version`) e trate o commit como snapshot da skill, não como prova do binário. |
| Healthcheck do exemplo | `99:71-80` usa `/health`; `11:25-49` diz que `/health` é liveness e `/health/ready` é membership | Seguir `11` e `14`: usar readiness (RS-02), não copiar o exemplo. |
| Quickstart com `latest` | `02:44-58` usa `rustfs/rustfs:latest` e depois manda pinar | O V7 já usa digest; manter o pin e ignorar o `latest`. |
| Condição `aws:SecureTransport` | `07:51-68` mostra policy com `aws:SecureTransport=true` | O S3 do V7 é HTTP interno; essa condição negaria o app. Usar a policy sem ela até RS-11. |
| Exemplo “completo” | `99:7-106` sobe 4 drives, console, TLS, OIDC, notificações e `EC:2` | Não é template para o V7 single-drive/sem console; aproveitar apenas os blocos indicados. |

## 2. Resumo dos achados

| ID | Achado | P/E | Relação |
|---|---|---|---|
| RS-01 | Atualizar para 1.0.2-glibc e re-pinar o digest | P1 / B | — |
| RS-02 | Healthcheck de **readiness** (e write gate), não liveness | P1 / B | R-03 |
| RS-03 | Logs em stdout, nível `info`; não depender de `/logs` tmpfs | P1 / B | O-04 |
| RS-04 | `stop_grace_period`, `no-new-privileges`, `cap_drop`, PIDs/nofile | P1 / B | S-01/P-08 |
| RS-05 | Explicitar `strict` e `UNSUPPORTED_FS_POLICY=fail`; não silenciar `EC:0` | P1 / B | — |
| RS-06 | **Backup/DR da mídia** (hoje inexistente) | **P0** / M | R-05 não cobre |
| RS-07 | Usuário de workload separado do root | P1 / M | S-04 |
| RS-08 | Bootstrap do bucket: versioning, lifecycle, PAB, limite de versões | P1 / M | RS-07 |
| RS-09 | Limites e tuning com cgroup/medição | P2 / M | P-08 |
| RS-10 | Decisão sobre single-drive/`EC:0` | P1 / M | RS-06 |
| RS-11 | Segmentar storage na rede `data`; TLS interno depois | P2 / M | S-03 |
| RS-12 | Criptografia em repouso/SSE (condicional) | P2 / A | — |
| RS-13 | OTLP/métricas e alertas (depois dos logs) | P2 / M | O-02 |
| RS-14 | `_FILE` para credenciais root; separar do app | P2 / B | S-04 |

## 3. Achados

### RS-01 — Atualizar RustFS 1.0.1 → 1.0.2

- **Hoje:** `rustfs/rustfs:1.0.1-glibc@sha256:87763ed4…` (`docker-compose.yml:126`). A skill está
  pinada em **1.0.2** (`SKILL.md:20-22`; `VERSION`).
- **Mudança:** subir para a tag glibc 1.0.2 e re-pinar o **index digest**:
  `rustfs/rustfs:1.0.2-glibc@sha256:1617cf459224b161bde874144f96969421ad07b4eb086a44c38d1fe31f36c595`.
  O digest foi resolvido com `docker buildx imagetools inspect rustfs/rustfs:1.0.2-glibc`.
- **Ganho:** a release 1.0.2 inclui correções que atingem este deployment — `RUSTFS_OBS_USE_STDOUT`
  com endpoints por sinal, admissão de multipart copy/HTTP write buffers, redução de I/O no
  multipart, normalização de falha de quórum em PUT e correções de scanner/heal
  ([release oficial 1.0.2](https://github.com/rustfs/rustfs/releases/tag/1.0.2), 16/10/2026).
  Para uploads de mídia e observabilidade, é a atualização de menor risco e maior retorno.
- **Risco:** a topologia `FormatV3` é imutável (`12:3-21`), mas a atualização de 1.0.1 para 1.0.2
  não muda o layout. Ainda assim, faça backup da mídia (RS-06) e valide leitura de objetos
  existentes antes de considerar concluída. Não pule para 1.0.3 sem atualizar esta skill e testar.
- **Verificação:** `docker compose exec storage rustfs --version`; `/health/ready` 200; GET de um
  objeto existente; PUT/GET/range/multipart de um objeto novo; `docker compose logs storage`
  sem pânico; `make check` e `make check E2E=1`.
- **P/E:** P1 / B.

### RS-02 — Healthcheck deve ser de readiness

- **Hoje:** `curl -fsS http://127.0.0.1:9000/health` (`docker-compose.yml:149`). `/health` é
  **liveness** e não depende de storage/IAM/quorum (`11:25-34`); a API S3 responde `503` com
  `Retry-After: 5` antes de `FullReady` (`11:36-37`; `14:18-24`). Como `php-fpm`/`queue` usam
  `condition: service_healthy` (`docker-compose.yml:49-55,78-84`), o app pode começar a aceitar
  requests antes de o S3 aceitar escritas.
- **Mudança:** probe de read **e** write:
  `curl -fsS http://127.0.0.1:9000/health/ready && curl -fsS http://127.0.0.1:9000/minio/health/cluster`.
  Aumentar `start_period` para `30s` e manter `interval: 10s`, `timeout: 3s`, `retries: 10`.
  Se o cluster probe oscilar sem motivo aparente, usar só `/health/ready` e documentar que o
  write gate é verificado no deploy.
- **Ganho:** `depends_on` passa a significar “S3 aceita leitura e escrita”, não apenas “processo
  vivo”; evita 503 na primeira mídia após um restart.
- **Risco:** readiness depende de IAM/locks (`11:39-44`); num single-node saudável não deve
  falhar. Um probe de write gate mais estrito pode atrasar o start durante recuperação — que é o
  comportamento desejado.
- **Verificação:** `docker compose up -d storage php-fpm queue`; `docker compose ps` com health
  `healthy`; parar o storage e confirmar que `php-fpm`/`queue` não sobem; `./up_prod.sh --dry-run`.
- **P/E:** P1 / B.

### RS-03 — Logs em stdout e nível `info`

- **Hoje:** `RUSTFS_OBS_LOGGER_LEVEL: "error"` e `/logs` em `tmpfs` (`docker-compose.yml:131-132,143`).
  Em produção o default da skill é log em **arquivo**; `RUSTFS_OBS_USE_STDOUT=true` também envia
  para stdout (`11:54-62`). O `tmpfs` some no restart e não há coletor — na prática, os logs do
  storage não existem. Isso contradiz o requisito “logs ativados em produção”.
- **Mudança:** `RUSTFS_OBS_LOGGER_LEVEL: "info"`, `RUSTFS_OBS_USE_STDOUT: "true"` e
  `RUSTFS_OBS_ENVIRONMENT: "production"`. Manter `/logs` em `tmpfs` (a imagem o possui como UID
  10001) e deixar o `json-file` rotacionado do Docker como retenção. Não apontar
  `RUSTFS_OBS_LOG_DIRECTORY` para um caminho sem volume.
- **Ganho:** `docker compose logs storage` passa a mostrar eventos úteis
  (`disk_local_durability_mode`, `put_object_body_read_stalled`, `storage_class_zero_redundancy`,
  `scanner_pause_backlog` — `11:64-71`), com correlação pelo log do Docker. A release 1.0.2
  corrige o honor de `RUSTFS_OBS_USE_STDOUT` com endpoints por sinal.
- **Risco:** volume de log maior que `error`; o `json-file` 10 MB × 3 já limita. Não habilitar
  `debug` em produção.
- **Verificação:** `docker compose logs storage --tail=50`; reiniciar e confirmar que as linhas
  continuam; `docker compose logs storage | grep -i durability`.
- **P/E:** P1 / B.

### RS-04 — Encerramento gracioso e sandbox do container

- **Hoje:** nenhum `security_opt`, `cap_drop`, `pids_limit`, `ulimits` ou `stop_grace_period`
  (`docker-compose.yml:125-153`). O Compose usa o default de 10 s para SIGTERM; a skill usa
  `TimeoutStopSec=45s` no systemd (`02:134-151`) e o servidor faz fsync no shutdown.
- **Mudança:**
  - `stop_grace_period: 45s` (deixa o `strict` terminar os fsyncs).
  - `security_opt: ["no-new-privileges:true"]` e `cap_drop: [ALL]` (a porta 9000 não exige
    capability).
  - `pids_limit: 256`, `ulimits.nofile: 65536`.
  - `mem_limit: 512m` via P-08, com `docker stats` antes/depois.
  - `read_only: true` **condicional**: adicionar `tmpfs: ["/logs:mode=1777", "/tmp:size=64m,mode=1777"]`
    e testar; a skill não exige rootfs gravável, mas a imagem pode tocar `/app`/`/tmp`.
- **Ganho:** reduz privilégio/movimento lateral, evita truncar fsync no restart e impede fork/oom
  do storage derrubar o host.
- **Risco:** `cap_drop: ALL` não deve afetar o non-root; `read_only` pode quebrar se o binário
  escrever fora de `/data`/`/logs`/`/tmp`. Aplicar um por vez.
- **Verificação:** `docker inspect -f '{{.HostConfig.SecurityOpt}} {{.HostConfig.CapDrop}} {{.HostConfig.PidsLimit}} {{.HostConfig.ReadonlyRootfs}}'`;
  PUT de 110 MB durante `docker compose restart storage`; `docker events` sem OOM.
- **P/E:** P1 / B.

### RS-05 — Durabilidade e filesystem explícitos

- **Hoje:** os defaults da 1.0.2 são `strict` e `warn` (`03:60-66`, `03:55`), mas nada no Compose
  registra a decisão. `strict` é o único modo defensável em single-node (`04:146-160`; `99:311-312`).
- **Mudança:** `RUSTFS_DURABILITY_MODE: "strict"` e `RUSTFS_UNSUPPORTED_FS_POLICY: "fail"`.
  **Não** definir `RUSTFS_STORAGE_CLASS_STANDARD=EC:0`: o explícito **silencia** o warning
  `storage_class_zero_redundancy` (`04:117-124`); o default implícito mantém o aviso visível.
- **Ganho:** falha cedo se `DATA_PATH` for NFS/CIFS/FUSE/overlay (`SKILL.md:46-48`; `14:34-43`),
  em vez de descobrir corrupção/IO lento em produção; documenta a garantia de fsync.
- **Risco:** `fail` pode rejeitar um filesystem POSIX não previsto (ZFS etc.) — testar no host
  antes. Não usar `RUSTFS_UNSAFE_BYPASS_DISK_CHECK` para “resolver”.
- **Verificação:** log de startup com `disk_local_durability_mode`; `findmnt -T $DATA_PATH`;
  `rustfs info --json`; `docker compose up -d storage` sem `PoolTopologyMismatch`.
- **P/E:** P1 / B.

### RS-06 — Backup/DR da mídia (P0)

- **Hoje:** `up_prod.sh` gera `pg_dump -Fc` e valida com `pg_restore --list` (`up_prod.sh:260-297`);
  o disco `s3` aponta para o RustFS **do próprio host** e o script avisa que ele não é offsite
  (`up_prod.sh:173-180`; `config/_OTHER_CONFIGS.md`). Não há cópia dos objetos de mídia. O bucket
  é single-drive `EC:0`; a perda do host/disco perde a mídia. O DB pode ser restaurado, mas as
  referências de mídia não.
- **Mudança (recomendada):** rotina diária de **S3-level copy** para um endpoint externo:
  `mc mirror --preserve --overwrite --exclude 'staging/*' \
  local/$AWS_BUCKET offsite/$OFFSITE_MEDIA_BUCKET`.
  Usar um bucket offsite **dedicado à mídia** (novo `OFFSITE_MEDIA_BUCKET`, mesmo endpoint/
  credenciais do `OFFSITE_*`) para preservar as keys sem misturar com os dumps do banco. Nunca usar
  `--remove` (backup não é espelho destrutivo). Aplicar lifecycle de retenção no destino
  (ex.: 30/90 dias) e rodar um restore trimestral para um bucket de scratch, comparando
  `mc stat`/checksum e lendo pelo app.
  Alternativa: bucket replication para um S3 externo (skill `09:16-61`), que exige versioning e
  target compatível; **não** usar `replication-check` como health check — ele é mutante
  (`05:180-192`).
- **Ganho:** RPO explícito para a mídia; recuperação de host perdido, corrupção de `xl.meta`
  (`04:179-191`) e delete acidental. O dump do DB sozinho não reconstrói os arquivos.
- **Risco:** custo de banda/espaço; `staging/` é quarentena e não precisa ir; versioning/objetos
  antigos aumentam o volume; mirror não copia bucket policy/lifecycle — manter isso como código.
  Se RS-08 habilitar versioning, um `mc mirror` sem `--versions` copia apenas a versão **current**;
  valide o suporte a `--versions` na versão pinada do `mc` (ou use bucket replication), senão o
  backup não preserva o histórico de versões.
- **Verificação:** contar objetos/tamanho antes/depois (`mc du local/"$AWS_BUCKET"` e
  `mc ls --recursive local/"$AWS_BUCKET" | wc -l`); restaurar um objeto e comparar SHA-256/ETag;
  desligar o storage local e ler a cópia; documentar RPO/RTO.
- **P/E:** **P0** / M.

### RS-07 — Separar root de workload (least privilege)

- **Hoje:** o Compose entrega `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY` como
  `RUSTFS_ACCESS_KEY`/`RUSTFS_SECRET_KEY` (`docker-compose.yml:140-141`) e o Laravel usa as mesmas
  (`config/filesystems.php:54-55`). Qualquer RCE no PHP herda **root** do storage, incluindo
  `admin:*`, IAM e KMS. A skill exige remover `admin:*` de workloads e usar policies mínimas
  (`07:238-253`).
- **Mudança:** criar um usuário de workload (service account) com policy customizada no bucket:
  `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject`, `s3:ListBucket`,
  `s3:AbortMultipartUpload`, `s3:ListMultipartUploadParts` (ajustar após testar o upload
  multipart). O root fica só no bootstrap (`storage-init`) e em operações administrativas.
  O `storage:ensure-bucket` atual roda no `php-fpm` com as credenciais do app
  (`up_prod.sh:310-325`); se o app perder `CreateBucket`, mover a criação/configuração para um
  one-shot `storage-init` com root. Não adicionar a condição `aws:SecureTransport=true` da skill
  enquanto o S3 for HTTP interno: isso negaria o app.
- **Ganho:** uma chave de app vazada não apaga buckets, não altera IAM/KMS e não lê outros
  buckets; reduz o impacto de RCE.
- **Risco:** a policy pode faltar uma ação usada pelo Flysystem (multipart/list); testar upload
  grande, download por stream e delete. O root continua necessário no deploy.
- **Verificação:** `mc admin user info`; `mc admin policy attach`; com a chave do app,
  `mc admin info` deve falhar e `mc cp`/`mc rm` no bucket deve funcionar; `GET /midia/...` 200.
- **P/E:** P1 / M.

### RS-08 — Bootstrap do bucket: versioning, lifecycle e acesso privado

- **Hoje:** `storage:ensure-bucket` só verifica/cria o bucket
  (`EnsureStorageBucketCommand.php:20-54`). Não habilita versioning, não configura lifecycle de
  multipart/noncurrent, não aplica Public Access Block e não limita versões. O app serve mídia por
  `GET /midia/{path}` (`src/services/Media/README.md:47-50`), então o bucket pode ser privado.
- **Mudança:** transformar o bootstrap em configuração idempotente:
  - `PutPublicAccessBlock` com os quatro flags `true` (bucket privado; o app usa stream).
  - Versioning `Enabled` + `RUSTFS_API_OBJECT_MAX_VERSIONS=20` (default ilimitado,
    `03:80`); lifecycle `AbortIncompleteMultipartUpload` em 7 dias e
    `NoncurrentVersionExpiration` em 30/90 dias (`06:72-96`).
  - `mc ilm import`/API equivalente; não configurar transição/tier (sem target).
  - Quota HARD só depois de medir o volume; quota mal dimensionada vira `503`
    (`06:98-116`).
- **Ganho:** recuperação de delete/overwrite, limite de versões e limpeza de multipart órfão
  (evita disco cheio silencioso). O PAB impede exposição acidental do bucket.
- **Risco:** versioning aumenta espaço; lifecycle depende do scanner; PAB pode quebrar um acesso
  público que não exista hoje (o app usa stream, então é seguro). A regra de lifecycle não pode
  ter múltiplas transições (`06:76-79`).
- **Verificação:** `mc version info local/$AWS_BUCKET`; `mc ilm rule ls`; upload multipart
  interrompido e conferir abort após o prazo; `aws s3api get-public-access-block`; `mc ls` sem
  `--anonymous`.
- **P/E:** P1 / M.

### RS-09 — Limites e tuning com cgroup/medição

- **Hoje:** sem limites (`docker-compose.yml:125-153`); a skill usa 16 workers Tokio, 1024
  blocking, 32 MiB de encode EC e 4 MiB de duplex (`03:108-138`). O RustFS lê CPU/memória do
  cgroup v2 (`11:164-171`), mas isso não garante que os defaults caibam em 512 MB.
- **Mudança:** aplicar P-08 (`mem_limit`, `cpus`, `pids_limit`, `nofile`) e medir. Se o host tiver
  2 vCPU, começar com `RUSTFS_RUNTIME_WORKER_THREADS=4` e
  `RUSTFS_RUNTIME_MAX_BLOCKING_THREADS=256`; `RUSTFS_API_MAX_CONNECTIONS=256`.
  Reduzir `RUSTFS_ERASURE_ENCODE_MAX_INFLIGHT_BYTES`/`OBJECT_DUPLEX_BUFFER_SIZE` **somente** se
  `memory.current`/OOM mostrarem pressão; não copiar valores de outro host.
- **Ganho:** previsibilidade de RSS e CPU; evita OOM do storage derrubar PHP/Postgres.
- **Risco:** limite baixo gera `OOMKilled`/`SlowDown`; `io_uring` e cleanup isolation não são
  necessários agora (`03:144-153`).
- **Verificação:** `docker stats --no-stream` durante upload multipart; log de startup com
  `container resources (detected from cgroup)`; `docker events` sem OOM; `rustfs_container_*`.
- **P/E:** P2 / M.

### RS-10 — Single-drive/`EC:0`: decisão explícita

- **Hoje:** `/data` único → `N=1`, `M=0` (`04:102-124`). Não há redundância; uma falha de disco
  perde o set. A skill classifica `EC:0` como “raw capacity, not redundancy” e exige camada
  externa + sign-off (`SKILL.md:51-53`).
- **Decisão:** escolher **uma**:
  1. **Manter single-drive** (recomendado para o host atual) + RS-06 (backup offsite) + aceite
     formal de RPO; documentar que não há HA.
  2. **Novo deployment com 4 discos físicos** e `EC:2` (`RUSTFS_VOLUMES='/data/vol{1...4}'`),
     que tolera 2 falhas **de disco**, não a perda do host. A topologia é imutável:
     não se adicionam drives ao pool existente; migre via S3 (`12:3-34,62-104`).
  3. Multi-node fica fora do escopo.
- **Ganho:** tira o `EC:0` do “default silencioso” e transforma em decisão operacional; evita
  acreditar que um único bind mount é redundante.
- **Risco:** a opção 2 dobra o consumo bruto e exige janela de migração; discos no mesmo host não
  protegem contra falha do host. Não definir `EC:0` explícito: isso silencia o warning
  (`04:117-119`).
- **Verificação:** log `storage_class_zero_redundancy` (single) ou `rc admin pool list`/status
  (4 discos); `mc stat` após falha simulada dentro da paridade (apenas opção 2).
- **P/E:** P1 / M.

### RS-11 — Rede: storage só no plano de dados; TLS depois

- **Hoje:** a bridge `internal` é compartilhada por `web`, `php-fpm`, `queue`, `postgres`,
  `valkey` e `storage` (`docker-compose.yml:176-177`). Qualquer container comprometido pode
  alcançar o S3; as credenciais root viajam em HTTP. S-03 já propõe separar `edge`/`app`/`data`.
- **Mudança:** no S-03, colocar `storage` **somente** em `data` (`internal: true`); `web` não
  deve alcançá-lo. Só depois avaliar TLS interno: `RUSTFS_TLS_PATH=/etc/rustfs/tls:ro`,
  `AWS_ENDPOINT=https://storage:9000` e CA confiável no PHP (exige mudança em
  `config/filesystems.php`/`http.verify`). A skill exige TLS para console/admin e recomenda TLS
  entre nós (`07:183-209,238-253`); aqui o console está desligado e não há peers.
- **Ganho:** reduz movimento lateral e sniffing de credenciais; TLS interno é defesa em
  profundidade, não substituto de rede.
- **Risco:** TLS interno quebra o app se a CA não for configurada; `SSE-C` exige TLS
  (`07:207-208`), então não usá-lo antes.
- **Verificação:** `docker compose exec web getent hosts storage` deve falhar; `php-fpm` alcança
  `storage:9000`; upload/stream 200. Com TLS, `openssl s_client` e upload.
- **P/E:** P2 / M.

### RS-12 — Criptografia em repouso/SSE (condicional)

- **Hoje:** sem SSE (`docker-compose.yml:135-144`). A skill oferece SSE-S3 com master key local
  **sem rotação** (perder a chave = perder tudo), SSE-KMS com Vault/AWS e SSE-C, e deixa claro
  que não é FIPS validado (`08:22-37,217-237`).
- **Mudança:** decidir por classificação de dados. Para mídia pública, o host com LUKS/volume
  cifrado é mais simples e não acopla o app a uma chave. Se houver PII/segredo em objetos,
  usar KMS de verdade (Vault Transit/AWS) e definir custódia/backup da chave **antes** de
  habilitar; não habilitar SSE-S3 local “só para marcar a caixa”.
- **Ganho:** confidencialidade em repouso sem perda silenciosa de legibilidade.
- **Risco:** KMS indisponível → `503` (`08:203-215`); trocar a master key local sem migração
  torna objetos antigos ilegíveis; SSE-C exige TLS e o cliente enviar o trio de headers.
- **Verificação:** `PutBucketEncryption`/`HeadObject` mostra SSE; app lê objeto cifrado; simular
  KMS parado e confirmar erro explícito (nunca fallback plaintext).
- **P/E:** P2 / A.

### RS-13 — OTLP/métricas e alertas

- **Hoje:** sem `RUSTFS_OBS_ENDPOINT`; `jaeger` só existe sob profile e recebe traces, não é
  coletor de métricas (`docker-compose.yml:164-173`). A skill recomenda OTLP para coletor e
  alertas de capacidade/readiness/bitrot/scanner (`11:3-23,173-190`).
- **Mudança:** primeiro RS-03 (stdout); depois, se O-02 existir, apontar
  `RUSTFS_OBS_ENDPOINT=http://<collector>:4318`, definir
  `OTEL_RESOURCE_ATTRIBUTES=rustfs.cluster.id=<projeto>` e importar as regras. Não apontar para o
  Jaeger esperando métricas.
- **Ganho:** capacidade (>80%), `/health/ready` 503, bitrot, heal e scanner deixam de ser
  invisíveis; o container passa a ter telemetria real.
- **Risco:** cardinalidade/volume; sem coletor, o exportador falha (circuito). Manter logs
  stdout como fallback.
- **Verificação:** collector recebe `rustfs_*`; dashboard mostra uso por bucket; alerta de
  capacidade dispara em teste; `docker compose logs storage` sem erro de export.
- **P/E:** P2 / M.

### RS-14 — Credenciais root via `_FILE`

- **Hoje:** `RUSTFS_ACCESS_KEY`/`RUSTFS_SECRET_KEY` por ambiente (`docker-compose.yml:140-141`);
  aparecem em `docker inspect`. A skill confirma `RUSTFS_ACCESS_KEY_FILE`/
  `RUSTFS_SECRET_KEY_FILE` (`03:33-34`; `15:209-219`) e recomenda `*_FILE` (`07:238-253`).
- **Mudança:** montar secrets/files `0400` e usar `_FILE`; manter as chaves do app separadas
  (RS-07). `RUSTFS_RPC_SECRET` e `RUSTFS_IAM_MASTER_KEY` **não** têm variante `_FILE` na skill
  (`99:97-102`); não inventar. Como não há peers remotos/2FA, não são necessários agora.
- **Ganho:** root não fica no `Config.Env`; reduz vazamento por `inspect`/diagnóstico.
- **Risco:** permissões do arquivo e compatibilidade da 1.0.1; validar após RS-01. Não rotacionar
  root em runtime (exige restart).
- **Verificação:** `docker inspect` sem valores; `docker compose exec storage env | grep RUSTFS_`
  sem secret; upload/stream; `mc admin info` com root.
- **P/E:** P2 / B.

## 4. O que **não** fazer

| Prática | Por quê | Evidência |
|---|---|---|
| Copiar `/data`, shards, `xl.meta` ou `format.json` como backup/migração | Não é backup lógico; `format.json` copiado reivindica identidade e quebra o slot | `SKILL.md:57-58`; `12:16-21`; `99:313` |
| Apagar `format.json`, `pool.bin`, `rebalance.bin`, `.scanner-pause-backlog.json` ou `.rustfs.sys` para “destravar” | Remove a autoridade de recuperação; não há force-reset suportado | `12:11-21,215-240`; `99:317-318` |
| Definir `EC:0` explícito para sumir com o warning | O warning é a única sinalização de zero redundância; explícito o silencia | `04:117-124` |
| Usar `RUSTFS_DURABILITY_MODE=relaxed` em single-node | Metadados podem sumir em queda de energia; só faz sentido multi-nó com energia independente | `04:146-160`; `99:312` |
| NFS/CIFS/FUSE/overlay para `/data` | Fora do boundary de produção; corrupção/IO inconsistente | `SKILL.md:46-48`; `02:9,15-16` |
| `RUSTFS_UNSAFE_BYPASS_DISK_CHECK` ou `RUSTFS_OBJECT_GET_SKIP_BITROT_VERIFY` | Ignora a checagem de independência/bitrot; não é tuning | `03:56`; `04:192-200` |
| `mc mirror --remove` como backup | Transforma o backup em espelho destrutivo; um delete local apaga o remoto | skill `02:285` (backup via S3/replicação; nunca cópia de shards) |
| Expor console/admin sem TLS/OIDC | Console default `rustfsadmin/rustfsadmin`; admin API é root | `SKILL.md:59-61`; `07:238-253` |
| Tratar `replication-check` como read-only | Ele grava/apaga probes no target | `05:180-192` |
| Mudar endpoints/ordem/topologia de um pool existente | `FormatV3` imutável; single-drive não expande | `SKILL.md:39-45`; `12:3-34` |

## 5. O que não adotar agora

| Recurso da skill | Por que não agora | Quando reavaliar |
|---|---|---|
| Multi-node, site replication, rebalance/decommission | Um host; não protege contra perda do host e adiciona operação | Quando houver 2+ hosts e requisito de HA |
| Bucket replication como **substituto** do backup | Exige target/versioning e ainda é cópia contínua; `mc mirror` é mais simples para DR | Se houver target S3 e necessidade de replicação contínua |
| KMS (Vault/AWS), SSE-KMS, SSE-C | Custo/complexidade de custódia; SSE-C exige TLS | Se houver PII/requisito de cifra gerenciada |
| OIDC/Keystone/console/MFA | Console desligado; single root; sem IdP | Se o console for exposto ou houver operadores humanos |
| Tiering/ILM para S3 remoto | Sem target; lifecycle local basta | Se o volume local crescer além do orçamento |
| Notificações/audit/S3 Tables | Sem sink/consumidor; fora do escopo | Quando houver pipeline de eventos/auditoria |
| SFTP/FTPS/WebDAV/FTP | Não usados; ampliam superfície; defaults desligados | Só com caso de uso e TLS/porta explícitos |
| io_uring, cleanup isolation, shard integrity, `PUT_FOREGROUND_ADMISSION` | Sem medição; risco de regressão | Com benchmark de GET/PUT/Range e cgroup |
| TLS interno imediato | S-03 (rede) resolve o vetor principal com menos risco | Depois de S-03 e com CA no cliente |
| Quota HARD | Pode virar `503` antes de a capacidade real ser conhecida | Depois de 30 dias de uso medido |

## 6. Sequência sugerida

| Etapa | Itens | Depende de | Critério |
|---|---|---|---|
| S0 — agora | RS-06 (backup), RS-01, RS-02, RS-03, RS-05, RS-04 | — | mídia copiada/restaurável; 1.0.2; `/health/ready`; logs stdout; `strict`/`fail`; hardening |
| S1 — próxima janela | RS-07, RS-08, RS-10 | RS-01 (para `_FILE`/1.0.2) | app sem root; bucket versionado/lifecycle/PAB; decisão single-drive documentada |
| S2 — medido | RS-09, RS-11, RS-13, RS-14, RS-12 | P-08, S-03, O-02; classificação de dados | limites sem OOM; storage isolado; métricas; segredos por arquivo; cifra decidida |

Ordem interna do S0: **RS-06 → RS-01 → RS-03 → RS-02/RS-05 → RS-04** (backup antes de trocar a
imagem; 1.0.2 antes do fix de stdout; `mem_limit` de RS-04 entra com P-08/RS-09). RS-10 é uma
decisão de produto/operação; não bloqueia o backup.

## 7. Verificação

```bash
docker compose config -q
docker compose up -d storage
docker compose exec -T storage curl -fsS http://127.0.0.1:9000/health/ready
docker compose exec -T storage curl -fsS http://127.0.0.1:9000/minio/health/cluster
docker compose logs storage --tail=50 | jq -e . >/dev/null
docker inspect -f '{{.HostConfig.SecurityOpt}} {{.HostConfig.CapDrop}} {{.HostConfig.PidsLimit}}' \
  "$(docker compose ps -q storage)"
docker compose exec -T storage rustfs info --json
```

Dados (com `mc` fixado por digest, em um one-shot na rede `data`; o storage **não** publica
porta no host):

```bash
mc alias set local http://storage:9000 "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY"
mc du local/"$AWS_BUCKET"
mc ls --recursive local/"$AWS_BUCKET" | wc -l
mc stat local/"$AWS_BUCKET"/permanent/<amostra>
mc version info local/"$AWS_BUCKET"
mc ilm rule ls local/"$AWS_BUCKET"
```

Upload e restart:

```bash
# 110 MiB (mesmo limite do PHP/app)
dd if=/dev/urandom of=/tmp/rs.bin bs=1M count=110
mc cp /tmp/rs.bin local/"$AWS_BUCKET"/staging/rs.bin
mc rm local/"$AWS_BUCKET"/staging/rs.bin
docker compose restart storage
docker compose exec -T storage curl -fsS http://127.0.0.1:9000/health/ready
```

Backup/restore (RS-06):

```bash
mc mb --ignore-existing offsite/"$OFFSITE_MEDIA_BUCKET"
mc mirror --preserve --overwrite --exclude 'staging/*' \
  local/"$AWS_BUCKET" offsite/"$OFFSITE_MEDIA_BUCKET"
mc mb --ignore-existing scratch/"$AWS_BUCKET"-restore
mc mirror --preserve --overwrite \
  offsite/"$OFFSITE_MEDIA_BUCKET" scratch/"$AWS_BUCKET"-restore
mc stat scratch/"$AWS_BUCKET"-restore/permanent/<amostra>
```

Para a aplicação, o aceite é `make check` duas vezes; `make check E2E=1` só é obrigatório se
S-03/O-01 mudarem borda/headers. Nenhum bloco deste documento foi aplicado ao
`docker-compose.yml`.


## Fontes

- Skill local: `public-v7/.agents/skills/rustfs/` (`SKILL.md`, `00`–`15`, `99`), pinada em 1.0.2.
- Release 1.0.2: <https://github.com/rustfs/rustfs/releases/tag/1.0.2>.
- Digests: `docker buildx imagetools inspect rustfs/rustfs:1.0.1-glibc` e
  `rustfs/rustfs:1.0.2-glibc` (resolvidos em 2026-10-21).
