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

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

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 credentialsURL 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 profilethe role credentials, about 6 hours
ECS task rolethe 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:signatureAge greater 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:SourceIp is outside your range (public endpoint) or aws:SourceVpc/aws:SourceVpce does 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

SymptomCause → fix
403 AccessDeniedThe 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 expiryThe signing credentials expired (role session, instance profile) or were revoked. Sign with longer-lived credentials
ExpiredTokenThe signer's temporary credentials are no longer valid; refresh them before generating new URLs
SignatureDoesNotMatchClock 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-rangeRange was signed but If-Range was not; add If-Range to the signed headers
A download already in progress at expiryCompletes; a reconnect after expiry fails

Read this document as markdown — the same bytes an agent receives.