S3 Presigned URLs: Grant Temporary, Secure File Access in 60 Seconds

You have private files locked in an S3 bucket, but you need to give a specific user temporary download access — without exposing your AWS credentials, making the bucket public, or building a proxy download endpoint. This is exactly the problem S3 Presigned URLs solve.

TL;DR

ConcernAnswer
What is it?A time-limited, cryptographically signed URL granting access to a private S3 object
Who signs it?Your backend server using IAM credentials (never the client)
Expiration range1 second to 7 days (604800 seconds max for SigV4)
Required IAM permissions3:GetObject on the target bucket/key
Core FixUse generate_presigned_url from boto3 with ExpiresIn=3600

How Presigned URLs Work: The Data Flow

The signing happens entirely on your server. The URL itself encodes the expiration, the target object, and a cryptographic signature derived from your IAM credentials. S3 validates this signature on every request — no session, no cookie, no backend proxy needed.

sequenceDiagram participant Client as Client (Browser/App) participant Backend as Your Backend Server participant IAM as AWS IAM (SigV4) participant S3 as Amazon S3 Client->>Backend: Request download link for file.pdf Backend->>IAM: Sign URL using IAM credentials IAM-->>Backend: Signed URL with expiry + signature Backend-->>Client: Return presigned URL (expires in 1hr) Client->>S3: GET presigned URL directly S3->>S3: Validate signature + check expiry S3-->>Client: Stream file.pdf (200 OK)

The Analogy: A Valet Parking Ticket

Think of a presigned URL like a valet parking ticket. The hotel (your backend) issues a ticket stamped with an expiry time. The valet (S3) accepts the ticket and hands over the car (file) without needing to call the hotel again — but only if the ticket is still valid and hasn't been tampered with. Once it expires, it's worthless. The guest never needs a key to the entire garage.

IAM: Minimum Required Permissions

The IAM role or user your backend uses to generate the URL must have s3:GetObject on the specific bucket and key prefix. Nothing more.

🔽 [Click to expand] IAM Policy: Least-Privilege for Presigned URL Generation
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPresignedUrlGeneration",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-private-bucket/uploads/*"
    }
  ]
}

Critical note: The bucket policy must NOT block public access overrides for the object itself — the presigned URL bypasses bucket-level public access blocks only if the signing identity has explicit s3:GetObject permission. If you use S3 Block Public Access at the account level, presigned URLs still work because they authenticate via IAM, not anonymous access.

Implementation: Generate a Presigned URL (Python / boto3)

This is the canonical pattern. Run this on your backend — Lambda, EC2, ECS, anywhere your IAM role is attached.

💻 [Click to expand] Python (boto3): Generate Presigned URL with 1-Hour Expiry
import boto3
from botocore.exceptions import ClientError

def generate_presigned_download_url(
    bucket_name: str,
    object_key: str,
    expiration_seconds: int = 3600  # 1 hour
) -> str | None:
    """
    Generate a presigned URL to download a private S3 object.

    :param bucket_name: Name of the S3 bucket
    :param object_key:  Full S3 key (e.g., 'uploads/reports/q1.pdf')
    :param expiration_seconds: URL validity window in seconds (default: 3600)
    :return: Presigned URL string, or None on failure
    """
    s3_client = boto3.client(
        's3',
        region_name='us-east-1'  # Must match bucket region for SigV4
    )

    try:
        url = s3_client.generate_presigned_url(
            ClientMethod='get_object',
            Params={
                'Bucket': bucket_name,
                'Key': object_key,
            },
            ExpiresIn=expiration_seconds,
            HttpMethod='GET'
        )
        return url
    except ClientError as e:
        print(f"[ERROR] Failed to generate presigned URL: {e}")
        return None


# --- Usage ---
if __name__ == '__main__':
    presigned_url = generate_presigned_download_url(
        bucket_name='your-private-bucket',
        object_key='uploads/reports/q1.pdf'
    )

    if presigned_url:
        print(f"Share this URL (valid for 1 hour):\n{presigned_url}")

What the Signed URL Actually Contains

Decoding a presigned URL reveals the embedded parameters that S3 uses to validate every incoming request:

https://your-private-bucket.s3.amazonaws.com/uploads/reports/q1.pdf
  ?X-Amz-Algorithm=AWS4-HMAC-SHA256
  &X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20240115%2Fus-east-1%2Fs3%2Faws4_request
  &X-Amz-Date=20240115T120000Z
  &X-Amz-Expires=3600
  &X-Amz-SignedHeaders=host
  &X-Amz-Signature=<computed_hmac_sha256>
  • X-Amz-Expires: Seconds from X-Amz-Date until the URL is invalid
  • X-Amz-Credential: Encodes the signing identity, date, region, and service
  • X-Amz-Signature: HMAC-SHA256 of the canonical request — any tampering invalidates it

Node.js Implementation (AWS SDK v3)

💻 [Click to expand] Node.js (AWS SDK v3): Presigned URL with 1-Hour Expiry
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3Client = new S3Client({ region: 'us-east-1' });

async function generatePresignedDownloadUrl(
  bucketName: string,
  objectKey: string,
  expiresInSeconds: number = 3600
): Promise<string> {
  const command = new GetObjectCommand({
    Bucket: bucketName,
    Key: objectKey,
  });

  const signedUrl = await getSignedUrl(s3Client, command, {
    expiresIn: expiresInSeconds,
  });

  return signedUrl;
}

// Usage
const url = await generatePresignedDownloadUrl(
  'your-private-bucket',
  'uploads/reports/q1.pdf'
);
console.log('Presigned URL:', url);

Common Pitfalls & How to Avoid Them

PitfallRoot CauseFix
SignatureDoesNotMatchClock skew between your server and AWS (>5 min)Sync server time via NTP; use EC2/Lambda (auto-synced)
URL expires immediatelyGenerating with temporary STS credentials that expire before the URLEnsure STS token TTL > URL expiration window
AccessDenied on valid URLSigning identity lacks s3:GetObjectAttach the least-privilege IAM policy shown above
URL works in browser but fails in curlSpecial characters in object key not URL-encodedURL-encode the key before passing to generate_presigned_url
Max expiry exceededRequesting >604800s (7 days) with SigV4Cap ExpiresIn at 604800; redesign flow if longer access needed

Architecture: Where Presigned URL Generation Lives

Browser / Mobile AppYour Backend ServicePrivate S3 BucketIAM RoleREST Endpointboto3 / SDKprivate-file.pdfs3:GetObject only Generate URL request1. Request download link2. Sign with attached role3. Signed URL (1hr TTL)4. Return presigned URL5. GET presigned URL directly6. Stream file (200 OK)

Wrap-up & Next Steps

Presigned URLs are the correct, zero-infrastructure solution for granting temporary, auditable access to private S3 objects — your backend signs, S3 enforces, and the client downloads directly without any proxy overhead.

Next Steps:

  • For uploads (not just downloads), use ClientMethod='put_object' with the same pattern to generate presigned PUT URLs.
  • To audit access, enable S3 Server Access Logging or AWS CloudTrail Data Events on your bucket.
  • For large-scale distribution, consider pairing presigned URLs with CloudFront signed URLs for CDN-level caching and geo-restriction.
  • 📖 Official Docs: AWS — Sharing objects with presigned URLs

Glossary

  • Presigned URL: A time-limited URL containing embedded AWS credentials and a cryptographic signature, granting temporary access to a specific S3 object without requiring the requester to have AWS credentials.
  • SigV4 (Signature Version 4): AWS's request signing protocol that uses HMAC-SHA256 to authenticate API calls; all presigned URLs use SigV4 by default.
  • IAM Least Privilege: The security principle of granting only the minimum permissions required for a task — here, only s3:GetObject on the specific key prefix.
  • STS (Security Token Service): AWS service that issues temporary credentials; if your signing identity uses STS tokens, the presigned URL cannot outlive the token's expiry.
  • Canonical Request: A standardized string representation of an HTTP request (method, URI, headers, payload hash) that SigV4 signs to produce the final request signature.

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

Lambda Infinite Loop with S3: How to Break the Recursive Trigger Cycle

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User