← Back to list

Generating Signed URLs for S3-Compatible Buckets

Secure file sharing shouldn’t force you to open up an entire bucket. Whenever I need to hand a teammate, customer, or build system…

PI in Neural Engineer · 2025-11-08 10:28 · 0 claps · 4.9 min read paywalled
#s3 #presigned-url #software-engineering #software #cybersecurity
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity 🥊 · Combat Sports

Generating Signed URLs for S3-Compatible Buckets

Secure file sharing shouldn’t force you to open up an entire bucket. Whenever I need to hand a teammate, customer, or build system temporary access to a specific object, I reach for signed URLs. They give me the flexibility of S3 while keeping access scoped, time-bound, and auditable. In this guide I’ll explain what signed URLs are, why they work, and how to generate them end-to-end using the MinIO Python SDK — complete with local testing using s3ninja so you can practice without touching production.

What Is a Signed URL?

A signed URL is a pre-authenticated link that encapsulates a storage request (GET, PUT, DELETE) plus a short-lived authorization token. Instead of making a bucket public or sharing the access and secret keys, I sign the request with my own credentials, attach an expiration timestamp, and hand off that link. Anyone with the URL can perform the single permitted action until the timer expires, without requiring AWS keys.

Security Benefits of Signed URLs

  • No exposed credentials: The frontend never receives my IAM keys. Clients only see the temporary URL, eliminating credential leaks in browser storage or mobile apps.
  • Limited scope: Every link targets one object, one HTTP verb, and one expiry window. Intercepting a URL doesn’t grant wider access.
  • Time-based control: Expiry windows can be seconds or days. When the clock runs out, the storage service returns AccessDenied.
  • Full audit trail: Requests still pass through S3 or MinIO logs, allowing me to trace usage without granting persistent permissions.
  • Stateless consumption: Any HTTP client — from curl to mobile SDKs—can use the link. No custom agents required.

How Signed URLs Are Generated

Behind the scenes, the SDK or CLI performs a few steps:

  1. Canonical request: Build a string representing the HTTP verb, resource path, query parameters, and headers.
  2. String to sign: Combine the canonical request, signing algorithm identifier (for S3 SigV4), timestamp, and credential scope (date/region/service).
  3. Signature: Derive a signing key from your secret access key and use it to HMAC the string to be signed. The result becomes the X-Amz-Signature query parameter.
  4. Assemble URL: Append the access key (X-Amz-Credential), timestamp (X-Amz-Date), expiration (X-Amz-Expires), and the signature to the object URL.

Because the signature covers all request parameters plus the expiry, any tampering or late requests fail authentication. That strong binding is why I trust signed URLs to gate production artifacts and AI datasets.

Prerequisites

  • Python 3.8 or newer
  • Local MinIO server or AWS S3 bucket
  • Optional: s3ninja Docker container for rapid mocking
  • minio SDK (pip install minio)
  • Access key and secret key with permissions to the target bucket

Spinning Up a Local MinIO Server

MinIO is my go-to S3-compatible server for local development, edge deployments, and AI pipelines. To bootstrap it locally:

  1. Download the latest binary for your platform from [https://dl.min.io/server/minio/release/.](https://dl.min.io/server/minio/release/.)
  2. Start a single-node server:
mkdir ./data 
minio server ./data

Access details:

Why the MinIO Python SDK?

The MinIO SDK is lightweight, dependency-free, and compatible with AWS S3, MinIO, or any other compatible endpoint. I like it for its:

  • Familiar Pythonic surface area.
  • Straightforward configuration for custom endpoints or TLS setups.
  • Built-in helpers like presigned_get_object and presigned_put_object.

Python Walkthrough: Create a Bucket and Upload a File

I typically start by creating a bucket and seeding a test file so there’s something to share later.

from minio import Minio

client = Minio(
    endpoint="localhost:9000",
    access_key="minioadmin",
    secret_key="minioadmin",
    secure=False
)
bucket_name="mock-binaries1"
destination_file = "my-test-file.txt"
# Create bucket
try:
    found = client.bucket_exists(bucket_name=bucket_name)
    if not found:
            client.make_bucket(bucket_name=bucket_name)
            print("Created bucket", bucket_name)
    else:
            print("Bucket", bucket_name, "already exists")
except Exception as e:
    #client.make_bucket(bucket_name=bucket_name)
    print("bucket does not exist", e)
    pass

buckets = client.list_buckets()
for bucket in buckets:
    print(bucket.name, bucket.creation_date)

# Upload a test file
with open("message3.txt", "w", encoding="utf-8") as f:
    f.write("Hello MinIO!")
client.fput_object(bucket_name=bucket_name, object_name=destination_file, file_path="message3.txt")
print(
         "successfully uploaded as object",
        destination_file, "to bucket", bucket_name,
    )

Generate a Signed URL for Downloads

The example below signs a GET request that expires in 15 seconds (short for quick demos).

object_name = destination_file
expiry = timedelta(seconds=15)

try:
    # Get presigned URL for downloading
    get_url = client.presigned_get_object(
        bucket_name,
        object_name,
        expires=expiry,
        response_headers={"response-content-disposition": "attachment"},
    )
    print("Temporary download link:", get_url)

I test the link with curl. As long as I download within the 15-second window, the request succeeds; afterward I get a 403.

curl -o a.txt "http://localhost:9000/mock-binaries1/my-test-file.txt?response-content-disposition=attachment&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20251107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251107T213846Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=0294203ae94080368dd5a2d3a970e5e4fc93c13bba92be982855defd6d174692"

After expiry:

<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Request has expired</Message>
<Key>my-test-file.txt</Key>
<BucketName>mock-binaries1</BucketName>
<Resource>/mock-binaries1/my-test-file.txt</Resource>
<RequestId>1875D7641FF251C8</RequestId>
<HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId>
</Error>

Put Object Using a Presigned URL

Uploads follow the same pattern. I generate a PUT URL whenever I need a browser or automation job to drop a file into my bucket without exposing long-lived credentials.

from datetime import timedelta

put_url = client.presigned_put_object(
    bucket_name,
    "upload-test.txt",
    expires=timedelta(minutes=15)
)
print("Temporary upload link:", put_url)

You can test the upload using curl:

# Create a test file
echo "Testing presigned PUT" > upload.txt

# Upload using the presigned URL
curl -X PUT -T upload.txt \
  "http://localhost:9000/mock-binaries1/upload-test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minioadmin%2F20251107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251107T213846Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=..."

-T uploads the file. Once the timer expires, the same URL stops accepting uploads.

Architecture Design

In production, I keep credential handling inside the backend service, never in the browser. The flow I recommend looks like this:

  1. Authenticate the user with the front-end’s identity provider (OIDC, SAML, or custom auth). Attach RBAC context — team, project, or data classification labels — to the session token.
  2. Call a backend signer service that runs inside a trusted network segment. Only this service stores the S3/MinIO access key and secret; the frontend never sees them.
  3. Validate authorization inside the signer. Check the user’s RBAC claims and map them to application-specific ACL rules (for example, data scientists may read models but not upload logs).
  4. Generate the signed URL with the MinIO SDK, setting the appropriate HTTP method, expiry, and optional response headers. If the ACL requires read-only access, issue a GET URL; for uploads, issue a short-lived PUT.
  5. Return the URL to the client. The frontend uses it immediately to download or upload without storing any long-lived credentials.

Because the backend enforces RBAC before minting each URL, access control stays centralized. If you ever rotate the access key or tighten an ACL, the change takes effect on the next signing request with no UI updates required.

Best Practices

  • Keep expiry short: A Few minutes are usually enough. Regenerate if the recipient needs more time.
  • Monitor logs: CloudTrail or MinIO audit logs show when signed URLs are used; investigate unexpected spikes.
  • Content-type constraints: For uploads, you often lock Content-Type and Content-MD5 in the signature to prevent clients from overwriting data with unexpected formats; this is worth explaining.
  • Revocation Strategy: Since you can’t invalidate a URL after issuance, the only recourse is to rotate credentials or shorten the TTL.
  • Rotate keys: Access keys should rotate regularly. A rotation invalidates existing signed URLs, so plan accordingly.
  • Use HTTPS endpoints: Even though the URL is signed, plaintext HTTP transport leaks bucket names and object keys; always use HTTPS endpoints.

Wrapping Up

Signed URLs enable me to share artifacts, ML models, and logs without modifying bucket policies. We examined the cryptographic flow behind them, set up MinIO locally, generated download and upload links using the Python SDK, and even simulated S3 with s3ninja for safe testing.

Thanks for reading .Subscribe or follow to receive new posts and support my work. Share your experiences in the comments below


메타데이터
post_id
9d06a97baecd
slug
generating-signed-urls-for-s3-compatible-buckets-9d06a97baecd
url
https://blog1.neuralengineer.org/generating-signed-urls-for-s3-compatible-buckets-9d06a97baecd
canonical_url
https://blog1.neuralengineer.org/generating-signed-urls-for-s3-compatible-buckets-9d06a97baecd
author_url
https://medium.com/@pi45757
status
ok
fetched_at
2026-06-09 15:37:30