GCP Signed URLs Explained: Temporary Access to Private Cloud Storage Objects
A signed URL is a time-limited link to a specific Cloud Storage object that includes a cryptographic signature in the URL itself. The recipient does not need a Google account or any GCP IAM permission. Cloud Storage validates the signature at the edge when the request arrives. Signed URLs are the standard pattern for giving external users or systems temporary upload or download access to a private bucket without touching your IAM policy.
What a signed URL is
A signed URL is a Cloud Storage URL that has been cryptographically signed by a service account. The signature encodes the allowed HTTP method (GET, PUT, etc.), the exact object path, an expiry timestamp, and other parameters. When a client makes a request using that URL, Cloud Storage verifies the signature, checks the expiry, and confirms the request matches the permitted method and path. No GCP credentials are required from the caller.
This makes signed URLs fundamentally different from normal Cloud Storage access. Normal access requires the caller to authenticate with Google and hold an IAM role on the bucket. A signed URL delegates that permission in a scoped, time-limited form to whoever holds the link.
Signed URLs are typically generated by your application server, which holds the service account credentials, and then given to clients on demand. The client uses the URL directly with Cloud Storage. Your server is not in the data transfer path.
For background on how Cloud Storage is structured and how normal access control works, see Storage Buckets Explained and Cloud Storage IAM vs ACLs.
Think of your application server as a receptionist at a secure building. When a visitor needs temporary access, the receptionist prints them a dated pass: it names the room they can enter (the object path), what they can do there (read or write), and when the pass expires. Security at the door (Cloud Storage) checks the pass is genuine and not expired, then lets the visitor through. The visitor never becomes an employee and needs no building key of their own. When the pass expires, it stops working automatically.
The signed URL is that pass. Your server creates it. The client uses it. Cloud Storage enforces it.
Why signed URLs exist
Cloud Storage buckets are private by default. To read or write any object, a caller must have a valid GCP identity and an IAM role granting the relevant permission on the bucket. This is the right default for internal systems.
The problem is when you need to share a file with someone outside your GCP project. Consider these situations:
- A customer needs to download their invoice from a private bucket
- A mobile app needs to let users upload a profile photo directly to Cloud Storage
- A partner system needs to fetch a data export file once
- You want to email a one-time download link that expires in an hour
The obvious alternatives each have significant problems:
Making the object public removes all access control. Anyone with the URL can download it, indefinitely, with no expiry and no audit trail. This is only appropriate for genuinely static public content such as marketing images or open documentation.
Giving external users IAM access requires them to have a Google account. The access does not expire automatically. You have to remember to revoke it. This is impractical for one-off or external sharing.
Proxying every download through your server keeps you in control but means all file transfer traffic flows through your application server. For large files or high volumes this adds latency, bandwidth costs, and a potential bottleneck.
Signed URLs solve this cleanly. You grant temporary, scoped access to a specific object, for a specific HTTP method, with a specific expiry, without modifying any IAM policy or making anything permanently accessible.
How signed URLs work
Here is the full process from URL creation to the file being served:
Your server builds a canonical string containing the HTTP method, the full object path in the bucket, the expiry timestamp, and any required headers.
Your server signs the canonical string using a service account private key. The signature proves the URL was issued by a party with access to that service account.
The signature is appended to the URL as query parameters, along with the expiry time and the signing service account identity.
Your server returns the signed URL to the client via your API, an email, or however the client is consuming it.
The client makes an HTTPS request to the signed URL. No GCP credentials are sent. The URL itself is the credential.
Cloud Storage validates the signature using the public key for the signing service account, checks the request matches the permitted method and object path, and checks the expiry has not passed.
If everything is valid, Cloud Storage serves the object (for GET) or accepts the upload (for PUT). If not, it returns a 403.
The client never needs to know anything about GCP, IAM, or the service account behind the URL. The entire permission is encoded in the link.
V4 signing is the current standard and should be used in all new code.
V2 signing is deprecated. If you see X-Goog-Signature in
your URL query string, you are using V4. Maximum expiry for V4 is 7 days.
Common use cases
Customer download links. Generate a signed GET URL for an invoice, report, or receipt and include it in an email or API response. Set the expiry to match the expected download window.
Direct browser or mobile uploads. Instead of routing a user’s file upload through your server, issue a signed PUT URL. The client uploads directly to Cloud Storage. Your server is removed from the data path entirely for large files.
Sharing private exports. If you generate nightly reports or data exports into a private bucket, signed URLs let you share them with specific recipients without changing the bucket’s access policy.
Partner system integrations. A partner API can fetch a file from your bucket using a signed URL your system generates on demand, without needing its own GCP credentials.
Offloading transfers from your application server. Any time a file is large or a transfer is high-volume, signed URLs let clients connect directly to Cloud Storage rather than streaming through your application.
When to use signed URLs
- The recipient is external to your GCP IAM setup (a user, a customer, a partner system)
- Access should expire automatically after a short time
- You want the client to transfer files directly with Cloud Storage rather than through your server
- You need to share a specific object without changing the bucket’s access policy
You want permanent public access. Use
allUsersIAM or public objects for that.Your own backend must retain full control of every request and log the transfer at the application level.
The recipients already have GCP identities. Give them appropriate IAM roles instead, or use IAM Conditions to scope their access more tightly.
You need per-URL revocation. Signed URLs cannot be individually revoked.
Signed URLs vs other access methods
| Method | Who can access | Expires | Requires GCP identity | Best for |
|---|---|---|---|---|
| Signed URL | Anyone with the URL | Yes, configurable | No | External one-off access |
| Public object | Anyone, indefinitely | No | No | Genuinely public static content |
| IAM access | Authenticated GCP principals | Only via Conditions | Yes | Internal or trusted systems |
| Backend proxy | Whoever your server allows | Your server controls | No (server authenticates) | Full server-side control and logging |
| IAM Conditions | Authenticated GCP principals | Yes (time-based) | Yes | Internal users needing time-limited access |
The key distinction between a signed URL and a public object is expiry and scope. A signed URL expires and is scoped to one specific object. A public object is accessible to anyone with its URL, indefinitely. Use signed URLs any time access should be temporary.
Compared to a backend proxy, a signed URL offloads the transfer entirely to Cloud Storage infrastructure. This is more efficient for large files but means your server has less visibility into exactly what is transferred. For strict audit requirements, proxying through your server may be preferable. For offloading transfer costs and reducing server load, signed URLs are better.
For browser form uploads specifically, Cloud Storage also supports signed policy documents. These allow more granular control over what a browser can upload (file size limits, content-type restrictions), whereas a PUT signed URL allows any bytes to be written to the specified path.
Creating signed URLs with gcloud
The gcloud storage sign-url command generates a V4 signed URL
using the credentials of the current gcloud session or a specified service
account. This is useful for testing and one-off operations. For production
use, generate signed URLs programmatically in your application code.
# Signed URL valid for 1 hour to download a specific file
gcloud storage sign-url gs://my-app-private-assets/report-q1-2026.pdf \
--duration=1h \
--method=GET
# Signed URL valid for 15 minutes to upload a file (PUT requires content-type)
gcloud storage sign-url gs://my-app-uploads/user-123/avatar.png \
--duration=15m \
--method=PUT \
--content-type=image/png
# Signed URL using service account impersonation (preferred pattern)
gcloud storage sign-url gs://my-app-private-assets/export.csv \
--duration=30m \
--method=GET \
--impersonate-service-account=download-sa@my-project.iam.gserviceaccount.comgcloud storage sign-url uses V4 signing by default. The URL
includes the expiry, the signing service account, and the signature as
query parameters. It is valid from the moment of creation until the expiry,
regardless of how many times it is used within that window.
The —impersonate-service-account flag signs the URL on behalf
of a service account without downloading its key file. This requires the
iam.serviceAccounts.signBlob permission on that service account.
See Service Account Impersonation
for how impersonation works and when to use it.
Creating signed URLs in application code
In production, your application server generates signed URLs on demand and returns them to clients. The client then uses the URL directly with Cloud Storage. Your server is not in the data transfer path.
The upload workflow:
- Client requests an upload URL from your server
- Server generates a signed PUT URL for a specific, server-controlled object path and returns it
- Client uploads the file directly to Cloud Storage using the signed URL
- Cloud Storage validates the signature and stores the object
- Client notifies your server that the upload is complete
- Server validates the uploaded object (file type, size, content) before trusting it
The download workflow is identical but uses GET, and step 3 is a download rather than an upload.
from google.cloud import storage
import datetime
def generate_signed_download_url(bucket_name, object_name, expiry_minutes=60):
"""
Generates a V4 signed URL for downloading a Cloud Storage object.
When running on GCP infrastructure, the attached service account is used
to sign via the IAM API. No key file is required.
The service account must have iam.serviceAccounts.signBlob permission.
"""
client = storage.Client()
bucket = client.bucket(bucket_name)
blob = bucket.blob(object_name)
url = blob.generate_signed_url(
version="v4",
expiration=datetime.timedelta(minutes=expiry_minutes),
method="GET",
)
return url
def generate_signed_upload_url(bucket_name, object_name, expiry_minutes=15):
"""
Generates a V4 signed URL for uploading to a Cloud Storage object path.
The client must send the same Content-Type header when uploading.
"""
client = storage.Client()
bucket = client.bucket(bucket_name)
blob = bucket.blob(object_name)
url = blob.generate_signed_url(
version="v4",
expiration=datetime.timedelta(minutes=expiry_minutes),
method="PUT",
content_type="application/octet-stream",
)
return urlWhen running on GCP (Cloud Run, GKE, Compute Engine), the client library uses the service account attached to the compute resource to sign via the IAM API. No key file is needed. This is the recommended approach and avoids managing private key material entirely.
For more on how service accounts work and what permissions they need, see Service Accounts. For how to upload files via the CLI rather than signed URLs, see Uploading Files with gsutil.
Security considerations
A signed URL is a credential. Anyone who has it can use it until it expires. Treat it accordingly.
Web servers, load balancers, and CDNs often log full request URLs including query parameters. If a client redirects to a signed URL and that redirect is logged, the URL is now in your log storage in plaintext. Redact query parameters from Cloud Storage request logs, or proxy the request instead of redirecting.
Use the shortest expiry that works. Minutes for one-time uploads, hours at most for downloads. A 7-day expiry on a sensitive file is a 7-day window for anyone who intercepts the URL.
Scope to specific object paths. Never construct the object path from unvalidated user input. If user-controlled input reaches the path, a malicious client can request a signed URL for any object in the bucket. Build the path server-side from values you control.
Validate uploads after completion. A signed PUT URL allows the client to write any bytes to that path. Validate the uploaded file’s type, size, and content server-side before treating it as trusted.
Use IAM-based signing, not key files. The IAM signBlob API signs on behalf of a service account without a downloaded key file. Downloaded key files are a significant security liability. See Why Service Account Keys Are Dangerous for why the distinction matters.
Use a dedicated service account for signing. Sign with a service account that has the minimum required permissions on the bucket, not with broad application credentials.
For access patterns that do not require external sharing, consider IAM Conditions to restrict access by time, resource, or request context instead of using signed URLs. See Cloud Storage Security for a broader look at bucket hardening.
If you are seeing unexpected 403 errors when using signed URLs, see Storage Access Denied Errors for common causes and fixes.
Common mistakes
Setting expiry times that are too long. A signed URL valid for 24 hours or 7 days is a credential that can be stolen and reused throughout that entire window. Default to 15 minutes for uploads, 1 hour for downloads. Only go longer if you have a clear reason.
Logging full signed URLs. The signature and all parameters are in the query string. If your access logs capture full URLs, signed URLs appear there in plaintext. Configure log redaction before this becomes a problem.
Signing with downloaded service account key files. The IAM signBlob API is the correct modern approach. If your code loads a JSON key file to sign URLs, replace it with IAM-based signing. See Service Account Keys Explained for context on when key files are ever appropriate.
Letting user input control the object path. Your server must construct the object path from values it controls, not raw client input. A malicious client could request a signed URL for a path it has no business accessing.
Confusing signed URLs with making objects public. A signed URL expires and is scoped to one object. A public object is accessible to anyone indefinitely. These are very different access models with very different risk profiles.
Assuming a signed URL can be individually revoked. It cannot. The only ways to invalidate one are to delete the signing key (if a key file was used) or remove the signBlob permission from the signing service account (if IAM signing was used). Both affect all URLs signed with that key or account.
Using signed URLs for internal service-to-service traffic. If both services have GCP identities, use IAM roles directly. Signed URLs are designed for external access. Using them internally adds unnecessary complexity and bypasses the audit trail that IAM provides.
Summary
- A signed URL embeds a cryptographic signature, object path, HTTP method, and expiry into the URL. No GCP credentials are needed by the recipient to use it.
- The primary use case is giving external users or systems temporary access to a specific private Cloud Storage object without modifying your IAM policy.
- V4 signing is the current standard. Maximum expiry is 7 days. In practice, keep expiry to minutes for uploads and an hour or less for downloads.
- Use IAM-based signing (signBlob API) rather than downloaded service account key files. Running on GCP infrastructure handles this automatically.
- Your server generates the signed URL and returns it to the client. The client communicates directly with Cloud Storage. Your server is not in the transfer path.
- Treat signed URLs as secrets: do not log them, keep expiry times short, and construct object paths server-side from values you control.
- Signed URLs cannot be individually revoked. Short expiry times are the main mitigation if a URL is intercepted.
Frequently asked questions
Do users need a Google account to use a signed URL?
No. A signed URL is a self-contained credential embedded in the URL itself. Anyone who has the link can use it regardless of whether they have a Google account or any GCP permissions. This is the core point: signed URLs grant temporary access to people and systems outside your IAM structure entirely. Treat them as secrets. Do not log them, do not embed them in client-side code, and do not share them publicly.
What is the maximum expiry time for a signed URL?
For V4 signing (the current standard), the maximum expiry is 7 days (604,800 seconds). In practice, use the shortest expiry that fits your use case: 15 minutes for a one-time upload, an hour or less for a download link, and never more than a day for anything sensitive.
Can a signed URL be revoked before it expires?
Not individually. There is no way to cancel one specific signed URL without affecting others. Signed URLs created with a service account key can be invalidated by deleting that key pair. URLs signed using the IAM signBlob API can be invalidated by removing the signBlob permission from the service account, but this affects every URL that account has signed. The best mitigation is to keep expiry times short so the exposure window stays small.
What is the difference between GET and PUT signed URLs?
A GET signed URL allows the recipient to download (read) a specific object. A PUT signed URL allows the recipient to upload (write) to a specific object path. You can also create signed URLs for DELETE and other HTTP methods, but GET and PUT are by far the most common. A signed URL only authorises the specific method it was created for. A PUT signed URL cannot be used to download the same file.
Are signed URLs more secure than making objects public?
Yes, significantly. A public object is accessible to anyone, indefinitely, with no audit trail. A signed URL expires, is scoped to a specific object, and can be traced back to the service account that signed it. That said, a signed URL is still a credential: anyone who has it can use it until it expires. Short expiry times, never logging the URL, and scoping it to a specific path are all essential.
Do signed URLs bypass IAM?
Yes, by design. The signature is issued by a service account that has IAM access to the bucket. When a recipient uses a signed URL, Cloud Storage validates the signature against the signing service account, not against the recipient's identity. The recipient has no GCP identity of their own in this transaction. This is what makes signed URLs useful for external access, and also what makes them sensitive.
Can I use signed URLs for browser uploads?
Yes. A PUT signed URL can be used from a browser to upload a file directly to Cloud Storage without routing the file through your server. The typical pattern is: the browser requests a signed URL from your backend, your backend generates it and returns it, and the browser uploads the file directly to Cloud Storage using the signed URL. This avoids large file payloads passing through your application server.