# Identity & Access

IdP integration, SSO, SCIM provisioning, admin access paths, least-privilege model

# Chapter 9 — Identity & Access

##### **9.1 Current State and Scope**

As of 2026-07-18 **no IdP integration exists**: Grafana and BookStack run on local admin accounts (both credentials exposed in the changelog — item 15 applies), the platform application is pre-deployment, and host access is the shared-key nonconformance of §2.5. This chapter defines the target identity architecture covering the IAM control family: IAM-01 (identity-aware proxy), IAM-02 (SSO federation), IAM-03 (phishing-resistant MFA), IAM-04 (tenant separation), IAM-05 (SCIM lifecycle), IAM-06 (access reviews). Human identity only — machine/agent credentials are Chapter 8; per-Compartment application RBAC internals (APP-05) are application-layer and specified in the platform application documentation, but their identity *source* is defined here.

##### **9.2 Identity Provider**

**Single source of truth: SBS's corporate IdP.** The Platform maintains **no local user store** — no local passwords on any surface (the two as-built local admins are migrated then reduced to break-glass, §9.6). Users exist on the Platform only as federated identities projected from the SBS directory.

**Discovery dependency (blocking):** SBS's incumbent IdP product (Entra ID, Okta, Google Workspace, or other) is unconfirmed — it is a named LH-SBS-DISC-002 IT-function question and a gate for this chapter's build items. The architecture below is written IdP-agnostic against capabilities every mainstream enterprise IdP provides: OIDC, SAML 2.0, SCIM 2.0, group claims, WebAuthn/FIDO2 MFA enforcement. Nelson Santos is execution owner for the IdP-side configuration (app registrations, group creation, SCIM token issuance).

**Federation protocol:** OIDC (Authorization Code + PKCE) everywhere it is supported — Grafana (native OIDC), the platform application (OIDC middleware), the identity-aware proxy (§9.3). SAML only where a component supports nothing better. Token hygiene: short-lived access tokens (≤1 h), refresh handled server-side, `email_verified` + group claims required in the ID token.

**MFA (IAM-03):** enforced **at the IdP**, not per-application — phishing-resistant methods only (WebAuthn/FIDO2 hardware or platform authenticators) for all Platform-mapped groups; TOTP acceptable *only* as a documented exception with expiry; SMS/voice prohibited. Conditional-access (where the IdP supports it): Platform app registrations require MFA on every authentication, no "remembered device" exceeding 12 h for admin roles.

##### **9.3 Identity-Aware Proxy (IAM-01)**

Enforcement point for surfaces that lack robust native OIDC or need pre-application gating. Implementation: **oauth2-proxy** deployed alongside nginx on the serving droplet, wired via `auth_request`:

<div class="overflow-x-auto w-full px-2 mb-6" id="bkmrk-surface-enforcement-"><table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">**Surface**</th><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">**Enforcement**</th></tr></thead><tbody><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`monitor.sbsdash.com`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Grafana native OIDC (preferred — it maps groups→roles internally, §9.5). oauth2-proxy not required</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`wiki.sbsdash.com`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">BookStack SAML/OIDC if licensed features suffice; else oauth2-proxy in front — **no unauthenticated byte** beyond the IdP redirect is served</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`admin.sbsdash.com`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">oauth2-proxy **in addition to** application auth — admin surface requires valid IdP session *before* the application sees the request, layered on the §5.7 edge IP allowlist</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`sbsdash.com`, `clients.`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Application-native OIDC; anonymous access limited to the sign-in redirect itself (FE posture: the public fingerprint is a sign-in page, §5.5)</td></tr></tbody></table>

</div>oauth2-proxy session cookies: `__Host-` prefix, `Secure`, `HttpOnly`, `SameSite=Lax`, secret from Vault (`secret/platform/`), session lifetime ≤ 8 h with IdP re-auth. The proxy passes identity to upstreams via signed headers; upstreams **reject unsigned/absent identity headers** so the proxy cannot be bypassed by direct container access (pairs with the `net-edge` design, §6.4).

##### **9.4 SCIM Provisioning (IAM-05)**

Lifecycle automation — the control that makes offboarding real:

- **Direction:** IdP → Platform. SBS directory group membership drives Platform access; the Platform never invites users directly.
- **Targets:** the platform application (SCIM 2.0 endpoint, Compartment-role mapping per §9.5) and Grafana (SCIM or team-sync per licensing; fallback is OIDC group-claim mapping at login — acceptable because login-time mapping plus short sessions bounds staleness to the session length).
- **Deprovisioning SLA:** IdP deactivation propagates to Platform deactivation **within 15 minutes** (SCIM push) and in the worst case at next token expiry (≤ 1 h). A user disabled in the SBS directory holds no live Platform session beyond that hour — this is the tested metric, not the config's existence.
- **SCIM tokens** live in Vault (`secret/platform/scim/`), rotate on the 90-day static schedule (§8.4), and their use is logged (MON-01).
- Joiner/mover/leaver flows are exercised end-to-end as a named gate: create test user → group add → verify access; group move → verify role change; deactivate → verify lockout inside SLA.

##### **9.5 Least-Privilege Model and Group Mapping (IAM-04 / APP-05 boundary)**

Access is **group-derived, never user-granted**. SBS directory groups map to Platform roles; the group list is the single audit surface for IAM-06:

<div class="overflow-x-auto w-full px-2 mb-6" id="bkmrk-directory-group-%28nam"><table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">**Directory group (naming)**</th><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">**Grants**</th></tr></thead><tbody><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`dash-platform-admin`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Master Admin console; oauth2-proxy admission to `admin.sbsdash.com`. Named individuals only, target ≤ 4</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`dash-c01-user` … `dash-c10-user`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">That Compartment's surfaces and workflows — the human half of IAM-04 tenant separation, mirroring the network/data/secrets walls (§6.4, §7.4, §8.3)</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`dash-c01-supervisor` …</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Receives that Compartment's Medium/High/Critical routings from Supervisor Agents; resolution authority</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`dash-monitor-view` / `dash-monitor-admin`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Grafana Viewer / Admin via group-claim mapping</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`dash-wiki-edit` / `dash-wiki-view`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">BookStack roles</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`dash-exec-dashboard`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">COO/CFO read-only reporting views</td></tr></tbody></table>

</div>Rules: no wildcard "all-Compartments" user group exists — cross-Compartment humans (e.g., the COO) hold the exec-dashboard role, which reads *reporting outputs*, not Compartment interiors; platform-admin grants administration, and its members' actions are fully logged (MON-01) — admin ≠ unobserved. Group membership changes are IdP-audited events; the quarterly IAM-06 access review walks every `dash-*` group against current staffing (Alexandra Del Rey coordinates sign-off; Nelson Santos executes directory changes), and the review artifact is filed in the wiki with date and reviewer — access certification (MON-04) consumes the same artifact.

##### **9.6 Admin Access Paths**

Consolidated statement of every privileged path and its chain, replacing ad-hoc practice:

<div class="overflow-x-auto w-full px-2 mb-6" id="bkmrk-path-chain-controls-"><table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">**Path**</th><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">**Chain**</th><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">**Controls**</th></tr></thead><tbody><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Web admin</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">IdP (MFA) → oauth2-proxy → edge IP allowlist (§5.7) → `admin.sbsdash.com`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">IAM-01/02/03; all actions app-logged</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Host SSH</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Operator (personal key) → bastion `monitor-servers` → target droplet</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Per-person users post item 17; bastion sessions logged; admin allowlist IaC-managed (§3.5)</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Vault admin</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Bastion → named admin token under `policy-admin`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">§8.2; every operation in Vault audit device</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">DO console / registrar / DNS</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Provider MFA (phishing-resistant), SBS-owned accounts</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">§2.1, §4.1; console use in Prod is break-glass + logged (§2.2.3)</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">**Break-glass**</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Sealed local credentials for Grafana/BookStack/app, stored in Vault + one printed copy in SBS-controlled physical custody</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Used only on IdP outage; use triggers immediate rotation + incident note (MON-05); tested annually</td></tr></tbody></table>

</div>The break-glass row is the honest residual of the "no local accounts" rule: total IdP outage must not equal total platform lockout, so exactly one dormant local admin per critical surface survives — vaulted, alarmed on use, rotated after.

##### **9.7 Verification**

Gates (LH-SBS-INST-001): unauthenticated request to every gated surface yields IdP redirect, zero application bytes (curl sweep); direct-to-container bypass attempt rejected (unsigned identity header); MFA challenge presented on fresh session for admin group (manual gate); SCIM lifecycle test — provision/move/deactivate with deactivation lockout ≤ 15 min (timed); group-mapping probe — `dash-c03-user` reaches c03 surfaces, denied on c08 (IAM-04 human-layer test mirroring §7.7); local password login disabled on Grafana/BookStack (negative login test); break-glass procedure executed in drill, rotation confirmed; IAM-06 review artifact exists with current date.