Build1 publisher3 min readPublished
Revoking one leaked S3 presigned URL takes down every URL its credential signed
AWS lets an S3 presigned URL live up to seven days, and anyone holding a leaked one can use it until it expires or its signer is revoked. Revoking the signer kills every URL it made, so short expiries and a bucket-level cap on signature age are the practical defence.
The Engineer · Build desk

What happened
- The AWS CLI and SDKs default a presigned URL's expiry to 3,600 seconds and accept up to 604,800 seconds, which is seven days.
- Anyone holding a presigned URL can perform its single GET or PUT operation without an AWS account of their own.
- A presigned URL stops working when its signing credential is revoked, deleted or deactivated, even if the requested expiry is later.
- Role-session URLs die when the session ends; STS AssumeRole sessions default to one hour and EC2 instance-profile credentials last at most about six hours.
- A leaked URL cannot be rotated, so stopping it early means revoking the signing credential along with every other URL that credential issued.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost Stopping one leaked link breaks every legitimate link from the same key, so the number of URLs per signing principal sets how many recipients lose access.
- decision A 'seven-day' link feature built in a Lambda role delivers roughly the session length, so the expiry a team configures is not the expiry its recipients get.
- capability A bucket policy on s3:signatureAge lets the bucket owner cap every presigned URL's useful life, whatever --expires-in each caller passes.
The one-hour default is sensible. The ceiling is 168 times longer [4][7], and the example command in a dev.to post on presigned-URL hardening asks for all of it with `--expires-in 604800` [19]. The S3 console stops at 12 hours [5]. A week-long link exists only when someone passes a number near the maximum [4]. The post, which says every number in it comes from AWS's own documentation [17], warns that setting seven days just in case gives a stolen link a week of life [18].
The link is the whole credential. It carries X-Amz-Algorithm, X-Amz-Credential, X-Amz-Expires and X-Amz-Signature as parameters, signed under SigV4, and it performs one operation within the signer's permissions [3][2]. The leak paths the post lists are all places a string gets copied: Slack, committed repos, request logs and client-side JavaScript [13].
The stated expiry is an upper bound. Credential lifetime sets the real one, and the seven-day ceiling holds only for long-term IAM user credentials [6][8]. The post treats the short life of role-signed URLs as a trap, because a seven-day URL minted in a Lambda under a role may stop working in under an hour [10]. I think it is the best property the design has. A role-signed link that leaks dies with the session [9].
The post's advice pulls two ways here. It lists signing with permanent keys among the regrets [12]. For long-lived links it then recommends a dedicated IAM user whose key you can rotate, or a service that re-issues the URL on demand [11]. In my context, an application handing downloads to authenticated users, I would build the re-issue service. The signer stays a role session. The post recommends the same pattern for any URL bound for a browser: generate it on the server at request time and hand it over through a short-lived endpoint [15].
A permanent key also raises the cost of an incident. Pulling it to stop one leaked link invalidates every link it signed [14]. The post's mitigations make that traceable: record which principal signed each URL, and log a hash or the object key, never the full URL [11][15].
The control I like most sits on the bucket. A policy using the `s3:signatureAge` condition key rejects presigned requests whose signature is older than a set age, and the post's example uses 10 minutes [16]. The limit belongs to the bucket owner, so a caller who passes 604800 still gets about 10 minutes of use [16][4]. For that number to work in another team's bucket, every consumer has to tolerate links that short. The post carves out one exception, a long unattended transfer where the recipient polls over days, and even there it prefers a refresh loop to one long timeout [20].
The post also names plaintext HTTP and the absence of a network lock among its regrets [12]. Its available text ends at the signature-age example, before it describes controls for either.
What to watch
- An AWS change to the 604,800-second ceiling for SigV4 URLs signed with IAM user keys, or to the console's 12-hour limit.
- The rest of the post's guidance on network locks and HTTPS; bucket-level conditions there would sit beside s3:signatureAge as policy the bucket owner controls.