How to Generate an S3 Presigned URL for Temporary Private File Access

When you need to grant a user time-limited, read-only access to a private S3 object — without exposing your AWS credentials or making the bucket public — a Presigned URL is the correct tool. It encodes your authorization directly into a signed, expiring URL that anyone can use, no AWS account required.

TL;DR

AspectDetail
What it doesGrants temporary HTTP access to a private S3 object
Who signs itYour backend using IAM credentials (user or role)
Expiration range1 second to 7 days (604800 seconds max for SigV4)
SDK methodgenerate_presigned_url (Python) / getSignedUrl or GetObjectCommand (Node.js v3)
Required IAM permissions3:GetObject on the target object/prefix
No bucket policy change neededBucket stays private; the signature carries the authorization

How It Works: The Architecture

A Presigned URL is not a proxy — it is a cryptographically signed request URL. When your backend generates it, AWS embeds the signing credentials, expiration timestamp, and request parameters into the URL itself using Signature Version 4 (SigV4). The client then presents this URL directly to S3, which re-computes the signature and validates it before serving the object.

sequenceDiagram participant C as "Client
(Browser / App)" participant B as "Your Backend
(API Server)" participant SDK as "AWS SDK
(Local Signing)" participant S3 as "Amazon S3" C->>B: GET /download-link?file=report.pdf B->>SDK: generate_presigned_url(bucket, key, ExpiresIn=3600) Note over SDK: Cryptographic signing only.
No network call to S3. SDK-->>B: Signed URL (valid 1 hour) B-->>C: { "url": "https://s3.amazonaws.com/...?X-Amz-Signature=..." } C->>S3: HTTP GET (Presigned URL) Note over S3: Validates SigV4 signature
and X-Amz-Expires timestamp. S3-->>C: 200 OK — File stream
  1. Client requests a download link from your backend (e.g., a REST API endpoint).
  2. Backend calls the AWS SDK using its IAM credentials to generate a presigned URL — no network call to S3 is made at this point; it is a local cryptographic operation.
  3. Backend returns the URL to the client. The URL contains the bucket, key, expiration, and SigV4 signature as query parameters.
  4. Client sends a direct HTTP GET to S3 using the presigned URL.
  5. S3 validates the signature and expiration. If valid, it streams the object directly to the client. Your backend is not in the data path.
Analogy: Think of a Presigned URL like a signed valet ticket. The hotel (your backend) issues a ticket with your signature and an expiry time. The valet (S3) checks the signature and timestamp — if both are valid, they hand over the car (the file). The guest never needs a key to the entire garage.

IAM: Least-Privilege Setup

The IAM principal (user or role) generating the presigned URL must have s3:GetObject permission on the target resource. Grant access to a specific prefix, not the entire bucket.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPresignedDownload",
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-private-bucket/downloads/*"
    }
  ]
}

Critical note: If the signing principal is an IAM Role with a session token (e.g., an EC2 instance profile or Lambda execution role), the presigned URL will include the session token. The URL will become invalid if the session expires before the URL's own expiration. Always set your URL expiration shorter than the role's session duration.

Implementation

Python (boto3)

🔽 Click to expand — Python boto3 example
import boto3
from botocore.exceptions import ClientError

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

    :param bucket_name: Name of the S3 bucket
    :param object_key:  S3 object key (path within the bucket)
    :param expiration_seconds: URL validity in seconds (default: 1 hour)
    :return: Presigned URL string, or None on failure
    """
    s3_client = boto3.client("s3", region_name="us-east-1")

    try:
        url = s3_client.generate_presigned_url(
            ClientMethod="get_object",
            Params={
                "Bucket": bucket_name,
                "Key": object_key,
            },
            ExpiresIn=expiration_seconds,
        )
        return url
    except ClientError as e:
        print(f"Error generating presigned URL: {e}")
        return None


# --- Usage ---
if __name__ == "__main__":
    presigned_url = generate_presigned_download_url(
        bucket_name="your-private-bucket",
        object_key="downloads/report-2024-q4.pdf",
        expiration_seconds=3600,  # 1 hour
    )
    if presigned_url:
        print(f"Share this URL (valid for 1 hour):\n{presigned_url}")

Node.js (AWS SDK v3)

🔽 Click to expand — Node.js AWS SDK v3 example
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,
  expirationSeconds: number = 3600
): Promise {
  const command = new GetObjectCommand({
    Bucket: bucketName,
    Key: objectKey,
  });

  const signedUrl = await getSignedUrl(s3Client, command, {
    expiresIn: expirationSeconds, // 1 hour
  });

  return signedUrl;
}

// --- Usage ---
(async () => {
  const url = await generatePresignedDownloadUrl(
    "your-private-bucket",
    "downloads/report-2024-q4.pdf",
    3600
  );
  console.log(`Share this URL (valid for 1 hour):\n${url}`);
})();

AWS CLI (Quick Test)

aws s3 presign s3://your-private-bucket/downloads/report-2024-q4.pdf \
  --expires-in 3600

What the Presigned URL Looks Like

The generated URL encodes all authorization as query parameters — no custom headers required from the client:

https://your-private-bucket.s3.us-east-1.amazonaws.com/downloads/report-2024-q4.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-signature>

Key Behavioral Gotchas

ScenarioBehavior
URL used after expirationS3 returns 403 AccessDenied with Request has expired
Object deleted after URL generationS3 returns 404 NoSuchKey
IAM role session expires before URLURL becomes invalid immediately upon session expiry
Bucket policy explicitly denies s3:GetObjectPresigned URL is rejected — explicit deny overrides the signature
Max expiration with SigV4604,800 seconds (7 days); longer values are rejected by S3

Wrap-Up & Next Steps

Presigned URLs are the standard, secure pattern for delegating temporary object access without modifying bucket policies or exposing credentials. For production use, consider:

  • Logging access: Enable S3 Server Access Logging or AWS CloudTrail Data Events to audit who downloaded what.
  • Presigned POST URLs: Use generate_presigned_post (boto3) to allow users to upload directly to S3 with enforced conditions (file size, content type).
  • CloudFront Signed URLs: If you need CDN-level caching, geographic restrictions, or sub-second latency, use CloudFront Signed URLs instead.

📖 Official Reference: AWS Docs — Sharing objects with presigned URLs

Glossary

TermDefinition
Presigned URLA time-limited, signed URL that grants access to a specific S3 object without requiring AWS credentials from the requester.
SigV4 (Signature Version 4)AWS's request signing protocol that cryptographically binds the request parameters, credentials, and timestamp into a tamper-proof signature.
IAM PrincipalThe AWS identity (user, role, or service) whose credentials are used to sign the URL and whose permissions are evaluated by S3.
ExpiresIn / X-Amz-ExpiresThe TTL in seconds embedded in the URL, after which S3 will reject the request regardless of signature validity.
Explicit DenyAn IAM or bucket policy Deny statement that overrides any Allow, including the authorization embedded in a presigned URL.

Comments

Popular posts from this blog

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

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

Lambda Infinite Loop with S3: How to Prevent Recursive Triggers