SEC-NB-002 — Notebook code can read injected credentials
| Field | Value |
|---|---|
| Status | Open; credential propagation is source-verified, deployed value presence is not captured |
| Architecture state | Source-backed security boundary |
| Severity | Critical |
| Affected surface | Notebook cells and subprocesses running in Enterprise Gateway remote kernels |
| Components | sparqd/bff-api, sparqd/platform-stacks; JupyterHub, Jupyter Server, Enterprise Gateway, Keycloak, and OpenBao are runtime dependencies |
| Associated issue | Not opened |
Where the problem appears
Credentials are placed in the remote kernel's process environment. Ordinary
notebook code can therefore retrieve their raw values with os.environ or
os.getenv(). A shell cell or subprocess can expose the same values through
commands such as printenv.
Concrete observed case
On current main, the BFF exchanges the signed-in user's token for the
workspace-specific JupyterHub audience and submits it as
oauth_access_token in JupyterHub server-start options. The Hub spawn hook:
- copies that bearer token to
OAUTH_ACCESS_TOKEN,OQULLUS_OAUTH_ACCESS_TOKEN, andCOGRION_OAUTH_ACCESS_TOKEN; - exchanges it for an OpenBao client token using the
secret-writerrole and places the result inOPENBAO_TOKEN; and - starts the single-user server with the confidential JupyterHub OAuth client secret under three client-secret aliases.
The single-user server and Enterprise Gateway are both configured to allow the
access-token aliases, OPENBAO_TOKEN, and OAuth client-secret aliases through
the kernel environment handoff. Refresh-token names are also allow-listed, but
the reviewed spawn path does not populate them. The single-user environment
additionally contains the shared Enterprise Gateway authentication token; that
token is not in the reviewed remote-kernel allow-list.
Consequently, a cell does not need to break out of the notebook or exploit Kubernetes to obtain these credentials. Environment lookup is normal Python behavior. Printing the value can then place it in browser output, the saved notebook document, version history, or diagnostic capture.
Expected condition
os.environ, os.getenv(), printenv, child processes, and notebook libraries
must not reveal confidential OAuth client secrets, refresh tokens, or reusable
secret-management tokens. The shared gateway credential remains inside the
trusted Jupyter Server and Enterprise Gateway boundary. If direct user
delegation is required, the exposed token is short-lived, audience- and
workspace-scoped, minimally privileged, and clearly treated as a user-visible
capability. Credential renewal and token exchange occur at a trusted broker.
Cause and evidence
sparqd/bff-apiapplies token exchange for the${workspace}-jupyterhubaudience, then includesreq.exchangedTokenin the JupyterHub start request.sparqd/platform-stacksimplements the spawn hook, OpenBao exchange, static single-user environment, Jupyter client allow-list, and matching Enterprise GatewayEG_CLIENT_ENVSallow-list.- Jupyter Enterprise Gateway client configuration documents that the client allow-list includes named process-environment values in a kernel-start request. The gateway-side client environment list admits those names into the kernel environment.
This proves the configured credential path. A safe deployed check that records
names and token metadata without values remains pending for realm
gj76anadpsc, workspace w-t5e2kcxjd.
Why it matters
Notebook cells execute arbitrary user code and third-party packages. Reading
these credentials requires only os.getenv(), not a privilege escalation. The
values can then be printed into persisted output, copied into logs, inherited
by subprocesses, or sent over the network. A leaked bearer or OpenBao token can
be reused until expiry; a leaked confidential client secret can outlive the
notebook session and affects every user sharing that OAuth client. Kernel
shutdown and idle culling do not revoke credentials already copied elsewhere.
The workspace-specific JupyterHub audience reduces the bearer token's reach, but it does not make a reusable client secret or secret-management token safe inside user-controlled code.
Existing controls and workarounds
- The BFF verifies the incoming JWT and exchanges it for a workspace-specific JupyterHub audience before spawn.
- Kubernetes isolates notebook workloads, and kernel/server culling bounds the lifetime of running processes.
- The reviewed source does not deliberately log token values.
These controls do not protect environment variables from notebook code or
packages running inside the authorized workload. Until the design changes, do
not run printenv or print os.environ in a notebook, avoid persisting token
values in cells or outputs, use only trusted packages, and rotate the shared
OAuth client secret if exposure is suspected.
Alternatives considered
- Recommended: expose a narrow notebook API or local broker. The trusted BFF or sidecar performs token exchange and OpenBao access per operation; client secrets and secret-management tokens never cross into the kernel.
- Where direct SDK access is required, mint a short-lived, single-audience, workspace- and user-bound token with minimal scopes. Deliver no refresh token or confidential client secret, and require re-authentication on expiry.
- Use workload identity for service-to-service infrastructure access, kept separate from user delegation and constrained to the notebook workload.
- Continue injecting reusable credentials as environment variables — rejected because environment access is part of normal notebook code execution, not a defensible secret boundary.
Component involvement
sparqd/bff-apiowns the user-token exchange and the token passed in the Hub server-start request. It is also a candidate boundary for narrowly brokered downstream operations.sparqd/platform-stacksowns the Hub spawn hook, OpenBao exchange, injected single-user environment, and both sides of the Enterprise Gateway environment allow-list.- JupyterHub/Jupyter Server/Enterprise Gateway transport and execute the configured launch environment.
- Keycloak defines token audience, scope, lifetime, exchange policy, and client
type. OpenBao defines the effective authority and lifetime of the
secret-writertoken. - Downstream services must reject tokens with the wrong audience, workspace, user, or scope even if a credential is obtained.
Reproduction and verification
Use a disposable notebook and never print or persist credential values. Do not
run an unfiltered printenv or display os.environ.
-
Record the deployed BFF and platform-stack revisions for
gj76anadpsc/w-t5e2kcxjd. -
In a newly created remote kernel, run a presence-only check such as:
import oscredential_names = ["OAUTH_ACCESS_TOKEN","OAUTH_CLIENT_SECRET","OQULLUS_OAUTH_ACCESS_TOKEN","OQULLUS_OAUTH_CLIENT_SECRET","COGRION_OAUTH_ACCESS_TOKEN","COGRION_OAUTH_CLIENT_SECRET","OPENBAO_TOKEN",]{name: bool(os.getenv(name)) for name in credential_names}Record only the Boolean result. A
trueresult proves that notebook code can retrieve the raw value; do not callos.getenv(name)as cell output. -
Repeat the presence-only check in the personal-server boundary, including refresh-token aliases and the shared Enterprise Gateway token. Do not record any value.
-
For JWT access tokens, decode locally and record only issuer, audience, scopes, issued-at time, and expiry. Do not copy the encoded token or signature into evidence.
-
Confirm that client-secret, OpenBao-token, refresh-token, and shared gateway variable values are absent from the remote kernel. Treat any presence as a failed security check.
-
With an approved synthetic identity, verify one intended operation succeeds, a different audience and workspace are denied, and the credential stops working after expiry or revocation.
-
Search notebook output, autosaved document versions, BFF/Jupyter logs, Spark event logs, and pod termination records for the synthetic marker. Retain no real credential material.
Resolution conditions
Close after confidential client secrets, refresh tokens, and reusable OpenBao tokens are absent from remote kernels and all environments accessible to notebook code; the shared gateway credential is isolated to the trusted server/gateway boundary; any remaining user delegation is short-lived and narrowly scoped; token exchange is brokered at a trusted boundary; downstream audience and workspace denial tests pass; and logs, outputs, history, and diagnostics are proven to redact credentials.
Validation and blockers
The injection and forwarding configuration is source-verified on BFF API
0ac008a4c581 and platform-stacks 0904a8417f05. Deployed presence, effective
Keycloak scopes and lifetimes, OpenBao policy authority, redaction coverage,
revocation behavior, and cross-audience denial remain to be captured without
revealing credential values.