Skip to content

Commit 6859f92

Browse files
committed
refactor(auth): remove sandbox token revocation
1 parent 4e8ce7d commit 6859f92

21 files changed

Lines changed: 158 additions & 291 deletions

File tree

architecture/gateway.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ Sandbox supervisor RPCs authenticate with either mTLS material or a sandbox
4646
secret depending on the runtime and deployment mode. User-facing mutations are
4747
authorized by role policy when OIDC or edge identity is enabled.
4848

49+
Sandbox secrets are gateway-signed JWTs bound to a single sandbox ID. Docker,
50+
Podman, and VM drivers deliver the initial token through supervisor-only
51+
runtime material; Kubernetes supervisors exchange a projected ServiceAccount
52+
token through `IssueSandboxToken`. Supervisors renew gateway JWTs in memory
53+
before expiry. Older tokens are not server-revoked; deployments bound replay
54+
exposure with short `gateway_jwt.ttl_secs` lifetimes.
55+
4956
## API Surface
5057

5158
The gateway API is organized around platform objects and operational streams:

crates/openshell-core/src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ pub struct GatewayJwtConfig {
342342
/// hostname-or-`openshell` placeholder if unset.
343343
#[serde(default = "default_gateway_id")]
344344
pub gateway_id: String,
345-
/// Token lifetime in seconds. Defaults to 24 hours.
345+
/// Token lifetime in seconds. Defaults to 1 hour.
346346
#[serde(default = "default_sandbox_token_ttl_secs")]
347347
pub ttl_secs: u64,
348348
}
@@ -352,7 +352,7 @@ fn default_gateway_id() -> String {
352352
}
353353

354354
const fn default_sandbox_token_ttl_secs() -> u64 {
355-
86_400
355+
3_600
356356
}
357357

358358
fn default_roles_claim() -> String {

crates/openshell-sandbox/src/debug_rpc.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//! flow (issue #1354). A `docker exec` (or `kubectl exec`) into a
88
//! running sandbox can issue raw sandbox-class gRPC calls without
99
//! standing up a custom binary inside the sandbox image — useful for
10-
//! confirming the cross-sandbox IDOR guard and refresh semantics.
10+
//! confirming the cross-sandbox IDOR guard and renewal semantics.
1111
//!
1212
//! Subcommands:
1313
//! - `get-sandbox-config --sandbox-id <id>` — call `GetSandboxConfig`
@@ -53,7 +53,7 @@ usage: openshell-sandbox debug-rpc <command> [options]
5353
5454
commands:
5555
get-sandbox-config --sandbox-id <UUID> call GetSandboxConfig
56-
refresh call RefreshSandboxToken
56+
refresh renew the gateway JWT
5757
show-token print raw gateway JWT
5858
show-principal print decoded JWT claims
5959

crates/openshell-sandbox/src/grpc_client.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ use tracing::{debug, info, warn};
4343
pub type AuthedChannel = InterceptedService<Channel, AuthInterceptor>;
4444

4545
/// Shared, refreshable Bearer header. All [`AuthInterceptor`] clones read
46-
/// the same slot, so the PR-5 refresh task can rotate the token in place
47-
/// without rebuilding the channel.
46+
/// the same slot, so the renewal task can replace the token in place without
47+
/// rebuilding the channel.
4848
type TokenSlot = Arc<RwLock<AsciiMetadataValue>>;
4949

5050
/// Process-wide token slot. Initialized by the first [`connect_channel`]
51-
/// call and shared with every subsequent client + the refresh loop.
51+
/// call and shared with every subsequent client and the renewal loop.
5252
static TOKEN_SLOT: OnceLock<TokenSlot> = OnceLock::new();
5353

54-
/// One-shot guard so the refresh loop spawns at most once per process.
54+
/// One-shot guard so the renewal loop spawns at most once per process.
5555
static REFRESH_SPAWNED: OnceLock<()> = OnceLock::new();
5656

5757
fn install_token_slot(token: &str) -> Result<TokenSlot> {
@@ -68,8 +68,8 @@ fn install_token_slot(token: &str) -> Result<TokenSlot> {
6868
}
6969

7070
/// gRPC interceptor that injects `authorization: Bearer <token>` on every
71-
/// outbound request. The token lives in a shared [`TokenSlot`] so the
72-
/// PR-5 refresh task can replace it without rebuilding clients.
71+
/// outbound request. The token lives in a shared [`TokenSlot`] so the renewal
72+
/// task can replace it without rebuilding clients.
7373
#[derive(Clone)]
7474
pub struct AuthInterceptor {
7575
bearer: TokenSlot,
@@ -162,9 +162,9 @@ async fn build_plain_channel(endpoint: &str) -> Result<Channel> {
162162
/// First call per process resolves the sandbox JWT via the three-step
163163
/// lookup (env → file → K8s SA bootstrap exchange) and installs it into
164164
/// the process-wide [`TOKEN_SLOT`]. Subsequent calls reuse the cached
165-
/// slot — the refresh loop keeps the value fresh, so re-running the
165+
/// slot — the renewal loop keeps the value fresh, so re-running the
166166
/// bootstrap is both unnecessary and (on the K8s SA path) expensive
167-
/// (one apiserver round-trip per call). The refresh loop itself is
167+
/// (one apiserver round-trip per call). The renewal loop itself is
168168
/// spawned once per process via [`REFRESH_SPAWNED`].
169169
async fn connect_channel(endpoint: &str) -> Result<AuthedChannel> {
170170
let channel = build_plain_channel(endpoint).await?;
@@ -250,7 +250,7 @@ pub async fn connect_channel_pub(endpoint: &str) -> Result<AuthedChannel> {
250250
connect_channel(endpoint).await
251251
}
252252

253-
/// Background task that rotates the sandbox JWT at ~80% of its remaining
253+
/// Background task that renews the sandbox JWT at ~80% of its remaining
254254
/// lifetime. The new token replaces the value in [`TOKEN_SLOT`], so all
255255
/// in-flight and future clients pick it up on their next request. The
256256
/// loop never panics: every failure is logged and re-attempted after a
@@ -270,7 +270,7 @@ async fn refresh_token_loop(channel: AuthedChannel, slot: TokenSlot) {
270270
Ok(value) => {
271271
if let Ok(mut guard) = slot.write() {
272272
*guard = value;
273-
info!("rotated gateway sandbox JWT in-place");
273+
info!("renewed gateway sandbox JWT in-place");
274274
}
275275
}
276276
Err(e) => warn!(error = %e, "refreshed JWT contained invalid header bytes"),

crates/openshell-server/src/auth/guard.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ mod tests {
8989
sandbox_id: id.to_string(),
9090
source: SandboxIdentitySource::BootstrapJwt {
9191
issuer: "openshell-gateway:test".to_string(),
92-
jti: "j-1".to_string(),
9392
},
9493
trust_domain: Some("openshell".to_string()),
9594
})

crates/openshell-server/src/auth/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ pub mod identity;
1616
pub mod k8s_sa;
1717
pub mod oidc;
1818
pub mod principal;
19-
pub mod revocation;
2019
pub mod sandbox_jwt;
2120

2221
pub use http::router;

crates/openshell-server/src/auth/principal.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub struct SandboxPrincipal {
7070
pub enum SandboxIdentitySource {
7171
/// Gateway-minted JWT validated against the gateway's signing key.
7272
/// Produced by [`super::sandbox_jwt::SandboxJwtAuthenticator`].
73-
BootstrapJwt { issuer: String, jti: String },
73+
BootstrapJwt { issuer: String },
7474
/// Per-sandbox client certificate. Reserved for the v2 channel-bound
7575
/// identity follow-up.
7676
BootstrapCert { fingerprint: String },

crates/openshell-server/src/auth/revocation.rs

Lines changed: 0 additions & 100 deletions
This file was deleted.

0 commit comments

Comments
 (0)