A presigned S3 GET URL for a private customer document turns up in a third party's access logs and is still being fetched. Why can you not simply revoke that one URL, and what actually cuts off access?
answer
- S3 never recorded that the URL exists
- validity is recomputed on every request
- explicit deny beats every allow
- aws:TokenIssueTime kills old sessions
- short expiries make a leak boring
basics
~20 sA presigned URL is a bearer token that S3 never recorded: validity is recomputed from the signature, the expiry and the signer's current permissions on every request. With nothing to revoke individually, you must deny the object, remove the signer's permission, kill the signing session, or move the object.
solid answer
~50 sPresigning is a purely local HMAC operation — S3 is never told a URL was issued, so there is no list to delete from. On each request it recomputes validity from the signature, the deadline and the signing principal's permissions *at that moment*. That last clause is the lever. Immediate options, roughly in order of blast radius: add an explicit `Deny` for that object or prefix to the bucket policy, which beats any allow; remove or scope down the signing role's `s3:GetObject`; revoke the role's active sessions with a policy denying requests whose `aws:TokenIssueTime` predates now; or delete or move the object. Rotating the object's KMS key does not help, since decryption is performed for the signer. Afterwards, fix the design: expiries in minutes, one narrow signing role per use case, and unpredictable keys so an old URL is worthless.
go deeper
Understand that a presigned URL works for anyone who has it until it expires, so it should be treated as a secret and given the shortest useful lifetime.
Explain that S3 recomputes validity per request from signature, expiry and current permissions, which is why there is no per-URL revocation and why policy changes take effect immediately.
Run the incident: explicit deny on the object first, then decide between narrowing the signer, revoking sessions via aws:TokenIssueTime, or moving the object — and weigh each one's blast radius.
Own the standards that make this survivable: minute-scale expiries, per-use-case signing roles, unpredictable keys, re-issue through an authorizing endpoint, and object-level audit logging enabled before it is needed.
## Why there is nothing to revoke Generating a presigned URL makes no API call. The SDK builds a canonical request and HMACs it with credentials it already holds; the URL exists only in your process and then in whoever's hands you put it. S3 learns of it for the first time when someone uses it. At that point it recomputes everything from scratch: does the signature verify against the named credential, is `X-Amz-Date + X-Amz-Expires` still in the future, and — crucially — is this principal *currently* allowed to perform this action on this resource? So a presigned URL is a bearer token with no server-side record and no identifier. "Revoke this URL" is not an operation S3 offers, because there is no object in S3 representing it. Every real remedy works by falsifying one of the three checks above. ## What actually cuts access **Explicit deny in the bucket policy.** The most surgical and fastest lever. An explicit `Deny` on that object key — or the prefix — overrides every allow the signer holds, and takes effect for new requests. This is the standard incident response: deny first, investigate after, remove the deny when the URL has expired. **Remove the signer's permission.** Because authorization is evaluated at request time, detaching or narrowing the policy that granted `s3:GetObject` invalidates every outstanding URL that principal signed for that resource. Blunt: it also kills legitimate URLs signed by the same role, which is precisely why the signing role should be narrow and single-purpose. **Revoke the signing session.** If the URL was signed with temporary credentials, attaching an inline policy to the role that denies all actions when `aws:TokenIssueTime` is earlier than the current instant invalidates every session issued before that point — the mechanism behind the console's "revoke active sessions". Sessions started afterwards are unaffected, so running workloads recover on their next credential refresh. If a long-lived IAM user access key signed it, you deactivate or delete that key, and you now understand why static keys for signing are a bad idea. **Delete or move the object.** A GET for a key that no longer exists returns 404. With versioning enabled, deleting places a delete marker so the current-version GET stops resolving. Effective, but it destroys availability for legitimate readers too, so it is usually the last resort — or the right one when the document should not have existed at that key. **What does not work.** Regenerating the URL with a shorter expiry does nothing; the old signature is still independently valid. Enabling Block Public Access does nothing, because a presigned request is authenticated, not anonymous. Rotating the encryption key does not help either: S3 decrypts on behalf of the signing principal, so as long as that principal can still use the key, the leaked URL still serves plaintext. ## How it leaked Worth naming, because the fix is design, not response. A presigned URL travels in the address bar, so it lands in browser history, in bookmarks, in a screenshot pasted into a ticket, and in any proxy, CDN or load-balancer access log that records full request URIs. Embedding one as the `src` of a resource can send it onward in a `Referer` header. Long expiries multiply every one of these exposures, and a URL that is valid for seven days will outlive the conversation it was shared in. ## Designing so a leak is boring - **Minutes, not days.** Sign for the shortest window the client actually needs. A five-minute URL that leaks into a log has usually expired before anyone reads the log. - **Re-issue instead of extending.** Hand out a durable link into your own service; it authorizes the user on each visit and mints a fresh short URL. Revoking a user's access then genuinely revokes it. - **Unpredictable keys.** Sequential or guessable keys mean one leaked URL teaches the attacker how to ask for the next one. Random identifiers make the leak specific to one object. - **One narrow signing role per use case.** So that the drastic remedies — deny, detach, revoke sessions — have a small blast radius rather than taking down every download in the account. - **Detection.** S3 server access logging and CloudTrail data events for the bucket let you see which objects were fetched, from what address, and how often, which is how you establish scope after the fact. Neither is on by default; the time to enable them is before the incident. ## The answer in an interview "There is no revocation because S3 never recorded the URL — validity is recomputed each request. So I break one of the inputs: an explicit deny on the object is the fastest, removing the signer's permission or revoking its sessions is broader, deleting the object is the last resort. Then I shorten expiries and re-issue from an authenticated endpoint so the next leak is a non-event."
- Someone suggests enabling Block Public Access to shut down the leaked URL. Does it work?No. Block Public Access constrains anonymous and public grants — ACLs and public bucket policies. A presigned request is an authenticated request carrying the signer's credentials, so it is unaffected. Reaching for it here signals a misunderstanding of what the URL is; the working levers are an explicit deny on the object, removing the signer's permission, revoking its sessions, or moving the object.
- The object is encrypted with a customer-managed KMS key. Does rotating or disabling that key stop the leaked URL?Rotating does not — rotation keeps old key material available for decryption, so reads continue. Disabling the key would break the read, but it also breaks every other object encrypted under it, which is far broader than the incident. Handle it at the S3 authorization layer with a targeted deny instead of at the key layer.
- How would you determine how many times the leaked URL was actually used?Through the bucket's own request records: S3 server access logs or CloudTrail data events for that bucket record each object-level GET with the requester, source address and timestamp, so you can count fetches of that key and see whether the traffic pattern is one client or many. Both are opt-in per bucket, so this only works if they were enabled before the leak.
saying these in an interview costs you the question
- Believes AWS keeps a registry of issued presigned URLs
- Thinks regenerating the URL invalidates the leaked one
- Reaches for Block Public Access to stop an authenticated request
- Assumes rotating the KMS key revokes access
- Signs URLs for days because 'the link has to keep working'