S3 presigned URLs are a clever way to grant temporary, secure access to your S3 objects without exposing them publicly or managing complex IAM policies for every user.
Let’s see one in action. Imagine you have a private S3 bucket, my-secure-bucket, and you want to let a user download a file named confidential-report.pdf for the next 15 minutes.
Here’s how you’d generate that URL using the AWS CLI:
aws s3 presign s3://my-secure-bucket/confidential-report.pdf --expires-in 900
This command will output a URL that looks something like this:
https://my-secure-bucket.s3.amazonaws.com/confidential-report.pdf?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Signature=vBqZ/21/5w/z/mXfK/l+1jR7s7/2xQ3GzFkFfNl6c5Y%3D&Expires=1678886400
When a user clicks this URL within its expiration time, the browser (or any client) will be authenticated using the embedded credentials and signature, allowing them to download confidential-report.pdf as if they had direct S3 access. After 15 minutes (900 seconds), the URL will simply return an AccessDenied error.
The problem S3 presigned URLs solve is granular, time-bound access. Without them, you’d either have to:
- Make the object public (terrible for security).
- Create a dedicated IAM user for each downloader (unmanageable at scale).
- Build a custom proxy server to fetch the object from S3 and stream it to the user (adds complexity and cost).
A presigned URL packages everything needed for temporary access into a single HTTP request. It’s essentially a signed GET (or PUT, POST, DELETE) request for an S3 object. The signature is generated using your AWS secret access key, the object’s key, the HTTP method, and the expiration timestamp. This signature cryptographically proves that the request was authorized by an entity that has permission to access the object and that it’s being made within the specified timeframe.
The core components of a presigned URL are:
- The S3 Object Endpoint:
https://my-secure-bucket.s3.amazonaws.com/confidential-report.pdf AWSAccessKeyId: Your AWS access key ID. This identifies who is making the request.Expires: A Unix timestamp indicating when the URL becomes invalid.Signature: The cryptographic hash that validates the request. This is the magic. It’s generated using HMAC-SHA256 with your secret access key and a string composed of the HTTP method, the canonicalized resource, and the expiration timestamp.
You can generate presigned URLs for various HTTP methods, not just GET. For instance, to allow a user to upload a file to a specific location in your bucket for 30 minutes:
aws s3 presign s3://my-secure-bucket/uploads/user123/new-document.txt --expires-in 1800 --use-method PUT
This generates a URL that a client can then use in a PUT request to upload new-document.txt directly to S3. The client doesn’t need its own AWS credentials; it just needs the presigned URL.
The security of presigned URLs hinges on two factors: the permissions of the IAM principal generating the URL, and the expiration time. The principal generating the URL must have the necessary S3 permissions (e.g., s3:GetObject or s3:PutObject) for the target object. If that principal’s permissions are revoked, any existing presigned URLs will still work until they expire, but new ones cannot be generated. The expiration time limits the window of opportunity for misuse.
When a client uses a presigned URL, S3 doesn’t re-evaluate the IAM policies of the user providing the URL. Instead, it validates the signature and the expiration timestamp embedded within the URL itself. If the signature is valid and the expiration time has not passed, S3 proceeds with the requested operation. This is why the secret access key used to sign the URL must be kept confidential by the generating application or user.
The signature calculation is a bit involved, but at its heart, it’s a standard process: take your secret access key, a specific string derived from the request details, and compute an HMAC-SHA256 hash. This hash is then base64-encoded. The exact string that gets hashed is crucial: it typically includes the HTTP method, a canonicalized resource path (which is just /bucket-name/object-key), and the expiration timestamp. The x-amz-algorithm, x-amz-credential, x-amz-date, and x-amz-signedheaders parameters are also part of the signed headers, but the core signature is based on the method, resource, and expiry.
The next logical step is to explore how to revoke access to objects before their presigned URLs expire, which involves a different set of S3 features.