fix(docker): inicializar permissoes do storage com seguranca
This commit is contained in:
1 parent
e142a44c01
commit
2be97ea969
8 files changed
+421
-3
No files matched your search
@@ -0,0 +1,115 @@
|
||||
# Permissões do armazenamento local em Docker
|
||||
|
||||
Escopo: `docker-compose.yml` (desenvolvimento local). Homologação e produção usam o bucket privado do Supabase e não montam volume em `/app/.data` — `docker-compose.staging.yml` não foi alterado.
|
||||
|
||||
## Problema
|
||||
|
||||
Cadastro de `modelo_imoveis_teste.xlsx` no compose local respondeu `Não foi possível continuar — Erro interno.`
|
||||
|
||||
```
|
||||
uid=1001(adaptador) gid=1001(adaptador)
|
||||
drwxr-xr-x 2 root root /app/.data
|
||||
STORAGE_NOT_WRITABLE
|
||||
```
|
||||
|
||||
Contornado à mão no volume local com `chown 1001:1001` e `chmod 750` em `/app/.data`, depois `STORAGE_WRITABLE`. Esta correção elimina o passo manual.
|
||||
|
||||
## Causa raiz
|
||||
|
||||
1. A imagem não continha `/app/.data`. `LocalPrivateStorage` grava em `/app/.data/storage` e só cria o diretório em runtime.
|
||||
2. Quando um volume nomeado é montado num caminho que não existe na imagem, o Docker cria o ponto de montagem como `root:root 755`. O volume vazio não tem de onde herdar dono.
|
||||
3. Web e worker rodam como `adaptador` (1001) sem capacidades. Não escrevem num diretório de root.
|
||||
4. O health check do storage local só fazia `mkdir -p`, que passa quando o diretório já existe. O processo subia saudável e o erro só aparecia no primeiro upload, como 500 genérico.
|
||||
|
||||
Reproduzido num volume descartável com a imagem anterior: `drwxr-xr-x 0 0 /app/.data` e `touch: Permission denied`.
|
||||
|
||||
## Correção
|
||||
|
||||
Duas camadas, porque `RUN chown` no Dockerfile sozinho não alcança volume que já existe.
|
||||
|
||||
| Camada | Arquivo | O que faz | Alcança |
|
||||
| --- | --- | --- | --- |
|
||||
| Imagem | `Dockerfile` | Cria `/app/.data/storage` com dono `adaptador` e modo `750`. Um volume nomeado **vazio** herda dono e modo na primeira montagem | Volumes novos |
|
||||
| Inicialização | `docker-compose.yml` (`storage-init`) + `docker/entrypoint.sh prepare-storage` | Serviço de uma execução, antes de web e worker, que ajusta **só** `/app/.data` e `/app/.data/storage` e prova a escrita como uid 1001 | Volumes existentes, inclusive os criados antes desta correção |
|
||||
| Falha segura | `docker/entrypoint.sh` (`web`, `worker`) | Antes de iniciar, grava e apaga um arquivo de prova em `/app/.data/storage`. Sem escrita, sai com código 1 e `STORAGE_NOT_WRITABLE` | Qualquer ambiente que use o storage local |
|
||||
|
||||
Alternativa A (preparar dentro do entrypoint de web e worker) foi descartada: exigiria iniciar os processos como root e rebaixar o usuário depois. A alternativa B mantém web e worker exatamente como antes — `USER adaptador`, sem capacidades — e concentra o único passo privilegiado num contêiner que termina.
|
||||
|
||||
## Fluxo de inicialização
|
||||
|
||||
```
|
||||
postgres (healthy) ─┐
|
||||
redis (healthy) ────┤
|
||||
storage-init ───────┼─ completed_successfully ─► web ─► worker
|
||||
(root, sem rede, │
|
||||
termina) │
|
||||
```
|
||||
|
||||
`storage-init`:
|
||||
|
||||
- `user: "0:0"`, `network_mode: none`, `cap_drop: [ALL]` e só `CHOWN`, `FOWNER`, `DAC_OVERRIDE`, `DAC_READ_SEARCH`, `SETUID`, `SETGID`; `no-new-privileges`.
|
||||
- Recusa link simbólico em `/app/.data` ou `/app/.data/storage`.
|
||||
- Muda dono/modo somente quando diferem de `1001:1001 750`, e registra cada mudança.
|
||||
- **Não é recursivo.** Itens com outro dono dentro do volume são contados e reportados, não alterados.
|
||||
- Prova a escrita com `setpriv --reuid 1001` e falha com `STORAGE_NOT_WRITABLE` se não conseguir. Falha do `storage-init` impede web e worker de subir.
|
||||
- UID/GID vêm de `id -u adaptador` na própria imagem, a mesma fonte do `USER` do Dockerfile.
|
||||
|
||||
## Permissões esperadas
|
||||
|
||||
| Caminho | Dono | Modo |
|
||||
| --- | --- | --- |
|
||||
| `/app/.data` | `1001:1001` | `750` |
|
||||
| `/app/.data/storage` | `1001:1001` | `750` |
|
||||
| Subpastas e arquivos criados pela aplicação | `1001:1001` | padrão do processo (`755`/`644`), protegidos pelo `750` acima |
|
||||
|
||||
| Processo | Usuário | Capacidades efetivas |
|
||||
| --- | --- | --- |
|
||||
| web | `adaptador` (1001) | nenhuma |
|
||||
| worker | `adaptador` (1001) | nenhuma |
|
||||
| storage-init | root, só durante a preparação | as seis listadas acima |
|
||||
|
||||
Web e worker montam o mesmo volume `adaptador-planilhas-local-storage` em `/app/.data`. O volume não é servido por HTTP: o download continua passando pela rota autenticada.
|
||||
|
||||
## Volume novo × existente
|
||||
|
||||
- **Novo:** herda `1001:1001 750` da imagem; `storage-init` só confirma e não altera nada.
|
||||
- **Existente com raiz de root:** `storage-init` ajusta os dois diretórios. Conteúdo, dono e modo dos arquivos existentes ficam como estavam.
|
||||
- **Existente já corrigido à mão:** nenhuma mudança de dono; o modo de `/app/.data/storage` passa de `755` para `750` na primeira execução.
|
||||
|
||||
## Validação
|
||||
|
||||
```bash
|
||||
docker compose --parallel 1 build web
|
||||
ADAPTER_IMAGE=adaptador-planilhas:local bash .github/scripts/storage-permissions.sh
|
||||
```
|
||||
|
||||
O script usa apenas volumes e contêineres descartáveis com prefixo `adaptador-storage-test-` e remove só o que criou. Casos:
|
||||
|
||||
| Caso | Verifica |
|
||||
| --- | --- |
|
||||
| A | Volume novo: `1001:1001 750` sem `storage-init`, escrita pelo uid 1001, `STORAGE_WRITABLE` |
|
||||
| B | Volume com raiz `root:root 755` e arquivos: diretórios corrigidos, conteúdo e dono dos arquivos intactos, item de root apenas reportado |
|
||||
| C | Duas execuções extras: snapshot de dono, modo, tamanho e SHA-256 idêntico; nenhuma mudança registrada |
|
||||
| C2 | `--fix-existing` ajusta o dono só quando pedido e preserva o conteúdo |
|
||||
| D | Um contêiner grava, outro lê e apaga; `web` real sobe e responde liveness; `worker` real passa da verificação |
|
||||
| E | web como 1001 com `CapEff` zero; imagem com `USER adaptador`; `prepare-storage` recusa rodar sem root |
|
||||
| F | Volume sem escrita: `web` e `worker` saem com código 1 e `STORAGE_NOT_WRITABLE`; com Supabase configurado a verificação não bloqueia |
|
||||
|
||||
O job `image` do CI executa o mesmo script.
|
||||
|
||||
## Recuperação
|
||||
|
||||
Se `storage-init` reportar `AVISO: N item(ns) ... com outro dono`, os arquivos continuam íntegros, mas a aplicação pode não conseguir sobrescrevê-los ou apagá-los. Ajuste controlado, limitado ao volume, sem seguir link simbólico e sem atravessar outro sistema de arquivos — **execute só com autorização**:
|
||||
|
||||
```bash
|
||||
docker compose run --rm storage-init prepare-storage --fix-existing
|
||||
```
|
||||
|
||||
Se `storage-init` falhar com `STORAGE_NOT_WRITABLE`: confira `docker compose logs storage-init`. Causas prováveis são volume montado somente leitura ou driver de volume que não aceita `chown`. Não use `chmod 777` nem rode web/worker como root.
|
||||
|
||||
## Limitações
|
||||
|
||||
- Bind mount (`./pasta:/app/.data`) não herda dono da imagem. No Docker Desktop/WSL2 sobre o disco do Windows o `chown` pode não ter efeito; a falha segura impede o processo de subir em silêncio. O compose usa volume nomeado.
|
||||
- `--fix-existing` é recursivo dentro do volume e por isso é manual.
|
||||
- O ajuste do volume local real `adaptador-planilhas-local-storage` acontece quando os contêineres forem recriados com o compose novo. Esta validação não recriou os contêineres em execução.
|
||||
- Não valida homologação: o storage de homologação é o bucket privado do Supabase (item D7 do checklist continua PENDENTE).
|
||||
Reference in new issue
Block a user