Presigned URLs
A presigned URL lets anyone who has the link perform one action on one object until it expires, without an Authorization header. Use a presigned GET to share a file from a private bucket, and a presigned PUT to let a client upload a file without ever seeing your bucket key.
hybrids3 uses the AWS Signature V4 query format, so URLs from hybrids3 and URLs from boto3 are interchangeable.
Creating one over HTTP
POST /presign/{bucket}/{key} needs the bucket key or the master key.
| Parameter | Default | Meaning |
|---|---|---|
method | GET | GET to download or PUT to upload. Anything else gets 400 InvalidArgument |
expires | 3600 | Lifetime in seconds, clamped to 1 to 604800 (7 days). A non-integer gets 400 InvalidArgument |
curl -X POST "http://localhost:8080/presign/permanent/doc.pdf?expires=3600" \
-H "Authorization: Bearer perm-secret"
{
"url": "http://localhost:8080/permanent/doc.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=permanent-id%2F20261110%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20261110T071856Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=...",
"method": "GET",
"expires": 3600
}
The URL carries the bucket’s public_key in X-Amz-Credential and an HMAC in X-Amz-Signature. The private key signs it on the server and never appears in it.
GET on a public bucket
A public bucket needs no key to read, so hybrids3 returns a plain URL without a signature or expiry:
curl -X POST "http://localhost:8080/presign/uploads/photo.jpg" \
-H "Authorization: Bearer uploads-secret"
{
"url": "http://localhost:8080/uploads/photo.jpg",
"method": "GET",
"expires": null
}
Presigned PUT
Public buckets allow anonymous reads, never anonymous writes, so a presigned PUT is always signed, also on a public bucket:
curl -X POST "http://localhost:8080/presign/uploads/inbox/report.pdf?method=PUT&expires=600" \
-H "Authorization: Bearer uploads-secret"
The recipient uploads with the URL alone:
curl -X PUT "<url>" --data-binary @report.pdf
The bucket’s max_file_size still applies and an oversized body gets 413. The upload replaces any object already stored under that key.
Creating one over MCP or boto3
The presign_url MCP tool follows the same rules. Pass method="PUT" for an upload URL and always pass base_url, since the MCP server cannot know the address clients use.
boto3 signs URLs on the client with the bucket’s key pair:
get_url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "permanent", "Key": "doc.pdf"},
ExpiresIn=3600,
)
put_url = s3.generate_presigned_url(
"put_object",
Params={"Bucket": "uploads", "Key": "inbox/report.pdf"},
ExpiresIn=600,
)
What a URL allows
- One HTTP method. The method is part of the signature, so a GET URL cannot upload and a PUT URL cannot download.
- One key in one bucket. Changing the path breaks the signature.
- One host. The URL signs the
Hostheader, so it only works when requested with the host it was created for. hybrids3 uses theHostheader of the request that created it; behind a proxy that has to be the public host, which is why the reverse proxy setup passes$http_host. - Until it expires. A GET URL used after
X-Amz-DateplusX-Amz-Expiresgets403 AccessDenied; an expired or altered PUT URL gets404 NoSuchBucket, like any failed write.
There is no way to revoke a single URL. Rotating the bucket key in the configuration and restarting invalidates every URL that key signed.
The 7-day cap applies to URLs that hybrids3 creates. A URL signed by a client that holds the key is checked against its own X-Amz-Expires with no upper limit, and a signed URL without X-Amz-Expires never expires.
Scheme behind TLS
hybrids3 builds the URL from the scheme and host it sees. Behind a TLS proxy it sees plain HTTP unless uvicorn trusts the proxy’s X-Forwarded-Proto header. Behind a reverse proxy shows the setting.