Authentication & Access
Last validated against the API server on 2026-08-31.
Auth is cross-cutting: the same Bearer JWT authenticates every admin RPC across Storage, Pipelines, Tables, and Vector. The S3-style modes (SigV4 / SigV2, presigned) apply to the Storage byte plane only.
Auth modes
K3 accepts these authentication shapes — pick whichever fits your client:
| Mode | Header / query | Where it works |
|---|---|---|
| Bearer JWT | Authorization: Bearer <token> | Everywhere — the K3 control plane (HTTP /admin/..., /:bucket/... and gRPC), the object gateway, and every wire adapter |
| Dodil API key as Bearer | Authorization: Bearer dk_… | The object gateway and the tables-gateway HTTP wires — not the K3 control plane, which validates Bearer as a Keycloak JWT only |
| Dodil API key as Basic | Authorization: Basic base64(dk_{lookup}:secret) | The tables-gateway HTTP wires (GraphQL, Qdrant, Pinecone). The username is not ignored — it is the dk_{lookup} half of the key, rejoined with the password to reconstruct the token |
| SigV4 header | Authorization: AWS4-HMAC-SHA256 ... | Standard S3 SDKs (aws-cli, boto3, @aws-sdk/client-s3, mc) on the Storage byte plane. Access key ID is either a service-account client_id or dk_ + the key’s 16-char lookup |
| SigV4 query | ?X-Amz-Algorithm=AWS4-HMAC-SHA256&... | SDK-issued presigned URLs |
| SigV2 query | ?AWSAccessKeyId=...&Signature=...&Expires=... | Legacy S3 clients — HMAC-SHA1, fully implemented, tried before SigV4 query |
| K3 presigned token | ?X-K3-Token=...&X-K3-Expires=...&X-K3-Org=... | URLs issued by GetObjectUrl. X-K3-Token is the signature — an HMAC-SHA256 over org, bucket, key, and expiry |
The SigV4 secret is not the API-key secret. When you sign with a
dk_…access key ID, K3 uses the lookup only to identify the service account the key is bound to; the signing secret is that SA’s secret. Issue the pair from the S3 credentials flow rather than pasting an API key secret — see Storage → S3 Compatibility.
The username-is-ignored form exists on exactly one door: the Postgres/Bolt wires, where a JWT-shaped password is taken as a bearer token and the user is discarded.
Your organization context is derived from the credential you present — JWT claims for bearer auth, the API-key / service-account lookup for SigV4, the signed X-K3-Org for K3 presigned URLs. You never send an x-organization-id header yourself: the auth layer writes it from the verified credential, overwriting anything you sent, before any handler sees the request.
The SigV4 / SigV2 modes are S3-compatibility features of the Storage byte plane. For setup with aws-cli / boto3 (access key, secret, endpoint), see Storage → S3 Compatibility.
Why API keys work on every wire — the session context
Elsewhere on Dodil, dk_ API keys are verified by a private lookup against IAM, so only gateways with private IAM connectivity can accept them. DataK³’s data wires still take dk_ keys everywhere because of a deliberate hand-off:
- The gateway tier (in the apps cluster, next to IAM) terminates your credential — it verifies a
dk_key against IAM (argon2 hash lookup), or a JWT against Keycloak, and resolves your grants. - It then mints a short-lived signed session context and forwards that to the remote data cluster. It is HMAC-SHA256 over a compact JSON claim set — not a JWT — with a 300 s TTL (plus 30 s clock skew at the verifier), bound to the one target db, and carrying only the grant subset relevant to that db.
- The data cluster verifies the context’s signature, version, expiry, and db binding, and enforces the carried grants at statement level — reads need
QueryTables, DML needsWriteTables, DDL needsManageTables, and a statement whose targets can’t be resolved falls back to a db-level check.
The context rides as x-k3-session-context on the HTTP/gRPC doors and as the k3_session_context startup parameter on the Postgres door. The gateway strips any client-supplied value of either before attaching its own — you cannot mint or forge one.
Downstream authorization derives solely from the signed context. On the Postgres door your password is consumed at the gateway and never forwarded at all; on the HTTP and gRPC doors the Authorization / api-key header is passed through to the adapter but is never evaluated there. Either way, a remote cluster can only act on the expiring, db-bound context. That is why the pg, object/S3, Bolt, Qdrant, Pinecone, GraphQL, and gRPC adapters all accept API keys even though the clusters serving them have no path to IAM. (Contrast: Ignite’s direct worker/invoke endpoints do no such hand-off and accept service-account JWTs only.)
gRPC
gRPC uses the same Bearer JWT — send it as the authorization metadata (Bearer <token>), e.g. grpcurl -H "Authorization: Bearer $DODIL_TOKEN". Org context is resolved server-side from the token, exactly as on HTTP.
The control-plane gRPC door accepts Bearer JWTs only. SigV4, SigV2, presigned URLs, and Basic auth are HTTP-plane credentials; the api-key metadata form belongs to the tables-gateway gRPC door, not to rpc.data.dodil.io.
Suspended organizations
If your organization’s status is SUSPENDED (e.g. non-payment), K3 rejects authenticated requests until the suspension is lifted. The check runs in the auth layer, right after the credential validates, on both the Bearer and SigV4 paths.
The status code differs by transport:
- HTTP (the k3-api surface, including its S3 proxy) —
402 Payment Required, body{"error": "Payment Required: organization is suspended due to non-payment"}. - gRPC —
PERMISSION_DENIED(7), which surfaces as HTTP 403, with the same message string. There is no gRPC status code for 402.
IAM also reports ACTIVE, BILLING_PAST_DUE, BILLING_GRACE_PERIOD, PAUSED, and CLOSED. Only the exact string SUSPENDED blocks requests — the comparison is a case-sensitive exact match, and no other status is checked.
The honest scope: the check lives in k3-api’s HTTP and gRPC auth layers, on the Bearer-JWT and SigV4-header paths. It does not run on presigned-URL requests (K3 presigned, SigV4 query, SigV2 query), on the standalone object gateway, or on the tables-gateway wires. A suspended org’s already-issued presigned URLs and its data-wire sessions keep working until the credential itself expires. Quota refusals on the byte plane also return 402, but with an S3 QuotaExceeded error code — that is entitlement, not suspension.
Unauthenticated endpoints
On HTTP, exactly these probe paths bypass auth (exact match, no prefix matching): /health, /healthz, /ready, /readyz, /metrics. To use them operationally (readiness, SLO scraping), see Operations → Service health.
On gRPC the bypass covers any method ending in /HealthCheck, plus the grpc.health.* and grpc.reflection.* services — reflection is deliberately public so grpcurl … list works before you authenticate.
Storage access control
Bucket-level authorization — access modes (PRIVATE / PUBLIC / CUSTOM), S3-style bucket policies, and CORS — is documented with the Storage primitive:
- Storage → Core Concepts — access modes + the policy model
- Storage → API Reference → Policy — Set / Get / Delete bucket policy
- Storage → API Reference → CORS — Put / Get / Delete bucket CORS
A PUBLIC bucket (or a policy that allows anonymous access) lets the access layer skip auth for matching reads; everything else still requires a credential.
Common failure patterns
On the K3 control plane, auth-layer errors are JSON: {"error": "<message>"}. The standalone object gateway answers in S3’s XML error shape instead — 401 <Code>AccessDenied</Code><Message>access key is unknown or revoked</Message> for an unresolvable key, and 403 for a resolvable key whose signature fails or whose bucket belongs to another org.
401/unknown access key— the SigV4 (or SigV2 query) access key resolved to no credential.401/SigV4 signature mismatch,signature mismatch— the key exists but the computed signature didn’t match.401/missing or invalid Authorization header— no Bearer token on a route that requires one.401/authentication failed: …— the Bearer token failed JWT validation. This is also what you get for adk_API key sent as the control-plane Bearer: the control plane only validates JWTs there.401/token exchange failed: …— the SigV4 key verified, but the client-credentials exchange for its org binding failed.402 Payment Required— credential validated, but the organization is suspended (see above).403— authenticated, but the bucket’s access mode / policy doesn’t grant the action (access denied,write access denied,insufficient permissions for write). Public-bucket reads are the exception; everything else needs a credential.
See also
- Conventions → Auth — the one-line version for HTTP callers
- Storage → S3 Compatibility — SigV4 setup for S3 SDKs
- Storage → GetObjectUrl — issue presigned URLs