Skip to main content

Identity and Sessions

MOD supports identity and session controls in Keycloak: bounded session lifetimes (NIST AC-11/AC-12), TOTP enrollment enforcement for platform admins (NIST IA-2), an AC-8 system-use notice on the login page, and a dormant x509 smart-card seam. MOD enables operators to configure these controls; it does not make a customer compliant with any framework, and enabling a control here does not by itself satisfy the corresponding control — enrollment coverage, key custody, and the operator's own policy remain the operator's responsibility.

Session lifetimes (AC-11 / AC-12)​

CORE/startup/keycloak-init.sh bounds the realm's online session lifetimes in both the create and the idempotent-update blocks, and converges them on every run:

SettingValueControl
accessTokenLifespan900 s (15 min)AC-12 access-token ceiling
ssoSessionIdleTimeout1800 s (30 min)AC-11 inactivity termination (admin-safe realm baseline, owner Q49=A; per-role/per-profile split — users 60m/12h, admins 30m/8h, gov/DoD 15m/8h — comes with FX.54)
ssoSessionMaxLifespan28800 s (8 h)AC-12 hard session ceiling

These mirror the platform settings auth.access_token_lifespan_seconds, auth.sso_session_idle_seconds and auth.sso_session_max_lifetime_seconds (registered in CORE/API/services/platform_settings.py). The init script pins the values because it runs inside the Keycloak container and cannot read the API DB at startup — so the registry and the script must be kept in sync.

Former 30-day client-level session overrides on the dashboard and WorkflowBuilder clients are converged to the same values, so a client can no longer extend a session past the realm ceiling. Offline (refresh-token) lifetimes deliberately stay unbounded — bounding them requires the exchange-token persistence fix first (token rotation would otherwise invalidate every stored token on first use and break long-running workflows).

Admin MFA enforcement (IA-2)​

The init script queues the CONFIGURE_TOTP required action on members of the platform_admin Keycloak group who do not yet carry a TOTP device. On their next browser login, Keycloak's stock Conditional-OTP step presents the one-time-password enrollment screen (QR/code) before the dashboard loads, and every subsequent login asks for the 6-digit code — enrollment is self-enforcing and no browser-flow surgery is needed.

The queue is idempotent: a user who already completed enrollment (a TOTP device present, GET /users/{id}/totp returns 200) or already has CONFIGURE_TOTP queued is skipped, so re-running the init does not force re-enrollment.

Exemptions​

Two classes of users are always skipped, because a pending required action breaks non-interactive authentication:

  • Members of the e2e_service_principals group — the E2E harness principals created by CORE/e2e/seed_principals.sh. They authenticate non-interactively (direct grant) and can never complete an OTP enrollment.
  • Machine users — usernames matching service-account-*, *@service.local (the live seeded service user is api@service.local), api_service, and platform-service. These are client_credentials / direct-grant only and never pass the browser flow; a queued CONFIGURE_TOTP would lock them out for no gain. After an apply, api@service.local must show requiredActions: [].

MFA for all users​

The opt-in platform setting auth.mfa_required_for_all (Admin → Platform settings, group auth, default false) extends TOTP enforcement to every user in the realm, machine users and the e2e_service_principals exemption group still skipped. This is the flip for the gov/DoD buyer tier. Flipping the setting alone is not enough: set MFA_REQUIRED_FOR_ALL="true" in keycloak-init.sh to match the registry (the script cannot read the API DB), then re-run the init.

The companion setting auth.mfa_required_for_admins (default ON) covers the admin-group-only behavior described above.

AC-8 login banner​

The platform setting auth.login_banner_text (default empty = no banner) is the AC-8 system-use notice. keycloak-init.sh converges the realm on every run by pushing the value into the login theme property themes.login.headerHtml (HTML-escaped, newlines become <br>), which the modtech login theme renders as a boxed .modtech-use-notice block at the top of the login card (login and register pages share the template macro). The fetch-merge-PUT touches only headerHtml, preserving any other login-theme property an operator set (e.g. a logo).

Failure mode: if the setting contains raw HTML it is escaped (&lt; renders literally), never executed as markup.

x509 / smart-card seam​

CORE/API/keycloak_auth.py carries an x509_piv_login_enabled seam that is a no-op while the platform setting auth.x509_enabled is false (the default). Beyond that seam, no x509/PIV smart-card login path exists in the sources this page covers — flipping the setting is a future enablement, not a working feature today.

Session + re-authentication controls (GY.C4b)​

Beyond session lifetimes, MOD Core supports three per-account controls, all tunable as platform settings (Admin → Platform settings, auth group) and all off by default so an existing install behaves exactly as before:

Concurrent-session cap (auth.session_max_concurrent)​

Integer; 0 = unlimited (the default). When set > 0, MOD installs and enables Keycloak's user-session-limits authenticator in the realm browser flow (via CORE/API/services/session_controls.py, applied the moment the setting changes — no Keycloak restart). auth.session_limit_behaviour chooses what happens when a user exceeds the cap: deny_new (refuse the new login, the default) or terminate_oldest (end the oldest session to make room).

  • No home-grown auth: the limit itself lives in Keycloak; MOD only configures the authenticator through the admin REST API.
  • Exempt by construction: service accounts, client-credentials clients and e2e principals never traverse the browser flow, so they are not capped.
  • Boot-time install is disabled-by-default: CORE/startup/keycloak-init.sh idempotently installs the execution (requirement DISABLED) so a restart re-converges the shape; the API then flips it on/off to match the live settings. (Keycloak 26.3.x ships the authenticator; the install logs and moves on only if a build/custom realm does not have it — a defensive path.)

Inactive-account auto-disable (auth.inactive_disable_days)​

Integer; 0 = off (the default). A daily scheduled sweep (services/inactive_accounts.py, job inactive_account_disable) soft- disables human accounts whose last recorded activity is older than N days — Keycloak enabled=false + MOD status DEACTIVATED, never a delete (ISO 27001 no-hard-delete). Platform owner / break-glass / service accounts are never touched; each disable is individually audited. auth.inactive_disable_exempt_emails carries the break-glass / monitored-service carve-out. A dry-run preview — who the sweep would disable, changing nothing — is at GET /v1/platform/compliance/inactive-accounts.

Re-authentication for privileged actions (auth.privileged_reauth_max_age_s)​

Integer (seconds); 0 = off (the default). Bounds how long a login remains acceptable for privileged actions — break-glass, compliance-profile apply, platform-settings writes, user role changes, and API-key creation. The check reads the Keycloak token's auth_time claim (when the user actually logged in), so a fresh token on a stale login is still rejected. A stale token returns 401 {"error": "reauth_required", "reauth_max_age_s": N}; the Dashboard intercepts that error (in CORE/Dashboard/js/api.js) and redirects the browser through Keycloak with prompt=login&max_age=N to force a fresh credential check, after which the operator retries the action. Service / api-key channels carry no human auth_time and are unaffected.

Wording note: MOD supports these controls; it does not claim or certify compliance with any specific framework.

How to re-apply​

The init script is idempotent and designed to converge an existing realm; it is the my_keycloak_init container's command, bind-mounted from CORE/startup/keycloak-init.sh. Re-run it against the live realm:

cd CORE/startup
docker run --rm --name my_keycloak_init_rerun \
--network mod_modtex-network \
-v "$PWD/keycloak-init.sh:/opt/keycloak/init/keycloak-init.sh:ro" \
-v "$PWD/.env.public:/env/public:ro" -v "$PWD/.env.secrets:/env/secrets:ro" \
--entrypoint bash core-keycloak-init \
-c 'set -a; . /env/public; . /env/secrets; set +a; exec bash /opt/keycloak/init/keycloak-init.sh'

Do not use --env-file: the env files quote their values and docker run --env-file keeps the quotes literally (compose strips them), so KEYCLOAK_INTERNAL_URL becomes "http://keycloak:8080" and the run dies at ERROR: Failed to obtain access token. Sourcing the files in the shell (set -a; . /env/public; . /env/secrets; set +a) strips the quotes the way compose does. The name my_keycloak_init is held by the original exited init container, hence the different --name.

Expected log lines: GSEC R-043 — converging login-theme banner, GSEC R-010 — CONFIGURE_TOTP queued on N user(s) (N = platform_admin members without a TOTP device), and the summary GSEC R-009 session policy: access 900s / SSO idle 900s / SSO max 28800s.

After the apply, re-seed the e2e principals (CORE/e2e/seed_principals.sh) so they land in the exemption group and their passwords converge to the file/env value.

How to verify each item​

  1. Session bounds — realm API:
    curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
    http://keycloak:8080/admin/realms/modtex \
    | jq '{accessTokenLifespan, ssoSessionIdleTimeout, ssoSessionMaxLifespan, themes}'
    → 900 / 1800 / 28800 and the login theme property present. Browser: sign in, leave the tab idle >30 min, then click an API action → bounced to the login page (idle expiry); stay active >8 h → hard-expiry forces re-login. Access tokens in devtools expire at 15 min.
  2. MFA — an admin without a TOTP device signs in → the Conditional-OTP step presents the enrollment screen before the dashboard loads; every later login asks for the code. A member of e2e_service_principals signs in with no OTP prompt. API check: GET /admin/realms/modtex/users/<admin-id> → requiredActions contains CONFIGURE_TOTP before enrollment and is empty after; machine users such as api@service.local must show requiredActions: [].
  3. Banner — with a non-empty auth.login_banner_text pushed to the login theme, the login page renders the notice in the boxed .modtech-use-notice block at the top of the card (login and register pages). With the setting empty, the block is absent entirely.
  4. x509 seam — Admin → Platform settings shows auth.x509_enabled = false and logins are unchanged.
  5. E2E principals — CORE/e2e/.auth/e2e_pass.txt exists (24 chars, gitignored) after re-seeding; CORE/e2e/run.sh mints all principals non-interactively with no hardcoded fallback password.