← Blog
A cream access ticket with an orange hourglass and a sage-green tab on a pale lavender background.
Published on
2026-10-29

S3 presigned URLs: share private files with temporary access

An S3 presigned URL grants temporary access to a specific Amazon S3 request, such as downloading a private file or uploading an object. The recipient uses the signed link instead of receiving your AWS credentials. Your application still decides who may request that link and which object they may access.

This guide covers GET downloads and PUT uploads for Amazon S3 general-purpose buckets. It includes Python examples, expiry rules, and a small acceptance checklist. The broader Amazon cloud storage guide explains how S3 differs from EBS, EFS, and FSx.

How an S3 presigned URL works

The signed request identifies an operation, bucket, object key, and validity period. S3 evaluates the signature and the signer's authorization when the request arrives. Creating a URL successfully does not prove that S3 will authorize it.

Consider an application that lets a customer download an invoice:

  1. The customer signs in to the application and requests an invoice.
  2. The server checks that this customer may access that invoice.
  3. The server selects the stored object key and generates a short-lived GET URL.
  4. The client downloads the file directly from S3.

Do not let a client substitute an arbitrary bucket or object key without an authorization check. A valid application login alone does not establish permission to read every customer's files. Our application storage guide covers the wider service-selection and access-design decisions.

Prepare a private test object and signing identity

Use a private test bucket and a small, non-sensitive file. Keep public access blocked. Configure an AWS identity with only the object permissions needed for the exercise. AWS's least-privilege guidance applies to presigned requests just as it does to other S3 access.

For the download example, the signing identity needs access to the selected object through s3:GetObject. A request for a specific version uses s3:GetObjectVersion. Objects encrypted with AWS KMS require the applicable key permissions as well. Check the GetObject permission requirements for your configuration.

Install Boto3 in your Python environment. Configure credentials through an approved role, session, or profile in the SDK credential chain; do not put access keys in the script or browser code. The examples deliberately specify Signature Version 4.

Create a presigned URL for a download

Save this as presigned_demo.py. Replace the bucket placeholder, region, and key with your test values. The key must identify an existing object. ExpiresIn=300 requests a five-minute lifetime; temporary credentials or policy restrictions can shorten it.

import boto3
from botocore.config import Config

s3 = boto3.client(
    "s3",
    region_name="us-east-1",
    config=Config(signature_version="s3v4"),
)

download_url = s3.generate_presigned_url(
    "get_object",
    Params={
        "Bucket": "YOUR_PRIVATE_TEST_BUCKET",
        "Key": "presigned-demo/sample.txt",
    },
    ExpiresIn=300,
)
print(download_url)

The Boto3 signing method takes the client method, its parameters, and an expiry in seconds. Run the script in a private terminal, then open the resulting URL in a browser. Keep that output out of shared logs and screenshots. For production, return it only to the authorized client rather than printing it.

If you prefer a console workflow, select the object and use Share with a presigned URL. This creates a download link; it does not make the bucket public.

Create a presigned URL for a file upload

For a PUT upload, the signing identity needs s3:PutObject for the destination. Choose the key on the server. Append the following example to the same script so it reuses the configured s3 client. It assigns a new key and signs the expected content type and a conditional-write header.

from uuid import uuid4

# Reuse the configured s3 client from the download example.
upload_key = f"presigned-demo/uploads/{uuid4()}.txt"
upload_url = s3.generate_presigned_url(
    "put_object",
    Params={
        "Bucket": "YOUR_PRIVATE_TEST_BUCKET",
        "Key": upload_key,
        "ContentType": "text/plain",
        "IfNoneMatch": "*",
    },
    ExpiresIn=300,
)
print(upload_url)
print("Required headers: Content-Type: text/plain; If-None-Match: *")

Send the file with an HTTP PUT client and both headers. This curl command works in a POSIX shell; on Windows PowerShell, use curl.exe if curl resolves to another command. Replace the URL placeholder locally, keeping the quotation marks:

curl --fail-with-body --request PUT --upload-file sample.txt --header "Content-Type: text/plain" --header "If-None-Match: *" "YOUR_PRESIGNED_PUT_URL"

Upload the raw file body, not a multipart form around it. Keep the content-type header identical to the signed value, as required by the S3 upload instructions. Do not change the method, path, or query parameters after signing.

Protect against overwriting an existing object

A regular PutObject request can replace the object at the same key; with versioning enabled, it can create a new current version. A unique server-selected key reduces accidental collisions.

The example also signs If-None-Match: *. This conditional write fails with HTTP 412 when a current object already exists at that key. In a versioned bucket, the condition checks the current version. It does not establish a permanent one-use link: later deletion or a delete marker can change the result.

A signed content type is not proof that an uploaded file is safe or actually has that format. Before making uploads available, validate their contents and apply your application's size, ownership, and malware-handling rules. Use a separate upload area while those checks run.

Understand expiry, reuse, and interrupted downloads

According to the S3 expiry rules, a console-generated link can last up to 12 hours; CLI and SDK links can last up to seven days with suitable credentials. A URL created with temporary credentials expires when those credentials expire, even if a longer duration was requested.

Presigned URLs are reusable bearer credentials. A download started before expiry may continue afterward, but restarting it after expiry can fail. Choose a short duration that still accommodates the user's task. Expiry does not remove a file already downloaded.

For a truly one-time download, design application-level authorization and consumption tracking. Simply generating a presigned URL and marking a database row “used” does not prevent the recipient from reusing that URL while S3 still accepts it.

Handle browser uploads and access controls separately

A browser application calling S3 from another origin may need an appropriate CORS configuration. Allow the required origin, HTTP method, and request headers. For the PUT example, account for Content-Type and If-None-Match. A command-line success does not verify browser behavior.

CORS is not a substitute for authorization. A link recipient using a non-browser client is not limited by the browser's cross-origin checks. Test both the actual browser origin and the underlying S3 permissions.

Protect the signing endpoint itself: authenticate callers, check access to the requested object, limit request rates, and avoid returning unrelated object information. Store an audit record of the requester, operation, approved object, and requested expiry without copying the complete signed URL.

Respond to an exposed link

Logging out of your application does not by itself invalidate an existing S3 URL. If a link is exposed, assess the object access and signing identity with your security owner. Permission changes or credential revocation can affect other clients too. Verify the resulting denial; generating a replacement link does not cancel the original.

Troubleshoot a rejected request

  • SignatureDoesNotMatch: compare the actual HTTP method, region, key, headers, and query string with the signed request. Check clock accuracy and any proxy that rewrites the request.
  • ExpiredToken: refresh the signing session through the approved credential flow, then issue a new link. Increasing the requested expiry cannot revive expired credentials.
  • AccessDenied: examine the signer's object permissions, explicit denies, applicable KMS permissions, and network restrictions.
  • HTTP 412 on the example PUT: check whether the key already has a current object. Do not remove the overwrite protection merely to silence the error.
  • Browser-only failure: inspect the CORS preflight and required headers before changing bucket permissions.

Record the error code and request identifier for diagnosis, while redacting credentials and signatures. Reproduce the request with a small test file before changing application behavior. When policies change, assess their effect on existing clients as well as the new link.

Test the complete workflow before production

The Python examples were checked locally with Python 3.12.14 and Boto3/Botocore 1.43.121 using dummy credentials. The check confirmed SigV4 signing, GET and PUT methods, expiry parameters, and required signed headers. It did not contact AWS or prove that a live bucket would accept the requests.

Run these acceptance checks in your own test environment:

  1. Confirm the intended recipient can download the intended file and that an unauthorized application user cannot obtain its link.
  2. Upload a small file with the required headers, then verify its key, content, and ownership through an authorized account.
  3. Repeat the conditional PUT while that object exists and confirm the expected rejection.
  4. Try a fresh request after expiry and test the temporary-credential lifetime that your application actually uses.
  5. Test the real browser origin, network path, interruption, and retry behavior.
  6. Inspect application logs for signed-URL leakage and remove disposable test data through the normal cleanup process.

Budget for S3 storage, requests, applicable retrievals, and data transfer, plus any application or monitoring services. Repeated use of a valid link can generate repeated requests and transfers. A signed URL changes access, not the service's charging model.

When to choose another sharing approach

Use presigned URLs when an application needs bounded access to a particular object without giving the recipient AWS credentials. If the requirement is permanent public delivery, recipient-specific access checks on every download, or a managed file-sharing experience, evaluate a delivery or application layer designed for that requirement.

For an S3-compatible service, verify its supported operations, signature behavior, expiry, headers, and conditional writes against its own documentation. This Amazon S3 walkthrough does not establish feature support for another provider.

Your next workload belongs on Hivenet.

Pick one AI, compute, or storage workload and see the difference for yourself. Spin it up in minutes, or let our team map your fastest path to production.