Signed URLs are a way to grant temporary, controlled access to private files stored in cloud object storage.
Let’s see this in action with Amazon S3. Imagine you have a private image file, my-secret-photo.jpg, in a bucket named my-private-bucket. Normally, only authorized users can access it. But with a signed URL, you can give someone a link that works for, say, 15 minutes, even if they don’t have direct S3 permissions.
Here’s how you’d generate one using the AWS CLI:
aws s3 presign s3://my-private-bucket/my-secret-photo.jpg --expires-in 900
This command outputs a URL that looks something like this:
https://my-private-bucket.s3.amazonaws.com/my-secret-photo.jpg?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Signature=bWkyc2VjcmV0cGhvdG8uanBnP...&Expires=1678886400
When someone uses this URL within the 15-minute (900-second) window, S3 verifies the signature and the expiration time. If they’re valid, S3 serves the file. After 15 minutes, the link becomes invalid, and S3 will return an AccessDenied error.
The core problem signed URLs solve is securely sharing private assets without making them public. Think of sharing a temporary download link for a large report, a one-time access link for a video, or providing a secure upload URL for a user to send files directly to your storage. It allows for fine-grained, ephemeral access control managed by your application, not the storage system itself.
Internally, a signed URL is a regular URL for the object, augmented with query parameters: AWSAccessKeyId, Expires, and Signature. The AWSAccessKeyId identifies the AWS credentials used to generate the signature. Expires is a Unix timestamp indicating when the URL becomes invalid. The Signature is the critical part: it’s a cryptographic hash of specific URL components (like the HTTP method, the expiration time, and the object’s path) generated using the secret access key associated with the AWSAccessKeyId. When a request comes in with a signed URL, the cloud provider recalculates the signature using the provided AWSAccessKeyId and the object’s details, then compares it to the Signature in the URL. If they match and the Expires timestamp is in the future, access is granted.
The levers you control are primarily the expiration time and the access permissions associated with the credentials used to sign the URL. You can set the expiration to be as short as a few seconds or as long as a week (depending on the provider’s limits). The permissions tied to the signing credentials dictate what operations (GET, PUT, DELETE, etc.) are allowed via the signed URL. For instance, you can create a signed URL for a PUT operation to allow a user to upload a file to a specific location without giving them broad write access to your bucket.
It’s important to remember that the Expires parameter doesn’t just control when the URL stops working; it’s also a key part of the data that gets signed. If you generate a signed URL with an expiration of 5 minutes, and then someone tries to use it after 6 minutes, it’s invalid. But if you generate it with an expiration of 5 minutes, and then a malicious actor intercepts it and changes the Expires parameter in the URL to 10 hours, that altered URL will not work because the signature will no longer match the modified expiration time. The signature is a hash of the original request components, including the original expiration time.
The next concept to explore is how to generate signed URLs programmatically for dynamic content.