Google Cloud Storage
Connect a Cloud Storage bucket through a service account that trusts only this Connection, or through a service account key.
Connect a Cloud Storage bucket once, and everyone in the Project can read it through Locations without holding Google Cloud credentials. Spiral obtains a short-lived access token of your service account and narrows it with a Credential Access Boundary to the root of one Location, so no reader holds the service account's full access. See Connections for what readers receive.
A gcs Connection authenticates in one of two ways:
| Authentication | Spiral stores | Use it when |
|---|---|---|
oidc (recommended) | The name of a workload identity pool provider that trusts Spiral, and the email of the service account that the Connection impersonates | You can create workload identity pools. No key exists, so there is none to leak or rotate. |
service_account_key | The private key from a service account key file | Your organization does not let you set up workload identity federation. You rotate the key yourself. |
Before you start
connection:createin the Project. Theeditorandadminroles have it.- The CLI, signed in with
spiral login, or Spiral Studio. - In the Google Cloud project that owns the bucket, permission to create
service accounts and IAM bindings, and for
oidc, workload identity pools. The steps use thegcloudCLI.
Create Connections on the branch main. All branches share one credential
for each Connection, so Spiral creates and drops Connections only on main.
Grant read access to the bucket
Whichever way the Connection authenticates, its service account needs read access to what the Connection names, and nothing more. Spiral narrows every token further, to the root of one Location, but your grant is the ceiling.
Create a service account for the Connection, and grant it read access on the bucket only, not on the project:
gcloud iam service-accounts create spiral-robotics-reader
gcloud storage buckets add-iam-policy-binding gs://example-robotics \
--member='serviceAccount:spiral-robotics-reader@example-robotics.iam.gserviceaccount.com' \
--role='roles/storage.objectViewer'Enable uniform bucket-level access on the bucket, so object ACLs cannot grant
more than IAM does. For a Connection to a prefix, such as
gs://example-robotics/recordings/, add an IAM condition on the binding that
limits it to object names under the prefix. A Connection that is to
allow writes also needs roles/storage.objectCreator; add it
only when you raise the Connection's allowed access.
Connect with workload identity federation
An oidc Connection stores no key. At each exchange, Spiral signs a
short-lived OpenID Connect token whose subject names this Connection, such as
conn_24041a03-26ef-45a2-88e2-3d89150ff6f7, and whose audience is your pool
provider. Google's Security Token Service accepts it through your pool, Spiral
impersonates your service account with it, and Google narrows the resulting
access token with a Credential Access Boundary to the objects under one root.
Your project trusts Spiral's issuer, https://auth.spiraldb.com, and never a
Google identity of Spiral's. Any Spiral Connection, in any Organization, can
ask Spiral for a token addressed to your pool, because the audience is just
the provider's name. What keeps them out is that you grant access to one
subject: the service account lets exactly the Connections it names
impersonate it. Grant roles to principal://…/subject/conn_… members only.
Never grant a role to the whole pool, principalSet://…/*, because every
Spiral Connection is a member of it.
-
Once per Google Cloud project, create a pool and an OpenID Connect provider that trusts Spiral's issuer and maps the token's subject. Use the pool for Spiral only:
gcloud iam workload-identity-pools create spiral --location=global gcloud iam workload-identity-pools providers create-oidc spiral \ --location=global \ --workload-identity-pool=spiral \ --issuer-uri='https://auth.spiraldb.com' \ --attribute-mapping='google.subject=assertion.sub'Leave the allowed audiences empty, so the provider accepts its default audience, which is the one Spiral uses. Print the provider's full name, which the Connection takes:
gcloud iam workload-identity-pools providers describe spiral \ --location=global \ --workload-identity-pool=spiral \ --format='value(name)' -
Create the Connection with the provider's name and the service account's email:
commit = client.project("demo").branch("main").commit() credential = ConnectionsCredential( gcs_oidc=ConnectionsGcsOidc( workload_identity_provider=( "projects/123456789012/locations/global/workloadIdentityPools/spiral/providers/spiral" ), service_account="spiral-robotics-reader@example-robotics.iam.gserviceaccount.com", ) ) created = commit.create_connection( "robotics-gcs", "gs://example-robotics/", credential, description="Robot recordings, read-only" ) head = commit.publish() print(f"Connection {created.connection_id} at commit {head.commit_id}")The provider name includes the project number, not the project ID.
-
Print what the service account must trust:
head = client.project("demo").branch("main") found = client.catalog.resolve_path("demo", head.commit_id, CatalogResolvePathRequest(path=[name])) if found.resource is None: raise LookupError(f"no Connection named {name}") connection = client.connections.read_connection(found.resource.descriptor.resource_id).connection if connection.identity is None: print("this Connection stores a key and has no identity") else: print(f"issuer {connection.identity.issuer}, audience {connection.identity.audience}") print(connection.identity.trust_policy)The output includes an IAM policy for the service account. It grants
roles/iam.workloadIdentityUserto this Connection's subject and to no other member of the pool. Add that binding:gcloud iam service-accounts add-iam-policy-binding \ spiral-robotics-reader@example-robotics.iam.gserviceaccount.com \ --role='roles/iam.workloadIdentityUser' \ --member='principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/spiral/subject/conn_24041a03-26ef-45a2-88e2-3d89150ff6f7'The Connection page in Studio shows the same policy.
-
Restrict the pool to the subjects of your own Connections. The pool then exchanges no token for any other Connection, so a role granted to the pool by mistake reaches nobody else:
gcloud iam workload-identity-pools providers update-oidc spiral \ --location=global \ --workload-identity-pool=spiral \ --attribute-condition="assertion.sub in ['conn_24041a03-26ef-45a2-88e2-3d89150ff6f7']"Add each new Connection's subject to the list. The same pool can also serve your Volumes, whose subjects are their IDs,
vol_…; list those too, or the pool refuses their tokens.
A Connection that you drop and create again has a new subject, and both the binding and the condition must name it.
Connect with a service account key
A key Connection stores the private key from a service account key file,
encrypted, and returns it to nobody. At each exchange, Spiral signs a token
grant with the key, trades it at Google's token endpoint for a one-hour access
token of the service account, and narrows that token with a Credential Access
Boundary, exactly as for an oidc Connection.
Create a key for the service account and download its JSON file:
gcloud iam service-accounts keys create ./spiral-robotics-reader.json \
--iam-account='spiral-robotics-reader@example-robotics.iam.gserviceaccount.com'The file is the secret. The CLI reads it from a file or from stdin, never from an argument, and the Connection takes no other option: the file names the service account and the key.
commit = client.project("demo").branch("main").commit()
credential = ConnectionsCredential(
gcs_service_account_key=ConnectionsGcsServiceAccountKey(
key_json=Path("spiral-robotics-reader.json").read_text()
)
)
created = commit.create_connection(
"robotics-gcs", "gs://example-robotics/", credential, description="Robot recordings, read-only"
)
head = commit.publish()
print(f"Connection {created.connection_id} at commit {head.commit_id}")Spiral stores the private key as the secret. It also keeps the service
account's email and the key's ID, which spiral connections show prints.
Spiral accepts only a key for Google's own token endpoint,
https://oauth2.googleapis.com/token. A file of another type, such as a
gcloud user credential, is refused. Delete the local file once the
Connection exists. To rotate the key, create a new one and
update the credential
with its file, then delete the old key in Google Cloud.
To move an existing Connection from a key to workload identity federation later, see Update a credential.
Create the Connection in Studio
- Select Create resource → Connection.
- Enter a name, select the provider Google Cloud Storage, and select the authentication: Service account (workload identity federation) or Service account key.
- Enter the URI: a bucket, or a prefix that ends with
/, such asgs://example-robotics/. - For workload identity federation, enter the workload identity provider and the service account. For a key, paste the whole key file as the secret.
- Create the Connection. For workload identity federation, the Connection page shows the IAM policy to add to the service account.
Verify the result
Test the Connection. Spiral obtains a token through the pool or with the key, narrows it, and lists one page of objects under the Connection URI with the narrowed token:
head = client.project("demo").branch("main")
found = client.catalog.resolve_path("demo", head.commit_id, CatalogResolvePathRequest(path=[name]))
if found.resource is None:
raise LookupError(f"no Connection named {name}")
result = client.connections.test_connection(found.resource.descriptor.resource_id)
print(f"credential accepted: {result.vend.succeeded}")
if result.access is not None:
print(f"storage reachable: {result.access.succeeded}")See Check that a Connection works for each step.
Allow writes
A Connection that is to allow writes needs a stronger role on the bucket. Spiral's access boundary then grants each resource no more than its level needs:
| Allowed access | Role on the bucket |
|---|---|
read | roles/storage.objectViewer |
write | roles/storage.objectViewer and roles/storage.objectCreator |
Object Creator creates objects but cannot overwrite or delete them. Do not
grant roles/storage.objectAdmin, roles/storage.objectUser, or a
project-level role. They also let the holder delete objects, change object
ACLs, or reach other buckets, and nothing Spiral does needs them.
Troubleshooting
| Problem | Cause |
|---|---|
The vend step fails at the token exchange for an oidc Connection | The pool provider does not trust https://auth.spiraldb.com, its allowed audiences are not empty, or its attribute condition does not list this Connection's subject. |
The vend step fails at impersonation with PERMISSION_DENIED | The service account does not grant roles/iam.workloadIdentityUser to this Connection's subject. |
The vend step fails with invalid_grant for a key Connection | The key was deleted or disabled in Google Cloud, or the service account was. Create a new key and update the credential. |
The access step fails with 403 | The service account cannot list the bucket. Check its role on the bucket. |
Availability
Reading a Location through a gcs Connection is
planned. Today a gcs Connection can be created, tested, and used to
obtain a credential directly, and a Location
over a gs:// root fails at its first read with an error that says so.