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¶
- Provisão idempotente (
GarageProvider):GetBucketInfo?globalAlias=<app_id>-or-CreateBucket→ reusa a key associada ao bucket (GetKeyInfo?showSecretKey=true) ouCreateKey+AllowBucketKey(read+write,owner:false). A idempotência da key usa okeys[]do bucket — sem inventar umListKeys.client_config = {endpoint, bucket, region}(→app.json, público). OsecretAccessKeyé cifrado (AesCipher/AUTH_CIPHER_KEY) emsecret_ref— nunca sai do auth. - Acesso = presign broker-signed (decisão 4.4a): o
GarageBrokerController(GET /api/apps/garage/presign, sobAppUserAuthFilter) 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. - Autz por app (I2): o token de usuário carrega
oauth_app_id; o filtro o repassa e o corretor exigeapp_id == oauth_app_id→ 403 cross-app. Um JWT de app A não assina recurso de app B. - 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 desecret_refno ato da injeção, viaAesCipher/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 poris_shown_once), nunca noapp.json(que leva só o client-grade público). Mobile/local (semproduction_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 peloAppUserAuthFilter(JWT de usuário, valida via a chave local). OJwtValidator.JwtClaimspassou a exporoauth_app_id. GARAGE_ALLOW_WORLD_READABLE_SECRETS=trueno cluster permite re-obter o secret (GetKeyInfo ?showSecretKey) na re-provisão — pilar da idempotência da key.