Skip to main content

SEC-NB-002 — Notebook code can read injected credentials

FieldValue
StatusOpen; credential propagation is source-verified, deployed value presence is not captured
Architecture stateSource-backed security boundary
SeverityCritical
Affected surfaceNotebook cells and subprocesses running in Enterprise Gateway remote kernels
Componentssparqd/bff-api, sparqd/platform-stacks; JupyterHub, Jupyter Server, Enterprise Gateway, Keycloak, and OpenBao are runtime dependencies
Associated issueNot 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, and COGRION_OAUTH_ACCESS_TOKEN;
  • exchanges it for an OpenBao client token using the secret-writer role and places the result in OPENBAO_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-api applies token exchange for the ${workspace}-jupyterhub audience, then includes req.exchangedToken in the JupyterHub start request.
  • sparqd/platform-stacks implements the spawn hook, OpenBao exchange, static single-user environment, Jupyter client allow-list, and matching Enterprise Gateway EG_CLIENT_ENVS allow-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​

  1. 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.
  2. 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.
  3. Use workload identity for service-to-service infrastructure access, kept separate from user delegation and constrained to the notebook workload.
  4. 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-api owns 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-stacks owns 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-writer token.
  • 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.

  1. Record the deployed BFF and platform-stack revisions for gj76anadpsc / w-t5e2kcxjd.

  2. In a newly created remote kernel, run a presence-only check such as:

    import os

    credential_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 true result proves that notebook code can retrieve the raw value; do not call os.getenv(name) as cell output.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.