Enterprise Auth and Tenant Isolation¶
You do not need to read this unless you are wiring SSO, SCIM, trusted reverse-proxy identity, or tenant propagation on a self-hosted deployment. Default API-key auth works without this page.
This page is the operator-facing contract for how agent-bom handles:
- API keys
- OIDC
- SAML SSO
- trusted reverse-proxy identity
- RBAC
- tenant propagation
- tenant defaults and fail-closed behavior
The goal is simple: identity, authorization, and tenant scoping should all resolve onto the same control-plane model.
For the browser-to-API trust split inside the self-hosted control plane, see UI, API, Auth, and Session Model.
For the default self-hosted contract on who can see tenant logs, audit, and support data, see Customer Data and Support Boundary.
What is especially strong¶
- Tenant context is not UI-only. It propagates from the authenticated request into the control-plane stores and Postgres row-level security.
- Authentication, RBAC, and tenant scope are one model. API keys, OIDC, SAML, and trusted proxy identity all converge onto the same backend authorization and tenant-propagation path.
- Audit is tenant-scoped and tamper-evident. Audit entries are HMAC-chained per tenant and can be filtered/exported by tenant.
- SAML and OIDC converge onto the same RBAC + tenant model. SAML does not invent a second authorization path; it mints short-lived control-plane keys that the existing middleware already understands.
- Operators can introspect live auth resolution.
GET /v1/auth/debugshows auth method, subject, role, tenant, and trace IDs without exposing raw secrets.
What is not perfect¶
- SAML is intentionally narrow. It is an assertion-verification path that returns a short-lived API key. It is not a full browser session framework with logout/session federation depth.
- Dashboard OIDC auth-code / PKCE is available; gateway laptop PKCE is later.
The control-plane dashboard can complete first-party OIDC login via
/v1/auth/oidc/login(PKCE S256) whenAGENT_BOM_OIDC_CLIENT_IDandAGENT_BOM_OIDC_REDIRECT_URIare set alongside the issuer. Per-user OAuth2 auth-code / PKCE for laptop-to-gateway MCP flows remains a later runtime surface. Reverse-proxy SSO is still preferred when present. mTLS is transport posture only — not user identity. - Good enterprise posture still depends on good IdP mapping. Claim naming, tenant binding, and deployment configuration matter almost as much as the code paths.
- Current multi-tenancy is strong for self-hosted teams, not yet turnkey MSSP. Tenant isolation is enforced in auth, stores, audit, fleet, and now shared gateway routing, but provider-style tenant lifecycle automation and richer delegation/admin surfaces are still later work. That is a separate maturity track, not something the self-hosted deployment story is trying to imply.
Auth modes¶
| Mode | Best for | Tenant source | Role source | Notes |
|---|---|---|---|---|
| API key | machine-to-machine, automation, internal service accounts | key metadata | key metadata | hashed at rest; role hierarchy enforced in middleware |
| OIDC bearer JWT | API callers with corporate IdP tokens | JWT tenant claim or tenant-bound issuer | JWT role claim / groups | issuer + audience verified; tenant defaults fail closed by default |
| OIDC browser SSO | Dashboard users without a reverse-proxy identity bridge | same OIDC tenant claim contract | same OIDC role claim / groups | auth-code + PKCE; mints httpOnly session cookie; auth runtime mode oidc_browser |
| SAML | enterprises that need SAML IdP compatibility | SAML attribute | SAML attribute | assertion is verified, then converted into a short-lived API key |
| Trusted proxy | same-origin ingress or auth gateway in front of API | X-Agent-Bom-Tenant-ID |
X-Agent-Bom-Role |
enable AGENT_BOM_TRUST_PROXY_AUTH=1; set exact AGENT_BOM_TRUSTED_PROXY_HOPS and API-facing AGENT_BOM_TRUSTED_PROXY_CIDRS before forwarded addresses affect auth rate limits |
| mTLS | proxy/gateway → API transport | n/a | n/a | not an identity path |
Credential display and detection¶
Error and log sanitization redact recognizable bearer credentials and generated
abom_ API keys. The file scanner and runtime credential detector also recognize
short tokens in explicit bearer headers. Detection identifies exposed credential
material; it does not prove that a token is valid. Runtime blocking still follows
the configured proxy or gateway policy.
RBAC model¶
agent-bom keeps the role model intentionally small:
adminanalystviewer
The permission matrix lives in
src/agent_bom/rbac.py
and the route minimum-role map lives in
src/agent_bom/api/middleware.py.
Operationally:
admincan manage keys, policies, fleet writes, and configurationanalystis the current backend role that the UI should present as a contributor operator role; it can run scans, push observability/runtime data, and create exceptionsvieweris read-only
The UI/session contract for that mapping now comes from:
GET /v1/auth/me
That endpoint returns:
- the authenticated actor and active tenant
- the backend role (
admin/analyst/viewer) - the UI role label (
admin/contributor/viewer) - capability and access summaries the browser can use to explain:
- what the actor can see
- what the actor can do
- what remains blocked server-side
Tenant propagation¶
The current tenant boundary is enforced in three places:
- Request state
- the auth middleware resolves
request.state.tenant_id - Store calls
- control-plane routes pass the request tenant into store reads/writes
- Postgres session + RLS
app.tenant_idis set on the Postgres session and RLS policies enforce the same boundary at the database layer
That means tenant scoping is not just a UI filter. It is part of the control plane and persistence contract.
OIDC claim-to-tenant mapping¶
Valid bearer access tokens can be reused until expiry. Each request still validates the token signature, issuer, audience and time claims. Browser ID-token exchanges keep single-use replay protection, scoped to the issuer, audience and login nonce.
The configured role claim takes precedence over group membership, including an
explicit viewer role. If that claim is present but malformed or unrecognized,
groups cannot elevate it: required-role validation rejects authentication;
configurations that permit a missing role grant only viewer access. Group
mapping applies only when the configured role claim is absent.
The OIDC knobs are:
export AGENT_BOM_OIDC_ISSUER="https://idp.example.com"
export AGENT_BOM_OIDC_AUDIENCE="agent-bom"
export AGENT_BOM_OIDC_ROLE_CLAIM="agent_bom_role"
export AGENT_BOM_OIDC_TENANT_CLAIM="tenant_id"
How tenant resolution works:
- if the configured tenant claim exists in the JWT, use it
- if the issuer is configured as a tenant-bound provider, use the bound tenant
- if
AGENT_BOM_OIDC_REQUIRE_TENANT_CLAIM=1, fail closed - otherwise, fail closed unless
AGENT_BOM_OIDC_ALLOW_DEFAULT_TENANT=1 - only with that explicit opt-in does the request resolve to
default
So the safe/default behavior is now:
- missing tenant claim = reject
Single-tenant compatibility mode is explicit, not silent.
Tenant-bound issuer mode¶
For stronger enterprise separation, bind one issuer per tenant:
export AGENT_BOM_OIDC_TENANT_PROVIDERS_JSON='{
"tenant-alpha": {
"issuer": "https://alpha.okta.example",
"audience": "agent-bom",
"tenant_claim": "tenant_id",
"require_tenant_claim": true
},
"tenant-beta": {
"issuer": "https://beta.okta.example",
"audience": "agent-bom",
"tenant_claim": "tenant_id",
"require_tenant_claim": true
}
}'
This mode gives you two protections:
- a token from the wrong issuer is rejected
- a token whose tenant claim does not match the bound tenant is rejected
SAML mapping¶
SAML configuration is driven by:
AGENT_BOM_SAML_IDP_ENTITY_IDAGENT_BOM_SAML_IDP_SSO_URLAGENT_BOM_SAML_IDP_X509_CERTAGENT_BOM_SAML_SP_ENTITY_IDAGENT_BOM_SAML_SP_ACS_URLAGENT_BOM_SAML_ROLE_ATTRIBUTEAGENT_BOM_SAML_TENANT_ATTRIBUTE
Recommended production posture:
- set
AGENT_BOM_SAML_REQUIRE_ROLE_ATTRIBUTE=1 - keep tenant attributes fail-closed; this is the default
- use
AGENT_BOM_SAML_ALLOW_DEFAULT_TENANT=1only for explicit single-tenant compatibility
SAML and OIDC both fail closed when a verified assertion/token omits tenant
context, unless the operator explicitly opts into the single-tenant default
bucket.
API keys and rotation¶
API keys remain the best fit for:
- automation
- internal services
- gateway/control-plane machine-to-machine traffic
The control plane supports:
- tenant-scoped keys
- role-scoped keys
- enforced TTL policy
- rotation in place
- revoke/delete flows
Key rotation endpoints and policy introspection:
GET /v1/auth/policyGET /v1/auth/keysPOST /v1/auth/keysPOST /v1/auth/keys/{key_id}/rotateDELETE /v1/auth/keys/{key_id}
SCIM lifecycle boundary¶
SCIM user and group provisioning records tenant-bound role membership metadata
for operators and audits. Single-tenant SCIM uses
AGENT_BOM_SCIM_BEARER_TOKEN plus AGENT_BOM_SCIM_TENANT_ID. Multi-tenant
control planes can instead set AGENT_BOM_SCIM_BEARER_TOKENS_JSON, a JSON
object mapping tenant_id to either a token string or an object with token
and optional token_id.
The first command is the IdP SCIM test request to
/scim/v2/ServiceProviderConfig with the bearer token for that tenant. The
artifact is a tenant-bound SCIM user or group under /scim/v2/Users or
/scim/v2/Groups; the next step is to verify /v1/auth/policy reports
payload_tenant_attributes_ignored=true.
The SCIM tenant is resolved only from server configuration, not from the IdP
payload. Mapped tokens are rejected when blank, duplicated, or bound to a
reserved tenant ID. Error and posture surfaces do not include token material.
SCIM deactivation constrains runtime access for matching provisioned identities
and revokes tenant API keys bound by principal ID, SCIM subject, or legacy
owner/name. Repeated deactivation retries credential cleanup after a storage
failure. Send active as a JSON boolean; other types return a SCIM 400 error
without changing lifecycle state. Upstream IdP sessions and unrelated service
credentials still require their own lifecycle controls.
The data-boundary contract exposes this as
payload_tenant_attributes_ignored=true, with tenant source
AGENT_BOM_SCIM_TENANT_ID or AGENT_BOM_SCIM_BEARER_TOKENS_JSON. The
repository also keeps the longer operator note at docs/SCIM_SECURITY_MODEL.md.
Operator debugging¶
Use:
This returns:
auth_methodsubjectroletenant_idoidc_issuer_suffixrequest_idtrace_idspan_id
That endpoint is the fastest way to answer:
- why did this request get
403 - which auth path was used
- which tenant was actually resolved
- whether the wrong issuer or tenant mapping was applied
Recommended deployment posture¶
For enterprise control-plane users:
- prefer OIDC or SAML
- keep MFA at the IdP
- require explicit tenant claims or tenant-bound issuers
For machine-to-machine paths:
- use tenant-scoped API keys
- keep TTLs finite
- rotate under the published policy
For same-origin enterprise ingress:
- use trusted proxy mode only when your reverse proxy is already enforcing user identity and tenant headers
Secret sourcing (compose vs Helm)¶
Compose / hosted POC stacks mount control-plane secrets as Docker secret
files (AGENT_BOM_*_FILE under /run/secrets/...). Do not put API keys,
HMAC material, connection crypto, or proxy attestation secrets in .env.
Helm may still inject the same values via Kubernetes Secret → process env
(secretKeyRef / ExternalSecrets). The API accepts either form (*_FILE
wins when set). Volume-mounted files in pods are a later hardening step; env
injection remains valid for one release on Kubernetes.
Browser UI and API trust split¶
The Node UI should be auth-aware and role-aware, but it should never be the authorization source of truth.
The practical rule is:
- the UI handles experience
- the API handles identity, authorization, tenancy, and audit
So a seamless self-hosted browser experience looks like this:
- the user authenticates through trusted proxy OIDC, first-party dashboard OIDC (auth-code + PKCE), direct OIDC bearer, or a narrower fallback such as a short-lived or session-only API key
- the API resolves subject, role, tenant, and request trace context
- the UI adapts to that state
- the API still enforces every request server-side
That means:
- the UI can hide or disable actions for clarity
- but the API must still return
401or403when the actor is not allowed - tenant scope is always resolved server-side, not accepted from arbitrary UI input
For the full architecture contract, including request integrity, audit, and the recommended hosted/session evolution path, see UI, API, Auth, and Session Model.
Self-hosted teams vs provider-style operators¶
The supported strength today is:
- a company or platform team running one self-hosted control plane for its own organization
- tenant-scoped auth, RBAC, audit, fleet, stores, and shared gateway routing
- customer-owned data, storage, and telemetry choices
That is not the same claim as:
- a provider operating one control plane for many customer organizations
- turnkey tenant onboarding lifecycle APIs
- richer per-tenant delegation templates and quota surfaces
- provider-style admin UX and support workflows
Those provider/MSSP surfaces are a later product track. They should not be inferred from current self-hosted auth and tenancy strength.
Honest security claim¶
For auth, tenancy, and control-plane actions, the most accurate short claim is:
agent-bomalready has strong attribution, tenant-scoped authorization, and tamper-evident audit. Stronger signed approvals, richer browser-session hardening, and turnkey MSSP-style tenancy administration are future hardening, not current overclaims.
Evidence and tests¶
- OIDC implementation:
src/agent_bom/api/oidc.py - SAML implementation:
src/agent_bom/api/saml.py - auth middleware:
src/agent_bom/api/middleware.py - RBAC:
src/agent_bom/rbac.py - auth/tenant tests:
tests/api/test_api_oidc.py,tests/api/test_api_hardening.py,tests/api/test_api_cross_tenant_matrix.py