Security and Identity¶
Audience: security engineers evaluating Bernstein for an enterprise deployment.
Overview¶
Bernstein's security model has two axes. The human axis authenticates operators against an identity provider (OIDC, SAML, or local username + password) and enforces RBAC on the FastAPI surface; tokens are JWT, sessions are persisted to .sdd/auth/, and every privileged action is recorded in an HMAC-chained tamper-evident audit log. The agent axis treats every spawned agent as a first-class identity: the orchestrator issues a per-agent JWT scoped to specific tasks and permissions, the API middleware validates that scope on every mutating request, and revocation is one POST.
Authentication is required by default (auth_middleware.py:14-19). The only path to "no auth" is an explicit opt-out (BERNSTEIN_AUTH_DISABLED=1 or auth.enabled: false in bernstein.yaml), which logs a loud warning on startup. SSO providers, RBAC route mapping, identity issuance, audit integrity, drain/export endpoints, and SBOM generation are all in-tree features — there is no separate "enterprise edition" toggle.
Auth providers¶
Three provider families are supported, configured under auth.* in bernstein.yaml and exposed by core/security/auth.py:923-... (AuthService).
| Provider | Config keys | Code |
|---|---|---|
| OIDC | auth.oidc.{enabled,issuer,client_id,client_secret,scopes,redirect_uri} | core/security/sso_oidc.py, routes/auth.py:172-261 |
| SAML 2.0 | auth.saml.{enabled,sp_entity_id,idp_metadata_url,...} | core/security/auth.py (SAML helpers), routes/auth.py:269-316 |
| Local users | auth.users[] (admin-managed via /auth/users) | core/security/auth.py (AuthUserStore), routes/auth.py:494-520 |
Group-to-role mappings are surfaced via GET /auth/group-mappings and modified by admins via PUT /auth/group-mappings (routes/auth.py:443-491). They map IdP group claims (e.g. bernstein-admins) to one of the three Bernstein roles.
A fourth path — legacy bearer tokens — exists for backwards compatibility (auth_middleware.py:7). It accepts a single shared secret configured by BERNSTEIN_AUTH_TOKEN. Treat it as a transitional mechanism; SSO + JWT is the supported deployment.
The /auth/providers endpoint returns which providers are enabled (routes/auth.py:147-164); use this to drive a self-describing login UI.
JWT lifecycle¶
Token implementation: core/security/jwt_tokens.py:31-93 (JWTManager). Default algorithm: HS256 with a 24-hour expiry; both knobs live on JWTConfig and are owned by the operator.
Issuance. Tokens are minted by JWTManager.create_token(session_id, user_id, scopes). Three issuers exist:
- Operator login — OIDC/SAML callback (
routes/auth.py:212-261,:269-308) returns an HTML page that stores the token inlocalStorage. Device flow (/auth/cli/device,/auth/cli/token) issues the same token via polling for CLI-based logins (routes/auth.py:324-372). - Agent identity —
core/agent_identity.pyissues task-scoped JWTs with claims{session_id, user_id=identity_id, task_ids: [...], permissions: [...]}. Stored in.sdd/auth/identities/. - Cluster nodes —
ClusterAuthenticator.issue_node_token(node_id)(core/protocols/cluster/cluster_auth.py:70-93); see Cluster mode.
Refresh. POST /auth/refresh (mounted alongside /auth/token per A2's endpoint inventory) re-issues a token without re-authenticating, as long as the prior session is still valid. Internally this is a fresh create_token against the existing session record; expired sessions are rejected.
Validation. Every protected request goes through AuthMiddleware.dispatch() (auth_middleware.py:160+), which:
- Skips
AUTH_PUBLIC_PATHS(auth_middleware.py:67-89) —/health,/.well-known/..., the login flow itself. - Decodes the bearer token via
JWTManager.verify_token()(jwt_tokens.py:78-93) which returnsNoneon bad signature or expiry. - Resolves the user (operator) or identity (agent), populates
request.state.user/request.state.identity, and enforcestask_idsscoping for/tasks/{id}/{complete,fail,progress,cancel, block,steal}paths (auth_middleware.py:55). - Returns
JSONResponse(401)on any verification failure.
Revocation.
- Operators:
POST /auth/logoutcallsAuthService.logout(session_id)(routes/auth.py:424-435), which setssession.revoked = Truein the session store. Subsequent requests with the same JWT fail validation. - Agent identities:
POST /identities/{id}/revoke(routes/identities.py:91-103). - Cluster nodes:
ClusterAuthenticator.revoke_token()/revoke_node()(cluster_auth.py:174-191).
Operators with the auth:manage permission can also force-logout other users via DELETE /auth/users/{id} (routes/auth.py:499-520).
RBAC¶
Three built-in roles in strict privilege order (core/security/auth.py:81-87):
- admin — full access, including
auth:manage,config:write,admin:manage(which gates shutdown/broadcast/drain),agents:kill. - operator — task and agent management, no config or user changes. Can write tasks, kill agents, manage cluster nodes, post to bulletin.
- viewer — read-only access to tasks, agents, status, costs, bulletin.
Per-role permission table (core/security/auth.py:90-139):
| Permission | admin | operator | viewer |
|---|---|---|---|
tasks:read | yes | yes | yes |
tasks:write | yes | yes | no |
tasks:delete | yes | no | no |
agents:read | yes | yes | yes |
agents:write | yes | yes | no |
agents:kill | yes | yes | no |
cluster:read | yes | yes | yes |
cluster:write | yes | no | no |
config:read | yes | no | no |
config:write | yes | no | no |
auth:manage | yes | no | no |
webhooks:manage | yes | no | no |
costs:read | yes | yes | yes |
bulletin:read | yes | yes | yes |
bulletin:write | yes | yes | no |
admin:manage | yes | no | no |
admin:manage is the kill-switch: shutdown, broadcast, drain, and the config writer all require it. Only ADMIN holds it by design (core/security/auth.py:109-113).
RBAC is enforced at the route level by RBACEnforcer (core/security/rbac.py:118-...), which maps URL prefixes + HTTP methods to required permissions. Default rules (core/security/rbac.py:79-115) cover /auth/users, /config, /webhooks, /cluster, /agents, /tasks, /bulletin, /costs, /status, /health. Order matters — first match wins — and additional rules can be passed in via RBACEnforcer(extra_rules=...).
To add a custom rule, append a RoutePermission(path_prefix, method, permission) (rbac.py:64-76) to the enforcer's extra rules at server startup. Mention the new permission in _ROLE_PERMISSIONS if existing roles should hold it; otherwise it is denied by default.
Policy engine¶
For decisions that go beyond simple route-level RBAC — for example "ask human before letting an agent edit migrations/", or "deny secret-file edits regardless of role" — Bernstein has a layered policy engine (core/security/policy_engine.py). It evaluates PermissionDecision records in this precedence order (policy_engine.py:29-36):
- DENY — mandatory block, bypass-immune.
- IMMUNE — safety-critical paths (e.g.
.git, key files), bypass-immune. - SAFETY — secret detection, bypass-immune.
- ASK — requires human approval (surfaces in
/approvals/queue). - ALLOW — permitted to proceed.
YAML rules live under policy: in bernstein.yaml and are loaded by PolicyEngine; optional Rego rules can be merged in via the OPA integration in policy_engine.py. The engine is also where command allowlists (command_allowlist.py), DLP scanning (dlp_scanner.py, dlp_scanner_v2.py), and PII output gates (pii_output_gate.py) plug in.
Multi-tenant isolation¶
Source: core/security/tenant_isolation.py, core/security/tenanting.py. Bernstein supports tenant-scoped data paths (tenant_isolation.py:1-5) where every tenant gets its own .sdd/{tenant_id}/{backlog,metrics,runtime/wal,audit} subtree (tenant_isolation.py:44-60). All task queries, WAL writes, and audit-log writes are filtered by tenant ID, and tenant resolution happens at the API edge via request_tenant_id() / resolve_tenant_scope() (core/tenanting.py).
When auth is configured, tenant scoping is automatic from JWT claims; unauthenticated dev mode falls back to DEFAULT_TENANT_ID. Operators audit cross-tenant leakage with tenant_isolation_verify.py and rate- limit per-tenant via tenant_rate_limiter.py.
Identities API¶
Agent identities are how Bernstein implements zero-trust spawning. Every agent gets its own JWT with explicit task_ids and permissions; the auth middleware refuses to let an agent mutate a task it wasn't issued for. The identities surface lives at core/routes/identities.py.
| Endpoint | Purpose | Code |
|---|---|---|
GET /identities | List identities. Filters: status, role. Returns id, role, session_id, status, permissions, created_at, parent_identity_id. | routes/identities.py:35-64 |
GET /identities/{id} | Full identity record (credential hash redacted before serialisation). | routes/identities.py:72-83 |
POST /identities/{id}/revoke | Revoke an agent identity. Body {reason: "..."}. Future requests with the identity's JWT fail. | routes/identities.py:91-103 |
GET /identities/{id}/audit | Per-identity audit trail. Returns the identity's events from the audit store. ?limit=100 default. | routes/identities.py:111-122 |
Backing store: core/agent_identity.py (AgentIdentityStore) under .sdd/auth/. The store is created lazily on first request (routes/identities.py:17-27). Credentials are stored hashed; the API strips them before responses (:82).
Audit log¶
The audit log is append-only, daily-rotated, and HMAC-chained (core/security/audit.py:1-15). Every event embeds an HMAC computed over the previous event's HMAC and the current event's payload, forming a hash chain that breaks if any record is rewritten or deleted.
Storage. One JSONL file per UTC day in .sdd/audit/YYYY-MM-DD.jsonl. Default retention: 90 days (audit.py:40).
Key handling. The HMAC key lives outside the audit directory so an attacker with write access to the JSONL files cannot also read or rotate the signing key (audit.py:6-14). Default location: $XDG_STATE_HOME/bernstein/audit.key, falling back to ~/.local/state/bernstein/audit.key. Override with the BERNSTEIN_AUDIT_KEY_PATH environment variable (audit.py:43). The key file is required to be mode 0600; group- or world-readable keys fail at load time (audit.py:71-86).
Integrity verification. On orchestrator startup, audit_integrity.py:DEFAULT_VERIFY_COUNT=100 events are walked and their HMAC chain re-checked (core/security/audit_integrity.py:1-30). Failures produce structured warnings that can be alerted on. To force a full check across all entries, call the helpers in audit_integrity.py directly.
Querying and export. GET /audit (core/routes/audit_log.py:92-...) supports filtering by event_type, ISO timestamp range (from, to), full-text search, and pagination (page, page_size up to 200). The filter logic is audit_log.py:40-74.
For SOC 2 evidence collection, pair GET /audit with GET /identities/{id}/audit for per-identity views, and verify the HMAC chain out-of-band before exporting. See Audit and SOC 2 evidence for the compliance narrative.
Drain and export¶
These are operator-side primitives intended for graceful shutdown, incident response, and compliance evidence export.
Drain (core/routes/drain.py): freeze new task claiming so existing agents finish without picking up new work. Three endpoints:
| Endpoint | Effect |
|---|---|
POST /drain | Sets app.state.draining = True. Response includes active_agents (claimed tasks). |
POST /drain/cancel | Resets draining = False. |
GET /drain | Returns current draining flag and active-agent count. |
The orchestrator's task-claim path checks app.state.draining and refuses to assign new work while it is set. Combine with /cluster/nodes/{id}/drain for a multi-node graceful shutdown — see Cluster mode.
Export (core/routes/export.py):
| Endpoint | Purpose |
|---|---|
GET /export/tasks?format=csv | All tasks as CSV or JSON (default). Fields: id, title, description, role, priority, status, assigned_agent, created_at, completed_at (export.py:23-33). |
GET /export/agents?format=csv | Agent snapshot from .sdd/runtime/agents.json. Fields: id, role, status, task_id, started_at (export.py:35-41). |
Both endpoints stream as Content-Disposition: attachment so they're safe to bookmark from a browser.
SBOM generation¶
core/routes/sbom.py exposes on-demand Software Bill of Materials generation for supply-chain compliance.
POST /sbom/generate— produce a CycloneDX or SPDX JSON SBOM from installed packages, optionally run vulnerability scanning viaosv-scannerorgrype, and gate the response on critical findings (sbom.py:122-214). Body fields:sbom_format,source,run_scan,block_on_critical. Response 422 whenblock_on_critical=trueand any CRITICAL vulnerability is present.GET /sbom/artifacts— list previously generated SBOM JSON artifacts from.sdd/artifacts/sbom/(sbom.py:217-247).
Generator implementation: core/security/sbom.py (SBOMGenerator, SBOMVulnerabilityGate). For scheduled SBOM emission and CI integration, the same primitives are exposed as a bernstein audit subcommand and as gates inside the Quality pipeline.
Compliance¶
Bernstein's compliance posture is a composition of the above primitives plus configurable policy. Rather than duplicate it here, see:
- Model policy — model allowlist/denylist, residency, cost ceilings, and the cascade-router escalation rules that interact with regulated workloads.
- Audit and SOC 2 evidence — the canonical walkthrough of the audit log, integrity proofs, and SOC 2 control mapping.
- Security hardening — sandbox hardening, allow-listed commands, and DLP scanning.
Compliance modules in code (core/security/):
eu_ai_act.py— EU AI Act risk assessment helpers.hipaa.py— HIPAA PHI gates.soc2_report.py— SOC 2 evidence packaging.compliance.py,compliance_policies.py,compliance_report.py— shared policy engine surfaced by thebernstein complianceCLI group (cli/commands/compliance_cmd.py).
Code pointers¶
| Concern | File |
|---|---|
| Auth middleware (every request) | src/bernstein/core/security/auth_middleware.py |
| AuthService, RBAC, role table | src/bernstein/core/security/auth.py |
| RBAC route enforcement | src/bernstein/core/security/rbac.py |
| JWT manager | src/bernstein/core/security/jwt_tokens.py |
| OIDC / SAML / device flow routes | src/bernstein/core/routes/auth.py |
| Agent identities API | src/bernstein/core/routes/identities.py |
| Agent identity store | src/bernstein/core/agent_identity.py |
| Audit log (HMAC chain) | src/bernstein/core/security/audit.py |
| Audit integrity verifier | src/bernstein/core/security/audit_integrity.py |
| Audit query / search routes | src/bernstein/core/routes/audit_log.py |
| Drain endpoints | src/bernstein/core/routes/drain.py |
| Export endpoints | src/bernstein/core/routes/export.py |
| SBOM endpoints | src/bernstein/core/routes/sbom.py |
| SBOM generator | src/bernstein/core/security/sbom.py |
| Cluster JWT auth | src/bernstein/core/protocols/cluster/cluster_auth.py |
| Compliance frameworks | src/bernstein/core/security/{eu_ai_act,hipaa,soc2_report}.py |
| OAuth / SSO config | src/bernstein/core/security/sso_oidc.py, oauth_pkce.py |
| Vault / secrets | src/bernstein/core/security/vault/, vault_injector.py |
| Tenant isolation | src/bernstein/core/security/tenant_isolation.py, tenanting.py |
| Permission modes (Claude profiles) | src/bernstein/core/security/{permission_mode,permission_matrix,permission_rules}.py |