Share or accept an object with a presigned URL
Generate a time-limited GET or PUT URL from the CLI or TypeScript, sign it with credentials that outlive the URL, restrict how it can be used, and fix the usual failures (403, SignatureDoesNotMatch, ExpiredToken, early expiry).
markdownguidev4WN6G-MPYQstage: buildtrust: officialservice: s3interface: cliinterface: sdkpresigned URLaws s3 presigngetSignedUrls3-request-presignerupload URLexpiresIns3:signatureAgeSignatureDoesNotMatchExpiredToken
A presigned URL lets anyone holding it make one specific request (method, bucket, key) as the signer, until it expires. It needs no bucket policy change. Two rules cause most failures: the signer must itself be allowed the operation, and the URL dies when the signer's credentials die, whatever expiry you set.
1. Pick credentials that outlive the URL
| Signing credentials | URL valid for at most |
|---|---|
| IAM user access keys (Signature Version 4) | 7 days |
| Assumed-role session (AssumeRole) | the session, 1 hour by default |
| EC2 instance profile | the role credentials, about 6 hours |
| ECS task role | the role credentials, rotated every 1–6 hours |
| Console "Share with a presigned URL" | 12 hours |
Revoking, deleting or deactivating the signing credentials also ends the URL early. For URLs meant to last days, sign with credentials that last that long, and accept the trade-off of long-term keys deliberately.
The signer needs the matching S3 permission (s3:GetObject for downloads, s3:PutObject for uploads) and, for SSE-KMS buckets, the KMS permission too.
2. Generate it
CLI (download only; up to 7 days):
aws s3 presign s3://<bucket>/<key> --expires-in 3600
For Regions launched after 20 March 2019, also pass --region <region> --endpoint-url https://s3.<region>.amazonaws.com.
TypeScript, AWS SDK for JavaScript v3:
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ region: "<bucket region>" });
const downloadUrl = await getSignedUrl(s3,
new GetObjectCommand({ Bucket: "<bucket>", Key: "<key>" }), { expiresIn: 900 });
const uploadUrl = await getSignedUrl(s3,
new PutObjectCommand({ Bucket: "<bucket>", Key: "<key>", ContentType: "image/png" }), { expiresIn: 900 });
The holder then uses the exact method: curl "<downloadUrl>" -o file, or curl -X PUT -H "Content-Type: image/png" --upload-file file "<uploadUrl>". Anything signed into the URL (method, headers such as content type) must be sent exactly as signed.
What an upload URL changes: it creates or overwrites the object at that key, as many times as it is used before it expires. URLs are bearer tokens: treat them like passwords, keep expiries short, and generate one per object.
3. Restrict how it can be used
- Signature age: a bucket-policy Deny on
s3:signatureAgegreater than a number of milliseconds (for example 600000 for 10 minutes) refuses old URLs regardless of the expiry chosen by the signer. - Network path: a Deny on the signer's identity when
aws:SourceIpis outside your range (public endpoint) oraws:SourceVpc/aws:SourceVpcedoes not match (VPC endpoint) limits where any of its URLs can be used. - Checksums: SigV4 URLs support additional checksum algorithms for upload integrity; include the checksum header the URL was signed with.
4. When it fails
| Symptom | Cause → fix |
|---|---|
403 AccessDenied | The signer lacks the S3 or KMS permission, a bucket-policy Deny applies, or the signer's credentials have expired. Test the same call as the signer (guides/s3/fix-403.md) |
| URL stops working earlier than its expiry | The signing credentials expired (role session, instance profile) or were revoked. Sign with longer-lived credentials |
ExpiredToken | The signer's temporary credentials are no longer valid; refresh them before generating new URLs |
SignatureDoesNotMatch | Clock drift on the signing machine; a proxy rewriting headers or the query string; method, headers or query differ from what was signed |
AccessDenied with HeadersNotSigned: if-range | Range was signed but If-Range was not; add If-Range to the signed headers |
| A download already in progress at expiry | Completes; a reconnect after expiry fails |