Skip to content

Commit b0a30dd

Browse files
committed
feat(sandbox): add delegated identity for token exchange
Signed-off-by: Gordon Sim <gsim@redhat.com>
1 parent 5b9daab commit b0a30dd

54 files changed

Lines changed: 13078 additions & 3547 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎architecture/compute-runtimes.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,8 @@ would allow them to claim an existing sandbox identity.
586586
The Kubernetes Secrets credential driver (`openshell-driver-kubernetes-secrets`)
587587
stores secrets in workspace-specific namespaces when `workspace_mode` is managed
588588
or operator. In shared mode, all secrets render into the single configured
589-
namespace.
589+
namespace. Gateway-global secret material has no workspace and always renders
590+
into the configured credential namespace in every workspace mode.
590591

591592
When runtime infrastructure changes, validate the relevant sandbox e2e path and
592593
update the matching driver README if a maintainer-facing constraint changes.

‎architecture/gateway.md‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,17 @@ state is stored as a separate object scoped to the provider instance through
455455
`objects.scope`. Its non-secret configuration remains inline, while refresh
456456
tokens, client secrets, private keys, and other secret source material are
457457
stored through the active credential driver and represented by opaque handles.
458+
Delegated identity credentials use the same boundary: access and refresh token
459+
bytes exist only in the selected credential backend, while the generic
460+
delegated credential object retains issuer, subject, expiry, revocation state,
461+
and opaque handles. The sandbox record contains only its authorization window
462+
and a reference to that generic credential object. The CLI obtains delegated
463+
material through a browser Authorization Code flow with PKCE separate from its
464+
normal login. It transfers that bundle without writing it to the local login
465+
store and continues to authenticate gateway RPCs with the normal login. The
466+
gateway is therefore the sole owner of the delegated refresh-token chain. A
467+
healthy gateway-owned grant is shared by that user’s delegated sandboxes;
468+
create and extend carry only the requested authorization window.
458469
The provider record keeps only the current injectable credential handles and
459470
optional per-credential expiry timestamps. A refresh normally mints one
460471
credential, but a strategy may co-mint several (AWS STS mints the access key,
@@ -468,6 +479,24 @@ credential storage for defense in depth. Multi-replica deployments can use that
468479
default with a shared database and shared key-encryption key, or opt into an
469480
external backend such as Vault or Kubernetes Secrets.
470481

482+
Credential-driver writes and generic object CAS writes form a compensating
483+
transaction. New delegated material is staged before its handles are committed.
484+
A failed CAS deletes the staged handles; a successful replacement records old
485+
handles as pending deletion before cleanup. Revocation and deletion first clear
486+
active handles and make the generic credential unusable, then delete backend
487+
material. Pending opaque handles remain durable across cleanup failures and a
488+
gateway worker retries them. Final deletion keeps a disabled tombstone until
489+
all backend material is gone, so a partial failure cannot silently orphan a
490+
reusable token.
491+
492+
Delegated credential refresh also uses a short-lived lease in the generic
493+
credential record. Gateway replicas acquire the lease with object-store CAS
494+
before resolving or submitting the refresh token. Non-owners reload the record
495+
until the owner commits the rotated handles, and an expired lease permits
496+
recovery from a failed replica. The final CAS verifies that the credential is
497+
still active and that the writer still owns the lease, so revocation cannot race
498+
with refresh and restore usable token handles.
499+
471500
Sandbox workload templates are workspace-scoped gateway resources. Workspace
472501
admins create and delete them; workspace users can read and list them. A
473502
template owns reusable workload intent: image, environment, CPU and memory
@@ -490,7 +519,9 @@ short-lived credentials still fail closed at their recorded expiry.
490519
Credential handles remain bound to the driver that created them. Before the
491520
0.1.0 compatibility boundary, gateways do not migrate inline refresh material
492521
or move handles between credential drivers; operators reconfigure affected
493-
grants when upgrading or changing backends.
522+
grants when upgrading or changing backends. Delegated identity has not shipped
523+
with inline token storage, so development deployments created from an earlier
524+
feature branch must delete and recreate those branch-only records.
494525

495526
### Optimistic Concurrency (CAS)
496527

‎architecture/sandbox-limits.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ can contain a valid sequence gap.
128128
| Corporate proxy CONNECT response headers | 8 KiB | Fail the tunnel. |
129129
| Corporate proxy CONNECT handshake | 30 s total | Fail the tunnel; validated-address attempts share the aggregate budget. |
130130
| Token-grant HTTP request | 30 s request and connect | Fail credential resolution. |
131-
| Response-derived token cache TTL | 5 min default; 1 h response cap; 30 s expiry margin | A positive profile `cache_ttl_seconds` override replaces the response-derived calculation. |
131+
| Client-credentials token cache TTL | 5 min default; 1 h response cap; 30 s expiry margin | A positive profile `cache_ttl_seconds` override replaces the response-derived calculation without changing existing client-credentials behavior. |
132+
| Token-exchange token cache TTL | 5 min maximum | The cap applies to positive profile overrides. Response-derived TTLs apply the 30 s expiry margin after the cap, for a 4 min 30 s maximum. This bounds continued token injection after delegated-identity withdrawal or credential revocation. |
132133

133134
## Sandbox-Local Surfaces
134135

‎architecture/sandbox.md‎

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -374,20 +374,54 @@ Provider profiles can also declare dynamic token grants. For matching HTTP
374374
endpoints, the supervisor obtains or exchanges OAuth2 access tokens, caches
375375
them, and injects them before forwarding the request. `client_credentials`
376376
grants use the supervisor SPIFFE JWT-SVID directly as the client assertion.
377-
`token_exchange` grants ask the gateway to broker an intermediate token using a
378-
stored provider subject credential and the gateway's own SPIFFE JWT-SVID; the
379-
supervisor then exchanges that intermediate token for the final upstream token
380-
using its own JWT-SVID. The gateway validates that its own JWT-SVID has the
377+
`token_exchange` grants ask the gateway to broker an intermediate token using
378+
either a stored provider subject credential or the sandbox creator's delegated
379+
OIDC identity, plus the gateway's own SPIFFE JWT-SVID; the supervisor then
380+
exchanges that intermediate token for the final upstream token using its own
381+
JWT-SVID. Delegated identity is opt-in at sandbox creation. The CLI first checks
382+
for a usable gateway-owned credential for the authenticated user. When one is
383+
missing, revoked, or no longer refreshable, the CLI runs a separate device
384+
authorization and transfers its token bundle to the gateway without replacing
385+
the normal CLI login. Sandbox create and extend then send only a per-sandbox
386+
authorization window. Delegated access and refresh tokens live behind opaque
387+
handles in the gateway's active credential driver; the generic credential and
388+
sandbox records never contain their bytes. Rotation stages replacement handles
389+
before updating metadata and uses a shared-store CAS lease so gateway replicas
390+
cannot submit the same refresh token concurrently. Revocation disables
391+
resolution before cleanup, and deletion retains a disabled tombstone until
392+
cleanup completes. Within one gateway process, sandbox creation and delegation
393+
extension share the gateway-wide state guard with credential deletion so a
394+
credential cannot be deleted between its active-reference check and a new
395+
delegation write. Like the gateway's other cross-object state guards, this
396+
coordination is process-local pending DB-backed multi-writer synchronization.
397+
Only the delegating user can
398+
extend or withdraw that window. Only the delegator can start or interact with
399+
the sandbox. Workspace and platform admins can stop it for containment or
400+
delete it to recover resources, but cannot start it. Other workspace users
401+
cannot start, stop, delete, or interact with it. Ordinary sandboxes retain the
402+
workspace role model. The gateway rejects exchange after expiry, withdrawal,
403+
missing credential state, or credential revocation.
404+
The gateway validates that its own JWT-SVID has the
381405
requested audience, a SPIFFE subject, and a non-expired `exp` claim when
382-
present. It also validates that the stored subject credential is declared by the
383-
provider profile, and that the supervisor JWT-SVID is a well-formed
406+
present. For provider-credential subject tokens, it also validates that the
407+
stored subject credential is declared by the provider profile. For delegated
408+
identity subject tokens, it validates that the sandbox was created with active
409+
delegation for the stored credential principal. The gateway also verifies that
410+
the supervisor JWT-SVID is a well-formed
384411
three-segment JWT with a SPIFFE subject in the same trust domain as the gateway
385412
SVID. The gateway verifies the supervisor JWT-SVID signature with JWT bundles
386413
fetched from its SPIFFE Workload API. Token grant endpoints are HTTPS-only
387414
except for loopback and Kubernetes service DNS hosts, and returned access tokens
388-
must be bearer-compatible before they are cached or injected. Token response
415+
must be bearer-compatible before they are cached or injected. Delegated OIDC
416+
discovery and refresh use a dedicated 30-second HTTP client that ignores ambient
417+
proxy settings and rejects redirects. Token response
389418
lifetimes are capped and cached with an expiry margin unless a profile supplies
390-
an explicit cache TTL override. Cache entries are scoped by the sandbox provider
419+
an explicit cache TTL override. Every `token_exchange` cache entry, including
420+
an override, is capped at five minutes so withdrawal and gateway credential
421+
revocation bound continued token injection. Response-derived entries apply the
422+
30-second margin after that cap and therefore live for at most four minutes and
423+
30 seconds. This bound does not invalidate an access token that an upstream
424+
service has already received. Cache entries are scoped by the sandbox provider
391425
environment revision so provider credential updates miss the old token cache
392426
without changing endpoint matching semantics. Gateway-brokered intermediate
393427
tokens are cached separately by provider resource version, supervisor SPIFFE

‎crates/openshell-cli/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,4 @@ serde_json = { workspace = true }
9797
temp-env = "0.3"
9898
tokio-stream = { workspace = true }
9999
url = { workspace = true }
100+
wiremock = "0.6"

0 commit comments

Comments
 (0)