feat: add signed bind-backed user icon uploads

This commit is contained in:
2026-08-06 15:40:55 +00:00
parent a0e1271c88
commit 9046a131f2
12 changed files with 331 additions and 17 deletions
+26
View File
@@ -134,6 +134,32 @@ Never give either caller `image_admin_secret`, which also authorizes explicit
branch changes. This fallback handles webhook delivery outages; if the image
service itself is stopped, restore it and run the caller command again.
### Short-lived user-icon uploads
Core and Core2026 can store validated account icons in this repository through
`PUT /v1/uploads/user-icons/<client>/<random-32-hex>.<extension>`. Each game
server validates the authenticated user and image first, then sends the raw
image body with `X-Image-Client`, `X-Image-Expires`, `X-Image-Request-Id`, and
`X-Image-Signature` headers.
The signature is HMAC-SHA256 over
`expires.requestId.pathname.contentType.sha256(body)`. Expiry may be at most
five minutes in the future, so a grant cannot be reused for another path,
content type, body, or later upload. The service also checks the image magic,
caller scope, and request replay before it writes one immutable file below the
host bind directory `runtime-data/uploads`. User uploads are deliberately not
added to Git; Nginx exposes that bind read-only at `/icons/users/`.
Create separate upload secrets with `deploy/scripts/init-secrets.sh`. Mount only
the matching `image_upload_core_secret` or `image_upload_core2026_secret` on the
game server. The shared secrets stay server-side in Docker secrets; they are
not returned to browsers or forwarded to Cloudflare.
Run `deploy/scripts/init-secrets.sh` before the first Compose start so
`runtime-data/uploads` exists with permissions that allow the hook container to
write and the Nginx container to read. Back up this directory independently of
the Git repository when moving servers.
Legacy HTTP mutation is disabled by default. An emergency PHP rollback must
first stop `image-hook`, then create the ignored `hook/legacy-enabled` sentinel
in the legacy checkout before restoring its Caddy/Gitea route. Remove the