fix(storage): sign presigned URLs for the host browsers reach, so compose uploads work - #1402
Merged
iammukeshm merged 4 commits intoSep 28, 2026
Conversation
…S3:PresignServiceUrl SigV4 binds a presigned URL to the host of the client that signs it, so every presigned PUT/GET pointed at Storage:S3:ServiceUrl. When the API reaches the store on an internal address (compose: http://rustfs:9000) browsers cannot resolve those URLs and uploads fail. The optional PresignServiceUrl gives presigning its own keyed IAmazonS3 built by the same helper (same credentials, region, path style); the presign protocol follows its scheme. All real I/O stays on ServiceUrl. Empty keeps the previous URLs. An invalid value fails at startup (ValidateOnStart).
…stack The api reached RustFS as http://rustfs:9000 and RustFS published no port, so every presigned upload/download URL handed to the admin and dashboard pointed at a host the browser cannot resolve: file uploads were broken in the compose deployment. - publish the RustFS S3 API on FSH_S3_PORT (default 9000); console stays off - RUSTFS_CORS_ALLOWED_ORIGINS = admin + dashboard URLs, as the AppHost does - api: Storage__S3__PresignServiceUrl from the new required FSH_S3_PUBLIC_URL - .env.example, README (fourth proxy subdomain, Host header, troubleshooting) - fsh new writes FSH_S3_PUBLIC_URL=http://localhost:9000 into the generated .env
…artup, not on first upload Uri.TryCreate trims, so " https://s3.example.com " passed startup validation and then failed every presign in the SDK's endpoint parser. The endpoint is now trimmed, and a URL with a path, query or fragment is rejected at startup: the path would be signed into every URL and break behind a proxy. The README now scopes the "skip the API" claim to presigned transfers, and two comments that overstated what the presign client does are corrected.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
iammukeshm
approved these changes
Sep 28, 2026
iammukeshm
left a comment
Member
There was a problem hiding this comment.
Approving the BuildingBlocks/Storage change: it's additive, and an empty PresignServiceUrl gives byte-identical behaviour. A signing-only second client is the right shape, since SigV4 binds the host and a post-sign rewrite can't work. Validating at startup (trimmed, no path) and the red-able tests are well done.
Two notes, not blockers:
- Compose now publishes the RustFS S3 port on all interfaces. Access still needs the keys, but it's more surface than before. I'm tracking a README line telling people to firewall it or bind it to the proxy.
- The public-file preview URL (
BuildPublicUrl→rustfs:9000) is still broken in compose, as you called out. I'm opening a follow-up issue so the visibility/bucket-policy decision gets its own discussion.
Thanks, Marcelo.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1401.
Browsers upload and download files through presigned S3 URLs, and SigV4 binds each URL to the host of the client that signs it.
S3StorageServicesigned with the client it uses for its own I/O, so every URL pointed atStorage:S3:ServiceUrl. In the Docker Compose stack that ishttp://rustfs:9000, which no browser can resolve, so file uploads from the admin and dashboard failed there.What changes
Storage:S3:PresignServiceUrl(new, optional). It names the host that presigned PUT and GET URLs are signed for. A secondIAmazonS3, registered as a keyed singleton (S3StorageService.PresignClientKey), is built by the same helper as the main client, so credentials, region and path style match. The presign protocol follows its scheme. Every other S3 call (put, delete, get, head) stays onServiceUrl. When the setting is empty, both clients useServiceUrland the URLs are the same as before. Signing is offline. Without explicit keys, the only call the presign client makes is the one-time fetch of ambient credentials.http(s)URL with no path, query or fragment. It is checked withValidateOnStart, so a bad value fails at boot and not on the first upload. A path would be signed into every URL and break behind a proxy. The value is trimmed, becauseUri.TryCreateaccepts surrounding whitespace that the SDK's endpoint parser then rejects.FSH_S3_PORT(default9000); the console port stays unpublished.RUSTFS_CORS_ALLOWED_ORIGINSallowsFSH_ADMIN_URLandFSH_DASHBOARD_URL, the same way the AppHost does.Storage__S3__PresignServiceUrlfrom a new requiredFSH_S3_PUBLIC_URL..env.exampleand the compose README cover a fourth proxy hostname, theHostheader rule and a troubleshooting row.fsh newwritesFSH_S3_PUBLIC_URL=http://localhost:9000into the.envit generates.FshWebApplicationFactoryregisters the keyed presign client next to the main one..agents/rules/storage.mddocuments the setting.Breaking for docker-compose users
Compose refuses to start until
FSH_S3_PUBLIC_URLis set indeploy/docker/.env. Host port 9000 must be free, orFSH_S3_PORTmust be set to another port. Behind a TLS proxy, the hostname inFSH_S3_PUBLIC_URLmust reach that port with theHostheader forwarded unchanged; otherwise the store answers403 SignatureDoesNotMatch. Aspire and the AWS Terraform stack need no change. On AWS S3, or any store whose own endpoint browsers can reach, leavePresignServiceUrlempty. A custom test factory that re-registers the S3 stack must also register the keyed client, asFshWebApplicationFactorydoes.This touches
src/BuildingBlocks/Storage. The change there is additive: one option, one keyed registration, and the presign call moved to the new client.Verification
e7acb495):Framework.Testspassed 257/257 andIntegration.Testspassed 765/765 (Testcontainers, Docker).S3PresignEndpointTests(13 cases) cover the endpoint fallback, the scheme, trimming and validation, including a URL with a path, a query and a padded value.PresignServiceUrlTestsruns against a real RustFS container. The API talks to it as127.0.0.1, and presigning is configured forlocalhost. The test checks that the URL carrieslocalhostand that the bytes round-trip through the presigned PUT and GET.Framework.Testsfailed 5 of 257 andPresignServiceUrlTestsfailed.Framework.Testsfailed with 1 test each.localhost:9000:POST /files/upload-urlreturned 200;PUTwent tohttp://localhost:9000and returned 200;POST /files/{id}/finalizereturned 200, and the success toast showed.Not addressed here
After the upload, the admin avatar preview does not render in the compose stack. The Files module stores a durable URL for
Publicfiles, built byBuildPublicUrl(PublicBaseUrl, or elseServiceUrl). In compose that ishttp://rustfs:9000/fsh/..., and the bucket has no anonymous read policy. The compose file says so on purpose. This is how the stack already behaved before this PR, which only fixes the upload itself. The fix needs a design decision: visibility is not encoded in the object key, so a public-read policy on the bucket would expose private files too. I left it out of this PR.Docs: fullstackhero/docs#254 updates Storage and Files and adds the changelog entry.