Pular para conteúdo

0012 — Garage (arquivos): bucket+key e corretor de presign broker-signed

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-10 · Decidido em: 2026-07-03

Contexto

Fase 5 da Central de Apps: o provider de arquivos (Garage — S3-compatível, Admin API v2 em arquivo-admin.xadm.biz). Por app: 1 bucket + 1 access key read/write escopada. É o 1º provider com secret per-app (o secretAccessKey), então define como o segredo é guardado e como o app acessa os arquivos sem que o segredo vaze para o device.

Decisão

  1. Provisão idempotente (GarageProvider): GetBucketInfo?globalAlias=<app_id>-or-CreateBucket → reusa a key associada ao bucket (GetKeyInfo?showSecretKey=true) ou CreateKey+AllowBucketKey (read+write, owner:false). A idempotência da key usa o keys[] do bucket — sem inventar um ListKeys. client_config = {endpoint, bucket, region} (→ app.json, público). O secretAccessKey é cifrado (AesCipher/AUTH_CIPHER_KEY) em secret_ref — nunca sai do auth.
  2. Acesso = presign broker-signed (decisão 4.4a): o GarageBrokerController (GET /api/apps/garage/presign, sob AppUserAuthFilter) devolve URLs presignadas curtas (SigV4, 10 min) para GET/PUT. A write-key só é decifrada no servidor para assinar; o device recebe só a URL. Blast-radius de device comprometido = poucas URLs expirando.
  3. Autz por app (I2): o token de usuário carrega oauth_app_id; o filtro o repassa e o corretor exige app_id == oauth_app_id → 403 cross-app. Um JWT de app A não assina recurso de app B.
  4. Assinatura = AWS SDK v2 S3Presigner (software.amazon.awssdk:s3, path-style, endpoint override) — implementação de referência do SigV4, offline (não faz I/O S3).

Alternativas consideradas

  • Entregar a write-key ao app (4.4b): zero round-trip, mas o segredo bucket-wide vive em todo device (extração do APK compromete o bucket até rotacionar). Preterido — só aceitável com key/bucket por-usuário.
  • SigV4 à mão (sem dep): erro-próprio/sensível a segurança. Preterido pela AWS SDK.
  • MinIO SDK: mais leve, mas a AWS SDK v2 é a referência.

Consequências

  • Injeção da write-key no Coolify (server/web) — HABILITADA (2026-08-10, o gancho deferido acima). Para app server/web (tem production_url → recurso Coolify), o broker injeta, além do client-grade, a write-key: GARAGE_ACCESS_KEY_ID + GARAGE_SECRET_ACCESS_KEY (decifrada de secret_ref no ato da injeção, via AesCipher/AUTH_CIPHER_KEY). O app faz S3 direto (1 hop), sem o round-trip de presign. Justificativa: um server é infra confiável (env tão protegido quanto o do auth), então o ganho de segurança do presign — manter o segredo fora de device não-confiável — não se aplica; e a key é per-app, escopada só ao bucket do próprio app (AllowBucketKey, owner:false), então o blast-radius de um server comprometido é o próprio bucket (rotação = re-rodar /xadm-setup + restart). O secret entra só no Coolify (mascarado por is_shown_once), nunca no app.json (que leva só o client-grade público). Mobile/local (sem production_url) não recebe a write-key — para device continua valendo o presign broker-signed (item 2), o segredo fica no auth. Ou seja: presign para device, S3 direto para server — coexistem.
  • Config nova (recurso auth): GARAGE_ADMIN_URL, GARAGE_ADMIN_TOKEN, GARAGE_S3_ENDPOINT, GARAGE_S3_REGION. Ausentes → o provider recusa provisionar (boot ok).
  • Nova superfície runtime /api/apps/** (corretores), guardada pelo AppUserAuthFilter (JWT de usuário, valida via a chave local). O JwtValidator.JwtClaims passou a expor oauth_app_id.
  • GARAGE_ALLOW_WORLD_READABLE_SECRETS=true no cluster permite re-obter o secret (GetKeyInfo ?showSecretKey) na re-provisão — pilar da idempotência da key.