[{"data":1,"prerenderedAt":1501},["ShallowReactive",2],{"post-\u002Fblog\u002Fpresigned-url-seven-day-ceiling":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"date":10,"author":11,"tags":12,"body":16,"_type":1495,"_id":1496,"_source":1497,"_file":1498,"_stem":1499,"_extension":1500},"\u002Fblog\u002Fpresigned-url-seven-day-ceiling","blog",false,"","Presigned URLs expire at seven days. Then what?","Seven days is not a setting: it is how long the SigV4 signing key lives. What happens at that wall, and the three patterns that outlive it.","2026-10-31","Clement Baranger",[13,14,15],"s3","security","presigned-urls",{"type":17,"children":18,"toc":1486},"root",[19,27,34,76,82,103,113,126,135,140,146,151,353,366,383,408,442,452,458,468,598,609,658,876,881,891,1044,1065,1075,1085,1178,1183,1189,1218,1224,1332,1338,1475,1480],{"type":20,"tag":21,"props":22,"children":23},"element","p",{},[24],{"type":25,"value":26},"text","Every sharing feature hits the same wall. Someone builds a \"copy link\" button, a customer forwards the link in a PDF, and eight days later the download returns a page of XML instead of a file. Nothing was broken by a bug: the link was signed with an expiry date, and that date has a ceiling nobody chose.",{"type":20,"tag":28,"props":29,"children":31},"h2",{"id":30},"the-short-version",[32],{"type":25,"value":33},"The short version",{"type":20,"tag":35,"props":36,"children":37},"ul",{},[38,44,57,71],{"type":20,"tag":39,"props":40,"children":41},"li",{},[42],{"type":25,"value":43},"A presigned URL is an ordinary link with a signature in the query string. Whoever holds it can fetch that one object, with that one HTTP method, until the time written into the link.",{"type":20,"tag":39,"props":45,"children":46},{},[47,49,55],{"type":25,"value":48},"On AWS the maximum is ",{"type":20,"tag":50,"props":51,"children":52},"strong",{},[53],{"type":25,"value":54},"604 800 seconds, seven days",{"type":25,"value":56},". It is not a quota you can raise in a support ticket: the SigV4 signing key is itself only valid for a week.",{"type":20,"tag":39,"props":58,"children":59},{},[60,62,69],{"type":25,"value":61},"Links often die earlier than they claim. Signed with a temporary role, a URL stops working when the session ends — one hour by default for ",{"type":20,"tag":63,"props":64,"children":66},"code",{"className":65},[],[67],{"type":25,"value":68},"AssumeRole",{"type":25,"value":70},". Rotate an access key and every URL signed with it dies at once.",{"type":20,"tag":39,"props":72,"children":73},{},[74],{"type":25,"value":75},"The bill is not bandwidth. It is the re-mint path you did not build: the first customer who clicks on day eight opens a ticket, and someone spends an afternoon proving the file still exists.",{"type":20,"tag":28,"props":77,"children":79},{"id":78},"what-actually-happens",[80],{"type":25,"value":81},"What actually happens",{"type":20,"tag":21,"props":83,"children":84},{},[85,87,93,95,101],{"type":25,"value":86},"Signing is arithmetic, done on the client. The caller builds a canonical request — method, host, path, and a fixed list of ",{"type":20,"tag":63,"props":88,"children":90},{"className":89},[],[91],{"type":25,"value":92},"X-Amz-*",{"type":25,"value":94}," query parameters — then HMACs it with a signing key derived from the secret access key, the date, the region and the service name. The result goes into the URL as ",{"type":20,"tag":63,"props":96,"children":98},{"className":97},[],[99],{"type":25,"value":100},"X-Amz-Signature",{"type":25,"value":102},". When the request arrives, S3 rebuilds the same string and compares.",{"type":20,"tag":21,"props":104,"children":105},{},[106,111],{"type":20,"tag":50,"props":107,"children":108},{},[109],{"type":25,"value":110},"The URL is the only record of its own permission",{"type":25,"value":112},": there is no table of issued links, no API to revoke one. What limits it is the parameters inside it and the credential behind it.",{"type":20,"tag":21,"props":114,"children":115},{},[116,118,124],{"type":25,"value":117},"The seven-day ceiling follows from the signing key, not from policy: AWS states that a presigned URL can be valid for a maximum of seven days because the signing key used in the calculation is valid for up to seven days. ",{"type":20,"tag":63,"props":119,"children":121},{"className":120},[],[122],{"type":25,"value":123},"X-Amz-Expires",{"type":25,"value":125}," takes an integer from 1 to 604 800. The S3 console caps a shared link at 12 hours while the CLI and the SDKs go to 7 days — same object, two answers, depending on where you clicked.",{"type":20,"tag":21,"props":127,"children":128},{},[129],{"type":20,"tag":130,"props":131,"children":134},"img",{"alt":132,"src":133},"Anatomy of a presigned URL: the query string carries X-Amz-Date, X-Amz-Expires capped at 604 800 seconds and the signature, with a timeline showing the four clocks that can end the link and the exact error each one produces","\u002Fdiagrams\u002Fpresigned-url-seven-day-ceiling.svg",[],{"type":20,"tag":21,"props":136,"children":137},{},[138],{"type":25,"value":139},"Expiry is checked when the request starts, not while it runs. AWS documents the edge case: a download that begins just before the deadline keeps streaming after it passes, but a dropped connection retried afterwards fails.",{"type":20,"tag":28,"props":141,"children":143},{"id":142},"where-it-bites",[144],{"type":25,"value":145},"Where it bites",{"type":20,"tag":21,"props":147,"children":148},{},[149],{"type":25,"value":150},"Read the body, not only the status code: a 403 on a presigned URL has four distinct causes.",{"type":20,"tag":152,"props":153,"children":154},"table",{},[155,179],{"type":20,"tag":156,"props":157,"children":158},"thead",{},[159],{"type":20,"tag":160,"props":161,"children":162},"tr",{},[163,169,174],{"type":20,"tag":164,"props":165,"children":166},"th",{},[167],{"type":25,"value":168},"Response",{"type":20,"tag":164,"props":170,"children":171},{},[172],{"type":25,"value":173},"Meaning",{"type":20,"tag":164,"props":175,"children":176},{},[177],{"type":25,"value":178},"Usual cause",{"type":20,"tag":180,"props":181,"children":182},"tbody",{},[183,219,269,297,325],{"type":20,"tag":160,"props":184,"children":185},{},[186,204,209],{"type":20,"tag":187,"props":188,"children":189},"td",{},[190,196,198],{"type":20,"tag":63,"props":191,"children":193},{"className":192},[],[194],{"type":25,"value":195},"400",{"type":25,"value":197}," ",{"type":20,"tag":63,"props":199,"children":201},{"className":200},[],[202],{"type":25,"value":203},"AuthorizationQueryParametersError",{"type":20,"tag":187,"props":205,"children":206},{},[207],{"type":25,"value":208},"The URL is refused before any signature check",{"type":20,"tag":187,"props":210,"children":211},{},[212,217],{"type":20,"tag":63,"props":213,"children":215},{"className":214},[],[216],{"type":25,"value":123},{"type":25,"value":218}," above 604 800 — the SDK accepted it, S3 did not",{"type":20,"tag":160,"props":220,"children":221},{},[222,254,259],{"type":20,"tag":187,"props":223,"children":224},{},[225,231,232,238,240,246,248],{"type":20,"tag":63,"props":226,"children":228},{"className":227},[],[229],{"type":25,"value":230},"403",{"type":25,"value":197},{"type":20,"tag":63,"props":233,"children":235},{"className":234},[],[236],{"type":25,"value":237},"AccessDenied",{"type":25,"value":239},", ",{"type":20,"tag":63,"props":241,"children":243},{"className":242},[],[244],{"type":25,"value":245},"Message",{"type":25,"value":247}," = ",{"type":20,"tag":63,"props":249,"children":251},{"className":250},[],[252],{"type":25,"value":253},"Request has expired",{"type":20,"tag":187,"props":255,"children":256},{},[257],{"type":25,"value":258},"The window closed",{"type":20,"tag":187,"props":260,"children":261},{},[262,264],{"type":25,"value":263},"Anyone opening a link older than its ",{"type":20,"tag":63,"props":265,"children":267},{"className":266},[],[268],{"type":25,"value":123},{"type":20,"tag":160,"props":270,"children":271},{},[272,287,292],{"type":20,"tag":187,"props":273,"children":274},{},[275,280,281],{"type":20,"tag":63,"props":276,"children":278},{"className":277},[],[279],{"type":25,"value":230},{"type":25,"value":197},{"type":20,"tag":63,"props":282,"children":284},{"className":283},[],[285],{"type":25,"value":286},"SignatureDoesNotMatch",{"type":20,"tag":187,"props":288,"children":289},{},[290],{"type":25,"value":291},"Signature recomputed differently",{"type":20,"tag":187,"props":293,"children":294},{},[295],{"type":25,"value":296},"Secret rotated, signed header changed in transit, clock drift on the signing host",{"type":20,"tag":160,"props":298,"children":299},{},[300,315,320],{"type":20,"tag":187,"props":301,"children":302},{},[303,308,309],{"type":20,"tag":63,"props":304,"children":306},{"className":305},[],[307],{"type":25,"value":230},{"type":25,"value":197},{"type":20,"tag":63,"props":310,"children":312},{"className":311},[],[313],{"type":25,"value":314},"InvalidAccessKeyId",{"type":20,"tag":187,"props":316,"children":317},{},[318],{"type":25,"value":319},"The access key no longer exists",{"type":20,"tag":187,"props":321,"children":322},{},[323],{"type":25,"value":324},"Key deleted after the URL was minted",{"type":20,"tag":160,"props":326,"children":327},{},[328,343,348],{"type":20,"tag":187,"props":329,"children":330},{},[331,336,337],{"type":20,"tag":63,"props":332,"children":334},{"className":333},[],[335],{"type":25,"value":230},{"type":25,"value":197},{"type":20,"tag":63,"props":338,"children":340},{"className":339},[],[341],{"type":25,"value":342},"ExpiredRequest",{"type":20,"tag":187,"props":344,"children":345},{},[346],{"type":25,"value":347},"Same as expired",{"type":20,"tag":187,"props":349,"children":350},{},[351],{"type":25,"value":352},"Cloudflare R2's own error code for it",{"type":20,"tag":21,"props":354,"children":355},{},[356,358,364],{"type":25,"value":357},"The error text for the first row is worth memorising, because it appears in logs without context: ",{"type":20,"tag":63,"props":359,"children":361},{"className":360},[],[362],{"type":25,"value":363},"X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds",{"type":25,"value":365},".",{"type":20,"tag":21,"props":367,"children":368},{},[369,374,376,381],{"type":20,"tag":50,"props":370,"children":371},{},[372],{"type":25,"value":373},"The rotation trap.",{"type":25,"value":375}," Access keys get rotated on a 90-day schedule, and nothing in that runbook knows about the links you handed out last Tuesday. Delete the old key and every URL signed with it returns ",{"type":20,"tag":63,"props":377,"children":379},{"className":378},[],[380],{"type":25,"value":314},{"type":25,"value":382},", days before its stated expiry. Keep the old key active until the longest window you ever issue has elapsed — seven days makes deletion safe by construction.",{"type":20,"tag":21,"props":384,"children":385},{},[386,391,393,398,400,406],{"type":20,"tag":50,"props":387,"children":388},{},[389],{"type":25,"value":390},"The temporary credential trap",{"type":25,"value":392}," is the one that surprises people who test in production only once. Presign inside a Lambda, an ECS task or anything assuming a role, and the link inherits the session, not the number you passed. ",{"type":20,"tag":63,"props":394,"children":396},{"className":395},[],[397],{"type":25,"value":68},{"type":25,"value":399}," defaults to a one-hour session; EC2 instance profile credentials rotate on roughly a six-hour cycle; ECS tasks get credentials valid for 1 to 6 hours. Ask for ",{"type":20,"tag":63,"props":401,"children":403},{"className":402},[],[404],{"type":25,"value":405},"ExpiresIn=604800",{"type":25,"value":407}," from a role and you get a URL that dies in an hour.",{"type":20,"tag":21,"props":409,"children":410},{},[411,416,418,424,426,432,434,440],{"type":20,"tag":50,"props":412,"children":413},{},[414],{"type":25,"value":415},"Provider deviation.",{"type":25,"value":417}," The seven-day number is a SigV4 property, but each implementation enforces it its own way. Cloudflare R2 documents an expiry between one second and 604 800 seconds and returns ",{"type":20,"tag":63,"props":419,"children":421},{"className":420},[],[422],{"type":25,"value":423},"403 ExpiredRequest",{"type":25,"value":425}," past it — with a detail worth designing for: R2 does not send CORS headers on that error, so browser JavaScript cannot read the body and your uploader reports a generic network failure instead of \"link expired\". MinIO's ",{"type":20,"tag":63,"props":427,"children":429},{"className":428},[],[430],{"type":25,"value":431},"mc share download",{"type":25,"value":433}," defaults to ",{"type":20,"tag":63,"props":435,"children":437},{"className":436},[],[438],{"type":25,"value":439},"--expire 168h",{"type":25,"value":441}," and its documentation shows a 30-day example, so the ceiling there depends on the deployment, not on the AWS doc. Test the number against the endpoint you actually run.",{"type":20,"tag":21,"props":443,"children":444},{},[445,450],{"type":20,"tag":50,"props":446,"children":447},{},[448],{"type":25,"value":449},"And the security half nobody puts in the ticket.",{"type":25,"value":451}," A presigned URL is a bearer token: no identity, no IP restriction by default, no record of who used it. A seven-day link pasted into a shared ticket is a seven-day password, cancellable only by destroying the credential that signed it.",{"type":20,"tag":28,"props":453,"children":455},{"id":454},"doing-it-right",[456],{"type":25,"value":457},"Doing it right",{"type":20,"tag":21,"props":459,"children":460},{},[461,466],{"type":20,"tag":50,"props":462,"children":463},{},[464],{"type":25,"value":465},"1. Mint per request, not per share.",{"type":25,"value":467}," A link that must work for a month should be minted when it is clicked, not when it is created. Store the object key; sign on demand.",{"type":20,"tag":469,"props":470,"children":474},"pre",{"className":471,"code":472,"language":473,"meta":7,"style":7},"language-python shiki shiki-themes github-light","# GET \u002Ffiles\u002F\u003Cid>\u002Flink — behind your own authentication\nobj = files.get(id, owner=current_user)          # your entitlement check\n\nurl = s3.generate_presigned_url(\n    \"get_object\",\n    Params={\n        \"Bucket\": \"docs-prod\",\n        \"Key\": obj.s3_key,\n        \"ResponseContentDisposition\": f'attachment; filename=\"{obj.filename}\"',\n    },\n    ExpiresIn=900,      # 15 minutes\n)\nreturn redirect(url, status_code=302)\n","python",[475],{"type":20,"tag":63,"props":476,"children":477},{"__ignoreMap":7},[478,489,498,508,517,526,535,544,553,562,571,580,589],{"type":20,"tag":479,"props":480,"children":483},"span",{"class":481,"line":482},"line",1,[484],{"type":20,"tag":479,"props":485,"children":486},{},[487],{"type":25,"value":488},"# GET \u002Ffiles\u002F\u003Cid>\u002Flink — behind your own authentication\n",{"type":20,"tag":479,"props":490,"children":492},{"class":481,"line":491},2,[493],{"type":20,"tag":479,"props":494,"children":495},{},[496],{"type":25,"value":497},"obj = files.get(id, owner=current_user)          # your entitlement check\n",{"type":20,"tag":479,"props":499,"children":501},{"class":481,"line":500},3,[502],{"type":20,"tag":479,"props":503,"children":505},{"emptyLinePlaceholder":504},true,[506],{"type":25,"value":507},"\n",{"type":20,"tag":479,"props":509,"children":511},{"class":481,"line":510},4,[512],{"type":20,"tag":479,"props":513,"children":514},{},[515],{"type":25,"value":516},"url = s3.generate_presigned_url(\n",{"type":20,"tag":479,"props":518,"children":520},{"class":481,"line":519},5,[521],{"type":20,"tag":479,"props":522,"children":523},{},[524],{"type":25,"value":525},"    \"get_object\",\n",{"type":20,"tag":479,"props":527,"children":529},{"class":481,"line":528},6,[530],{"type":20,"tag":479,"props":531,"children":532},{},[533],{"type":25,"value":534},"    Params={\n",{"type":20,"tag":479,"props":536,"children":538},{"class":481,"line":537},7,[539],{"type":20,"tag":479,"props":540,"children":541},{},[542],{"type":25,"value":543},"        \"Bucket\": \"docs-prod\",\n",{"type":20,"tag":479,"props":545,"children":547},{"class":481,"line":546},8,[548],{"type":20,"tag":479,"props":549,"children":550},{},[551],{"type":25,"value":552},"        \"Key\": obj.s3_key,\n",{"type":20,"tag":479,"props":554,"children":556},{"class":481,"line":555},9,[557],{"type":20,"tag":479,"props":558,"children":559},{},[560],{"type":25,"value":561},"        \"ResponseContentDisposition\": f'attachment; filename=\"{obj.filename}\"',\n",{"type":20,"tag":479,"props":563,"children":565},{"class":481,"line":564},10,[566],{"type":20,"tag":479,"props":567,"children":568},{},[569],{"type":25,"value":570},"    },\n",{"type":20,"tag":479,"props":572,"children":574},{"class":481,"line":573},11,[575],{"type":20,"tag":479,"props":576,"children":577},{},[578],{"type":25,"value":579},"    ExpiresIn=900,      # 15 minutes\n",{"type":20,"tag":479,"props":581,"children":583},{"class":481,"line":582},12,[584],{"type":20,"tag":479,"props":585,"children":586},{},[587],{"type":25,"value":588},")\n",{"type":20,"tag":479,"props":590,"children":592},{"class":481,"line":591},13,[593],{"type":20,"tag":479,"props":594,"children":595},{},[596],{"type":25,"value":597},"return redirect(url, status_code=302)\n",{"type":20,"tag":21,"props":599,"children":600},{},[601,607],{"type":20,"tag":63,"props":602,"children":604},{"className":603},[],[605],{"type":25,"value":606},"ResponseContentDisposition",{"type":25,"value":608}," is signed along with the request, so the filename a customer sees cannot be altered by editing the URL.",{"type":20,"tag":21,"props":610,"children":611},{},[612,617,619,625,627,633,635,640,642,648,650,657],{"type":20,"tag":50,"props":613,"children":614},{},[615],{"type":25,"value":616},"2. Cap the age in the bucket, not in the caller.",{"type":25,"value":618}," A bucket policy ",{"type":20,"tag":63,"props":620,"children":622},{"className":621},[],[623],{"type":25,"value":624},"Deny",{"type":25,"value":626}," on ",{"type":20,"tag":63,"props":628,"children":630},{"className":629},[],[631],{"type":25,"value":632},"s3:signatureAge",{"type":25,"value":634}," rejects any SigV4 presigned request older than a number of seconds you set, whatever the URL claims. A ",{"type":20,"tag":63,"props":636,"children":638},{"className":637},[],[639],{"type":25,"value":624},{"type":25,"value":641}," always wins over an ",{"type":20,"tag":63,"props":643,"children":645},{"className":644},[],[646],{"type":25,"value":647},"Allow",{"type":25,"value":649}," — the evaluation order behind that is covered in ",{"type":20,"tag":651,"props":652,"children":654},"a",{"href":653},"\u002Fblog\u002Fbucket-policy-allow-still-denied",[655],{"type":25,"value":656},"When two policies allow and the request still fails",{"type":25,"value":365},{"type":20,"tag":469,"props":659,"children":663},{"className":660,"code":661,"language":662,"meta":7,"style":7},"language-json shiki shiki-themes github-light","{\n  \"Version\": \"2012-10-17\",\n  \"Statement\": [{\n    \"Sid\": \"RejectStaleSignatures\",\n    \"Effect\": \"Deny\",\n    \"Principal\": \"*\",\n    \"Action\": \"s3:GetObject\",\n    \"Resource\": \"arn:aws:s3:::docs-prod\u002F*\",\n    \"Condition\": { \"NumericGreaterThan\": { \"s3:signatureAge\": \"900\" } }\n  }]\n}\n","json",[664],{"type":20,"tag":63,"props":665,"children":666},{"__ignoreMap":7},[667,676,701,714,735,756,777,798,819,860,868],{"type":20,"tag":479,"props":668,"children":669},{"class":481,"line":482},[670],{"type":20,"tag":479,"props":671,"children":673},{"style":672},"--shiki-default:#24292E",[674],{"type":25,"value":675},"{\n",{"type":20,"tag":479,"props":677,"children":678},{"class":481,"line":491},[679,685,690,696],{"type":20,"tag":479,"props":680,"children":682},{"style":681},"--shiki-default:#005CC5",[683],{"type":25,"value":684},"  \"Version\"",{"type":20,"tag":479,"props":686,"children":687},{"style":672},[688],{"type":25,"value":689},": ",{"type":20,"tag":479,"props":691,"children":693},{"style":692},"--shiki-default:#032F62",[694],{"type":25,"value":695},"\"2012-10-17\"",{"type":20,"tag":479,"props":697,"children":698},{"style":672},[699],{"type":25,"value":700},",\n",{"type":20,"tag":479,"props":702,"children":703},{"class":481,"line":500},[704,709],{"type":20,"tag":479,"props":705,"children":706},{"style":681},[707],{"type":25,"value":708},"  \"Statement\"",{"type":20,"tag":479,"props":710,"children":711},{"style":672},[712],{"type":25,"value":713},": [{\n",{"type":20,"tag":479,"props":715,"children":716},{"class":481,"line":510},[717,722,726,731],{"type":20,"tag":479,"props":718,"children":719},{"style":681},[720],{"type":25,"value":721},"    \"Sid\"",{"type":20,"tag":479,"props":723,"children":724},{"style":672},[725],{"type":25,"value":689},{"type":20,"tag":479,"props":727,"children":728},{"style":692},[729],{"type":25,"value":730},"\"RejectStaleSignatures\"",{"type":20,"tag":479,"props":732,"children":733},{"style":672},[734],{"type":25,"value":700},{"type":20,"tag":479,"props":736,"children":737},{"class":481,"line":519},[738,743,747,752],{"type":20,"tag":479,"props":739,"children":740},{"style":681},[741],{"type":25,"value":742},"    \"Effect\"",{"type":20,"tag":479,"props":744,"children":745},{"style":672},[746],{"type":25,"value":689},{"type":20,"tag":479,"props":748,"children":749},{"style":692},[750],{"type":25,"value":751},"\"Deny\"",{"type":20,"tag":479,"props":753,"children":754},{"style":672},[755],{"type":25,"value":700},{"type":20,"tag":479,"props":757,"children":758},{"class":481,"line":528},[759,764,768,773],{"type":20,"tag":479,"props":760,"children":761},{"style":681},[762],{"type":25,"value":763},"    \"Principal\"",{"type":20,"tag":479,"props":765,"children":766},{"style":672},[767],{"type":25,"value":689},{"type":20,"tag":479,"props":769,"children":770},{"style":692},[771],{"type":25,"value":772},"\"*\"",{"type":20,"tag":479,"props":774,"children":775},{"style":672},[776],{"type":25,"value":700},{"type":20,"tag":479,"props":778,"children":779},{"class":481,"line":537},[780,785,789,794],{"type":20,"tag":479,"props":781,"children":782},{"style":681},[783],{"type":25,"value":784},"    \"Action\"",{"type":20,"tag":479,"props":786,"children":787},{"style":672},[788],{"type":25,"value":689},{"type":20,"tag":479,"props":790,"children":791},{"style":692},[792],{"type":25,"value":793},"\"s3:GetObject\"",{"type":20,"tag":479,"props":795,"children":796},{"style":672},[797],{"type":25,"value":700},{"type":20,"tag":479,"props":799,"children":800},{"class":481,"line":546},[801,806,810,815],{"type":20,"tag":479,"props":802,"children":803},{"style":681},[804],{"type":25,"value":805},"    \"Resource\"",{"type":20,"tag":479,"props":807,"children":808},{"style":672},[809],{"type":25,"value":689},{"type":20,"tag":479,"props":811,"children":812},{"style":692},[813],{"type":25,"value":814},"\"arn:aws:s3:::docs-prod\u002F*\"",{"type":20,"tag":479,"props":816,"children":817},{"style":672},[818],{"type":25,"value":700},{"type":20,"tag":479,"props":820,"children":821},{"class":481,"line":555},[822,827,832,837,841,846,850,855],{"type":20,"tag":479,"props":823,"children":824},{"style":681},[825],{"type":25,"value":826},"    \"Condition\"",{"type":20,"tag":479,"props":828,"children":829},{"style":672},[830],{"type":25,"value":831},": { ",{"type":20,"tag":479,"props":833,"children":834},{"style":681},[835],{"type":25,"value":836},"\"NumericGreaterThan\"",{"type":20,"tag":479,"props":838,"children":839},{"style":672},[840],{"type":25,"value":831},{"type":20,"tag":479,"props":842,"children":843},{"style":681},[844],{"type":25,"value":845},"\"s3:signatureAge\"",{"type":20,"tag":479,"props":847,"children":848},{"style":672},[849],{"type":25,"value":689},{"type":20,"tag":479,"props":851,"children":852},{"style":692},[853],{"type":25,"value":854},"\"900\"",{"type":20,"tag":479,"props":856,"children":857},{"style":672},[858],{"type":25,"value":859}," } }\n",{"type":20,"tag":479,"props":861,"children":862},{"class":481,"line":564},[863],{"type":20,"tag":479,"props":864,"children":865},{"style":672},[866],{"type":25,"value":867},"  }]\n",{"type":20,"tag":479,"props":869,"children":870},{"class":481,"line":573},[871],{"type":20,"tag":479,"props":872,"children":873},{"style":672},[874],{"type":25,"value":875},"}\n",{"type":20,"tag":21,"props":877,"children":878},{},[879],{"type":25,"value":880},"Deploy that and intentional links longer than 15 minutes stop working — which is the point. Accidental ones become impossible.",{"type":20,"tag":21,"props":882,"children":883},{},[884,889],{"type":20,"tag":50,"props":885,"children":886},{},[887],{"type":25,"value":888},"3. Read the window before you ship the URL.",{"type":25,"value":890}," Decode what you issued, not what you asked for.",{"type":20,"tag":469,"props":892,"children":896},{"className":893,"code":894,"language":895,"meta":7,"style":7},"language-bash shiki shiki-themes github-light","aws s3 presign s3:\u002F\u002Fdocs-prod\u002Fcontracts\u002Facme.pdf --expires-in 900 --region eu-west-3 > \u002Ftmp\u002Fu.txt\ngrep -o 'X-Amz-Date=[0-9TZ]*' \u002Ftmp\u002Fu.txt\ngrep -o 'X-Amz-Expires=[0-9]*' \u002Ftmp\u002Fu.txt\ncurl -sS -o \u002Fdev\u002Fnull -w '%{http_code}\\n' \"$(cat \u002Ftmp\u002Fu.txt)\"\n","bash",[897],{"type":20,"tag":63,"props":898,"children":899},{"__ignoreMap":7},[900,955,977,997],{"type":20,"tag":479,"props":901,"children":902},{"class":481,"line":482},[903,909,914,919,924,929,934,939,944,950],{"type":20,"tag":479,"props":904,"children":906},{"style":905},"--shiki-default:#6F42C1",[907],{"type":25,"value":908},"aws",{"type":20,"tag":479,"props":910,"children":911},{"style":692},[912],{"type":25,"value":913}," s3",{"type":20,"tag":479,"props":915,"children":916},{"style":692},[917],{"type":25,"value":918}," presign",{"type":20,"tag":479,"props":920,"children":921},{"style":692},[922],{"type":25,"value":923}," s3:\u002F\u002Fdocs-prod\u002Fcontracts\u002Facme.pdf",{"type":20,"tag":479,"props":925,"children":926},{"style":681},[927],{"type":25,"value":928}," --expires-in",{"type":20,"tag":479,"props":930,"children":931},{"style":681},[932],{"type":25,"value":933}," 900",{"type":20,"tag":479,"props":935,"children":936},{"style":681},[937],{"type":25,"value":938}," --region",{"type":20,"tag":479,"props":940,"children":941},{"style":692},[942],{"type":25,"value":943}," eu-west-3",{"type":20,"tag":479,"props":945,"children":947},{"style":946},"--shiki-default:#D73A49",[948],{"type":25,"value":949}," >",{"type":20,"tag":479,"props":951,"children":952},{"style":692},[953],{"type":25,"value":954}," \u002Ftmp\u002Fu.txt\n",{"type":20,"tag":479,"props":956,"children":957},{"class":481,"line":491},[958,963,968,973],{"type":20,"tag":479,"props":959,"children":960},{"style":905},[961],{"type":25,"value":962},"grep",{"type":20,"tag":479,"props":964,"children":965},{"style":681},[966],{"type":25,"value":967}," -o",{"type":20,"tag":479,"props":969,"children":970},{"style":692},[971],{"type":25,"value":972}," 'X-Amz-Date=[0-9TZ]*'",{"type":20,"tag":479,"props":974,"children":975},{"style":692},[976],{"type":25,"value":954},{"type":20,"tag":479,"props":978,"children":979},{"class":481,"line":500},[980,984,988,993],{"type":20,"tag":479,"props":981,"children":982},{"style":905},[983],{"type":25,"value":962},{"type":20,"tag":479,"props":985,"children":986},{"style":681},[987],{"type":25,"value":967},{"type":20,"tag":479,"props":989,"children":990},{"style":692},[991],{"type":25,"value":992}," 'X-Amz-Expires=[0-9]*'",{"type":20,"tag":479,"props":994,"children":995},{"style":692},[996],{"type":25,"value":954},{"type":20,"tag":479,"props":998,"children":999},{"class":481,"line":510},[1000,1005,1010,1014,1019,1024,1029,1034,1039],{"type":20,"tag":479,"props":1001,"children":1002},{"style":905},[1003],{"type":25,"value":1004},"curl",{"type":20,"tag":479,"props":1006,"children":1007},{"style":681},[1008],{"type":25,"value":1009}," -sS",{"type":20,"tag":479,"props":1011,"children":1012},{"style":681},[1013],{"type":25,"value":967},{"type":20,"tag":479,"props":1015,"children":1016},{"style":692},[1017],{"type":25,"value":1018}," \u002Fdev\u002Fnull",{"type":20,"tag":479,"props":1020,"children":1021},{"style":681},[1022],{"type":25,"value":1023}," -w",{"type":20,"tag":479,"props":1025,"children":1026},{"style":692},[1027],{"type":25,"value":1028}," '%{http_code}\\n'",{"type":20,"tag":479,"props":1030,"children":1031},{"style":692},[1032],{"type":25,"value":1033}," \"$(",{"type":20,"tag":479,"props":1035,"children":1036},{"style":905},[1037],{"type":25,"value":1038},"cat",{"type":20,"tag":479,"props":1040,"children":1041},{"style":692},[1042],{"type":25,"value":1043}," \u002Ftmp\u002Fu.txt)\"\n",{"type":20,"tag":21,"props":1045,"children":1046},{},[1047,1049,1055,1057,1063],{"type":25,"value":1048},"Then read the failure, not just the code. ",{"type":20,"tag":63,"props":1050,"children":1052},{"className":1051},[],[1053],{"type":25,"value":1054},"curl -sS \"$(cat \u002Ftmp\u002Fu.txt)\" | head -8",{"type":25,"value":1056}," prints the ",{"type":20,"tag":63,"props":1058,"children":1060},{"className":1059},[],[1061],{"type":25,"value":1062},"\u003CExpires>",{"type":25,"value":1064}," element, which is the only thing that tells expiry apart from a permission change.",{"type":20,"tag":21,"props":1066,"children":1067},{},[1068,1073],{"type":20,"tag":50,"props":1069,"children":1070},{},[1071],{"type":25,"value":1072},"4. Rotate with the links in mind.",{"type":25,"value":1074}," Create the new key, deploy it, and leave the old one active for seven days — the longest any live URL can outlive it. Any shorter overlap breaks links you cannot enumerate.",{"type":20,"tag":21,"props":1076,"children":1077},{},[1078,1083],{"type":20,"tag":50,"props":1079,"children":1080},{},[1081],{"type":25,"value":1082},"5. When a link genuinely must live for months, stop using S3 presigning.",{"type":25,"value":1084}," Serve private content through CloudFront with origin access control and use CloudFront signed URLs: they are signed with a CloudFront key pair (RSA 2048 or ECDSA 256) and a policy whose expiry date you write, so the seven-day SigV4 signing-key limit does not apply. The bucket stays private.",{"type":20,"tag":469,"props":1086,"children":1088},{"className":893,"code":1087,"language":895,"meta":7,"style":7},"aws cloudfront sign \\\n  --url \"https:\u002F\u002Fcdn.example.com\u002Fcontracts\u002Facme.pdf\" \\\n  --key-pair-id K2JCJMDEHXQW5F \\\n  --private-key file:\u002F\u002Fcf-signing-key.pem \\\n  --date-less-than 2030-01-01\n",[1089],{"type":20,"tag":63,"props":1090,"children":1091},{"__ignoreMap":7},[1092,1114,1131,1148,1165],{"type":20,"tag":479,"props":1093,"children":1094},{"class":481,"line":482},[1095,1099,1104,1109],{"type":20,"tag":479,"props":1096,"children":1097},{"style":905},[1098],{"type":25,"value":908},{"type":20,"tag":479,"props":1100,"children":1101},{"style":692},[1102],{"type":25,"value":1103}," cloudfront",{"type":20,"tag":479,"props":1105,"children":1106},{"style":692},[1107],{"type":25,"value":1108}," sign",{"type":20,"tag":479,"props":1110,"children":1111},{"style":681},[1112],{"type":25,"value":1113}," \\\n",{"type":20,"tag":479,"props":1115,"children":1116},{"class":481,"line":491},[1117,1122,1127],{"type":20,"tag":479,"props":1118,"children":1119},{"style":681},[1120],{"type":25,"value":1121},"  --url",{"type":20,"tag":479,"props":1123,"children":1124},{"style":692},[1125],{"type":25,"value":1126}," \"https:\u002F\u002Fcdn.example.com\u002Fcontracts\u002Facme.pdf\"",{"type":20,"tag":479,"props":1128,"children":1129},{"style":681},[1130],{"type":25,"value":1113},{"type":20,"tag":479,"props":1132,"children":1133},{"class":481,"line":500},[1134,1139,1144],{"type":20,"tag":479,"props":1135,"children":1136},{"style":681},[1137],{"type":25,"value":1138},"  --key-pair-id",{"type":20,"tag":479,"props":1140,"children":1141},{"style":692},[1142],{"type":25,"value":1143}," K2JCJMDEHXQW5F",{"type":20,"tag":479,"props":1145,"children":1146},{"style":681},[1147],{"type":25,"value":1113},{"type":20,"tag":479,"props":1149,"children":1150},{"class":481,"line":510},[1151,1156,1161],{"type":20,"tag":479,"props":1152,"children":1153},{"style":681},[1154],{"type":25,"value":1155},"  --private-key",{"type":20,"tag":479,"props":1157,"children":1158},{"style":692},[1159],{"type":25,"value":1160}," file:\u002F\u002Fcf-signing-key.pem",{"type":20,"tag":479,"props":1162,"children":1163},{"style":681},[1164],{"type":25,"value":1113},{"type":20,"tag":479,"props":1166,"children":1167},{"class":481,"line":519},[1168,1173],{"type":20,"tag":479,"props":1169,"children":1170},{"style":681},[1171],{"type":25,"value":1172},"  --date-less-than",{"type":20,"tag":479,"props":1174,"children":1175},{"style":692},[1176],{"type":25,"value":1177}," 2030-01-01\n",{"type":20,"tag":21,"props":1179,"children":1180},{},[1181],{"type":25,"value":1182},"Use a canned policy for one file, a custom policy when you need an IP range or a start date.",{"type":20,"tag":28,"props":1184,"children":1186},{"id":1185},"how-s3-explorer-fits",[1187],{"type":25,"value":1188},"How S3 Explorer fits",{"type":20,"tag":21,"props":1190,"children":1191},{},[1192,1194,1199,1201,1206,1208,1216],{"type":25,"value":1193},"Presigning happens on your machine; what you debug at 2 a.m. is which key signed, against which endpoint, for which object. S3 Explorer's connection form takes a display name, endpoint, region, path-style toggle and credentials, and ",{"type":20,"tag":50,"props":1195,"children":1196},{},[1197],{"type":25,"value":1198},"Test connection",{"type":25,"value":1200}," reports how many buckets those credentials can actually see before you save the profile — the fastest way to tell \"the link expired\" from \"the key lost its permission\". The ",{"type":20,"tag":50,"props":1202,"children":1203},{},[1204],{"type":25,"value":1205},"per-request metrics dashboard",{"type":25,"value":1207}," counts the session's API calls by category with an error rate, so a wall of 403s from a revoked key shows up as a number. The Metadata and Versions tabs give you the exact key, endpoint and version a signature has to match. ",{"type":20,"tag":651,"props":1209,"children":1213},{"href":1210,"rel":1211},"https:\u002F\u002Fs3explorer.accessdevops.com",[1212],"nofollow",[1214],{"type":25,"value":1215},"S3 Explorer",{"type":25,"value":1217}," runs on Windows, macOS and Linux.",{"type":20,"tag":28,"props":1219,"children":1221},{"id":1220},"checklist",[1222],{"type":25,"value":1223},"Checklist",{"type":20,"tag":35,"props":1225,"children":1228},{"className":1226},[1227],"contains-task-list",[1229,1257,1266,1287,1296,1305,1314,1323],{"type":20,"tag":39,"props":1230,"children":1233},{"className":1231},[1232],"task-list-item",[1234,1239,1241,1247,1249,1255],{"type":20,"tag":1235,"props":1236,"children":1238},"input",{"disabled":504,"type":1237},"checkbox",[],{"type":25,"value":1240}," Every ",{"type":20,"tag":63,"props":1242,"children":1244},{"className":1243},[],[1245],{"type":25,"value":1246},"generate_presigned_url",{"type":25,"value":1248}," call in the codebase passes an explicit ",{"type":20,"tag":63,"props":1250,"children":1252},{"className":1251},[],[1253],{"type":25,"value":1254},"ExpiresIn",{"type":25,"value":1256},", and none exceeds 604 800.",{"type":20,"tag":39,"props":1258,"children":1260},{"className":1259},[1232],[1261,1264],{"type":20,"tag":1235,"props":1262,"children":1263},{"disabled":504,"type":1237},[],{"type":25,"value":1265}," No presigned URL is written to a database, an email template, a CRM field or a PDF; the object key is what you persist.",{"type":20,"tag":39,"props":1267,"children":1269},{"className":1268},[1232],[1270,1273,1274,1279,1280,1285],{"type":20,"tag":1235,"props":1271,"children":1272},{"disabled":504,"type":1237},[],{"type":25,"value":618},{"type":20,"tag":63,"props":1275,"children":1277},{"className":1276},[],[1278],{"type":25,"value":624},{"type":25,"value":626},{"type":20,"tag":63,"props":1281,"children":1283},{"className":1282},[],[1284],{"type":25,"value":632},{"type":25,"value":1286}," caps signature age independently of what the SDK asked for.",{"type":20,"tag":39,"props":1288,"children":1290},{"className":1289},[1232],[1291,1294],{"type":20,"tag":1235,"props":1292,"children":1293},{"disabled":504,"type":1237},[],{"type":25,"value":1295}," The credentials that sign are dedicated to signing, not a human's personal access key.",{"type":20,"tag":39,"props":1297,"children":1299},{"className":1298},[1232],[1300,1303],{"type":20,"tag":1235,"props":1301,"children":1302},{"disabled":504,"type":1237},[],{"type":25,"value":1304}," You have replayed an expired link and read the XML body, so you know which of the five 403s you get.",{"type":20,"tag":39,"props":1306,"children":1308},{"className":1307},[1232],[1309,1312],{"type":20,"tag":1235,"props":1310,"children":1311},{"disabled":504,"type":1237},[],{"type":25,"value":1313}," The key-rotation runbook keeps the old secret alive longer than the longest URL lifetime you issue.",{"type":20,"tag":39,"props":1315,"children":1317},{"className":1316},[1232],[1318,1321],{"type":20,"tag":1235,"props":1319,"children":1320},{"disabled":504,"type":1237},[],{"type":25,"value":1322}," Links that must outlive a week are minted at click time or served by CloudFront signed URLs.",{"type":20,"tag":39,"props":1324,"children":1326},{"className":1325},[1232],[1327,1330],{"type":20,"tag":1235,"props":1328,"children":1329},{"disabled":504,"type":1237},[],{"type":25,"value":1331}," Your monitoring counts 403 responses by error code, not as a single \"forbidden\" bucket.",{"type":20,"tag":28,"props":1333,"children":1335},{"id":1334},"sources",[1336],{"type":25,"value":1337},"Sources",{"type":20,"tag":35,"props":1339,"children":1340},{},[1341,1360,1378,1398,1426,1445,1463],{"type":20,"tag":39,"props":1342,"children":1343},{},[1344,1351,1353,1358],{"type":20,"tag":651,"props":1345,"children":1348},{"href":1346,"rel":1347},"https:\u002F\u002Fdocs.aws.amazon.com\u002FAmazonS3\u002Flatest\u002Fuserguide\u002Fusing-presigned-url.html",[1212],[1349],{"type":25,"value":1350},"Amazon S3 — Download and upload objects with presigned URLs",{"type":25,"value":1352},": credential types, the 1-minute-to-12-hour console limit, the 7-day SDK limit, the ",{"type":20,"tag":63,"props":1354,"children":1356},{"className":1355},[],[1357],{"type":25,"value":632},{"type":25,"value":1359}," example.",{"type":20,"tag":39,"props":1361,"children":1362},{},[1363,1370,1371,1376],{"type":20,"tag":651,"props":1364,"children":1367},{"href":1365,"rel":1366},"https:\u002F\u002Fdocs.aws.amazon.com\u002FAmazonS3\u002Flatest\u002Fdeveloperguide\u002Fsigv4-query-string-auth.html",[1212],[1368],{"type":25,"value":1369},"Amazon S3 — Authenticating requests with query parameters",{"type":25,"value":689},{"type":20,"tag":63,"props":1372,"children":1374},{"className":1373},[],[1375],{"type":25,"value":123},{"type":25,"value":1377}," minimum 1, maximum 604 800 exactly because the signing key lives seven days.",{"type":20,"tag":39,"props":1379,"children":1380},{},[1381,1388,1390,1396],{"type":20,"tag":651,"props":1382,"children":1385},{"href":1383,"rel":1384},"https:\u002F\u002Frepost.aws\u002Fknowledge-center\u002Frequest-has-expired-s3-object",[1212],[1386],{"type":25,"value":1387},"AWS re:Post — Resolve \"Request has expired\"",{"type":25,"value":1389},": the exact ",{"type":20,"tag":63,"props":1391,"children":1393},{"className":1392},[],[1394],{"type":25,"value":1395},"\u003CError>",{"type":25,"value":1397}," body returned by an expired link.",{"type":20,"tag":39,"props":1399,"children":1400},{},[1401,1408,1410,1417,1419,1424],{"type":20,"tag":651,"props":1402,"children":1405},{"href":1403,"rel":1404},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fr2\u002Fapi\u002Fs3\u002Fpresigned-urls\u002F",[1212],[1406],{"type":25,"value":1407},"Cloudflare R2 — Presigned URLs",{"type":25,"value":1409}," and ",{"type":20,"tag":651,"props":1411,"children":1414},{"href":1412,"rel":1413},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fr2\u002Fbuckets\u002Fcors\u002F",[1212],[1415],{"type":25,"value":1416},"R2 CORS",{"type":25,"value":1418},": the 1-second-to-7-day range and the ",{"type":20,"tag":63,"props":1420,"children":1422},{"className":1421},[],[1423],{"type":25,"value":342},{"type":25,"value":1425}," behaviour without CORS headers.",{"type":20,"tag":39,"props":1427,"children":1428},{},[1429,1436,1437,1443],{"type":20,"tag":651,"props":1430,"children":1433},{"href":1431,"rel":1432},"https:\u002F\u002Fwww.backblaze.com\u002Fapidocs\u002Fb2-get-download-authorization",[1212],[1434],{"type":25,"value":1435},"Backblaze B2 — b2_get_download_authorization",{"type":25,"value":689},{"type":20,"tag":63,"props":1438,"children":1440},{"className":1439},[],[1441],{"type":25,"value":1442},"validDurationInSeconds",{"type":25,"value":1444}," maximum 604 800.",{"type":20,"tag":39,"props":1446,"children":1447},{},[1448,1455,1457,1462],{"type":20,"tag":651,"props":1449,"children":1452},{"href":1450,"rel":1451},"https:\u002F\u002Fmin.io\u002Fdocs\u002Fminio\u002Flinux\u002Freference\u002Fminio-mc\u002Fmc-share-download.html",[1212],[1453],{"type":25,"value":1454},"MinIO — mc share download",{"type":25,"value":1456},": default ",{"type":20,"tag":63,"props":1458,"children":1460},{"className":1459},[],[1461],{"type":25,"value":439},{"type":25,"value":365},{"type":20,"tag":39,"props":1464,"children":1465},{},[1466,1473],{"type":20,"tag":651,"props":1467,"children":1470},{"href":1468,"rel":1469},"https:\u002F\u002Fdocs.aws.amazon.com\u002FAmazonCloudFront\u002Flatest\u002FDeveloperGuide\u002Fprivate-content-signed-urls.html",[1212],[1471],{"type":25,"value":1472},"Amazon CloudFront — Use signed URLs",{"type":25,"value":1474},": key groups, canned versus custom policy, expiration set in the policy.",{"type":20,"tag":21,"props":1476,"children":1477},{},[1478],{"type":25,"value":1479},"Prices and limits read from vendor documentation on 31 October 2026; error strings quoted, not paraphrased.",{"type":20,"tag":1481,"props":1482,"children":1483},"style",{},[1484],{"type":25,"value":1485},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":491,"depth":491,"links":1487},[1488,1489,1490,1491,1492,1493,1494],{"id":30,"depth":491,"text":33},{"id":78,"depth":491,"text":81},{"id":142,"depth":491,"text":145},{"id":454,"depth":491,"text":457},{"id":1185,"depth":491,"text":1188},{"id":1220,"depth":491,"text":1223},{"id":1334,"depth":491,"text":1337},"markdown","content:blog:presigned-url-seven-day-ceiling.md","content","blog\u002Fpresigned-url-seven-day-ceiling.md","blog\u002Fpresigned-url-seven-day-ceiling","md",1793445419962]