Ceph
Connect a bucket on a Ceph Object Gateway through a role that trusts only this Connection, or through a user's access key that assumes the role.
Connect a private bucket on a Ceph Object Gateway once, and everyone in the Project can read it through Locations without holding a key for the gateway. Spiral asks the gateway's own token service for a short-lived session of one role, narrowed to the root of one Location, so the role's own permissions stay the outer limit and no reader ever sees a long-lived key. See Connections for what readers receive.
A ceph Connection reads s3:// URIs, because the gateway serves the S3 API,
and it names the endpoint where the gateway serves S3 and STS. Ceph's token
service checks a role's trust policy, so every ceph Connection names a role,
and authenticates to it in one of two ways:
| Authentication | Spiral stores | Use it when |
|---|---|---|
oidc (recommended) | The endpoint and the ARN of a role that trusts this Connection | Your gateway runs Ceph 19.2.3 or later and can reach https://auth.spiraldb.com. No key exists, so there is none to leak or rotate. |
access_key | The endpoint, the role's ARN, and the access key of a user that the role trusts | Your gateway runs an earlier Ceph release, or cannot register an OpenID Connect provider. You rotate the key yourself. |
Prefer Ceph Squid 19.2.6 or later, Tentacle 20.2.4 or later, or a vendor release with the equivalent security patch. Earlier releases contain CVE-2026-39944, which lets a holder of a valid STS token obtain gateway administrator access. This affects both oidc and access_key, regardless of the session policy. Ceph 19.2.3 adds compatibility with Spiral's OIDC keys but does not fix this vulnerability. The gateway operator manages upgrades and follows Ceph's key migration guidance.
Before you start
-
connection:createin the Project. Theeditorandadminroles have it. -
The CLI, signed in with
spiral login, or Spiral Studio. -
A gateway with its token service enabled:
rgw_s3_auth_use_sts = trueand anrgw_sts_key, as the Ceph STS documentation describes. -
For
oidc, Ceph 19.2.3 or later. Spiral's issuer publishes its signing keys without certificates, and releases 19.2.0 to 19.2.2 refuse such keys: thevendstep fails and the gateway log saysx5c not present. On those releases, useaccess_keyor upgrade the gateway. -
A gateway user with the
rolescapability, and foroidctheoidc-providercapability, to create the role and register the provider:radosgw-admin caps add --uid=admin --caps="roles=*;oidc-provider=*" -
An
https://endpoint. A hosted Spiral deployment refuseshttp://endpoints and loopback hosts; only local development accepts them.
Create Connections on the branch main. All branches share one credential
for each Connection, so Spiral creates and drops Connections only on main.
Create the role
Whichever way the Connection authenticates, Spiral assumes one role on the
gateway. Create it with a temporary trust policy, which the next sections
replace, and give it read access to what the Connection names, and nothing
more. Spiral narrows every session further, to the root of one Location, but
the role's policy is the ceiling. For a Connection to s3://example-robotics/:
aws --endpoint-url https://ceph.example.com iam create-role \
--role-name spiral-robotics-reader \
--assume-role-policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}'
aws --endpoint-url https://ceph.example.com iam put-role-policy \
--role-name spiral-robotics-reader \
--policy-name read-example-robotics \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::example-robotics"
},
{
"Sid": "ReadObjects",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::example-robotics/*"
}
]
}'The aws commands sign with the keys of the gateway user that holds the
roles capability. create-role prints the role's ARN, such as
arn:aws:iam:::role/spiral-robotics-reader; a role in a tenant names the
tenant before :role/. The Connection takes the ARN as printed.
For a Connection to a prefix, such as s3://example-robotics/recordings/,
narrow ReadObjects to arn:aws:s3:::example-robotics/recordings/* and add
the condition "StringLike": {"s3:prefix": ["recordings/", "recordings/*"]}
to s3:ListBucket.
Grant no other action, and never s3:DeleteObject. A ceph Connection
allows only read, so the role never needs more.
Connect with the Connection's identity
An oidc Connection stores no key. At each exchange, Spiral signs a
short-lived OpenID Connect token whose subject names this Connection, and the
gateway's token service exchanges it for a session of the role. The role
trusts that one subject, so no other Connection, in your Organization or any
other, can assume it.
-
Create the Connection with the endpoint and the role's ARN:
commit = client.project("demo").branch("main").commit() credential = ConnectionsCredential( ceph_oidc=ConnectionsCephOidc( endpoint="https://ceph.example.com", role_arn="arn:aws:iam:::role/spiral-robotics-reader" ) ) created = commit.create_connection( "robotics-ceph", "s3://example-robotics/", credential, description="Robot recordings, read-only" ) head = commit.publish() print(f"Connection {created.connection_id} at commit {head.commit_id}") -
Print what the gateway 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 names the identity's issuer,
https://auth.spiraldb.com, its audience, which is the gateway's host, and a trust policy like this one, with the Connection's own subject:{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam:::oidc-provider/auth.spiraldb.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "auth.spiraldb.com:sub": "conn_24041a03-26ef-45a2-88e2-3d89150ff6f7", "auth.spiraldb.com:app_id": "ceph.example.com" } } }] }Save the trust policy as
trust-policy.json. -
Once per gateway, or once per tenant, register Spiral's issuer as an OpenID Connect provider. The client ID is the audience that the Connection printed. Ceph also records a thumbprint: the SHA-1 fingerprint of the last certificate in the chain that
https://auth.spiraldb.comserves, which the gateway checks when it fetches Spiral's signing keys:thumbprint="$(openssl s_client -showcerts -servername auth.spiraldb.com -connect auth.spiraldb.com:443 </dev/null 2>/dev/null \ | awk '/-----BEGIN CERTIFICATE-----/ { cert = "" } { cert = cert $0 "\n" } /-----END CERTIFICATE-----/ { last = cert } END { printf "%s", last }' \ | openssl x509 -fingerprint -sha1 -noout | cut -d= -f2 | tr -d ':')" aws --endpoint-url https://ceph.example.com iam create-open-id-connect-provider \ --url https://auth.spiraldb.com \ --client-id-list ceph.example.com \ --thumbprint-list "$thumbprint"The
opensslpipeline keeps the last certificate that the server sends and prints its SHA-1 fingerprint without colons. Register the thumbprint again when that certificate changes. -
Set the printed trust policy on the role, unchanged:
aws --endpoint-url https://ceph.example.com iam update-assume-role-policy \ --role-name spiral-robotics-reader \ --policy-document file://trust-policy.json
Keep the sub condition. Every Spiral Connection, in any Organization, can
obtain a token from https://auth.spiraldb.com for your endpoint. Only the
subject tells your Connection apart, so a role that trusts the provider
without it trusts every Connection. To let two Connections assume one role,
list both subjects in the condition. The app_id condition is the token's
audience, which Ceph reads from the aud claim: the gateway's host, with its
port when the endpoint names one.
A Connection that you drop and create again has a new subject, and the role
must trust the new one. The gateway fetches the issuer's signing keys from
https://auth.spiraldb.com/.well-known/openid-configuration, so it must reach
that host.
Connect with an access key
An access key Connection stores the secret of a gateway user, encrypted, and returns it to nobody. At each exchange, Spiral calls the gateway's token service as that user and assumes the role with a policy for one root, so the role's trust policy must name the user:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam:::user/spiral-robotics" },
"Action": "sts:AssumeRole"
}]
}Use a dedicated user for the Connection, with no policy of its own. The role holds the read access, and the session can do only what the role's policy and Spiral's policy both allow. The CLI reads the secret from a file or from stdin, never from an argument:
commit = client.project("demo").branch("main").commit()
credential = ConnectionsCredential(
ceph_access_key=ConnectionsCephAccessKey(
endpoint="https://ceph.example.com",
role_arn="arn:aws:iam:::role/spiral-robotics-reader",
access_key_id="K3V2YSRWVKLYE9ZPDG4D",
secret_access_key=Path("ceph-secret-key.txt").read_text().strip(),
)
)
created = commit.create_connection(
"robotics-ceph", "s3://example-robotics/", credential, description="Robot recordings, read-only"
)
head = commit.publish()
print(f"Connection {created.connection_id} at commit {head.commit_id}")To move an existing Connection from a key to the Connection's identity later, see Update a credential.
Create the Connection in Studio
- Select Create resource → Connection.
- Enter a name, select the provider Ceph, and select the authentication: IAM role (OpenID Connect) or Access key.
- Enter the URI: a bucket, or a prefix that ends with
/, such ass3://example-robotics/. - Enter the endpoint and the role ARN. For an access key, also enter the access key ID and the secret access key.
- Create the Connection. For the Connection's identity, the Connection page shows the issuer, the audience, and the trust policy to set on the role.
Verify the result
Test the Connection. Spiral obtains a session of the role with the key or the Connection's identity, and lists one page of objects under the Connection URI with it:
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.
Reads only
A ceph Connection allows only read. The gateway does not enforce the
policy condition that keeps a Spiral credential from replacing an object, so
Spiral vends no credential that writes through a Connection. A
ceph Volume is
different: Spiral manages every object on it, and hands out credentials that
write. On Ceph, such a credential can also replace objects.
Troubleshooting
| Problem | Cause |
|---|---|
The vend step fails with AccessDenied for an oidc Connection | The role's trust policy does not name this Connection's subject, or the provider is missing, has another client ID, or is registered in another tenant than the role. Set the trust policy that spiral connections show prints. The gateway log names the step that refused. |
The gateway log says x5c not present | The gateway runs a Ceph release before 19.2.3, which verifies only signing keys that carry a certificate. See Before you start. |
| The gateway log says the signature cannot be validated | The gateway cannot reach https://auth.spiraldb.com, or the registered thumbprint is not the fingerprint of the last certificate in the chain that it serves. |
The vend step fails for an access key | The key is wrong, or the role's trust policy does not name the key's user. |
The vend step fails with MethodNotAllowed or NotImplemented | The gateway's token service is off. Set rgw_s3_auth_use_sts and rgw_sts_key. |
The access step fails with AccessDenied | The role cannot list the bucket or the prefix. Check the role's policy. |
| Spiral refuses the endpoint | The endpoint is not an origin, or it is http:// or a loopback host on a hosted deployment. Give https://, a host, and a port, such as https://ceph.example.com, with no path and no user name. |
Next step
Create a Location with a root
inside the Connection URI, such as s3://example-robotics/recordings/.