Identity#

Who can sign in, and what each person may do (plan2 D21): the two ways an install signs people in, the roles and their permissions, the bundled identity provider (Keycloak), and the break-glass login for when the provider is down.

How people sign in#

One setting, ODS_AUTH in the core stack’s .env (the apps stack reads the same file on a release deploy), chooses between two modes:

Mode

Who signs in

Roles

basic (the default)

The install’s shared login (ODS_ADMIN_USER, ODS_ADMIN_PASSWORD), asked by the proxy. Fine for one person, or for trying the platform out

Everyone behind the shared login is an operator: nothing can tell people apart

oidc

Each person, with an OpenID Connect provider: the bundled Keycloak, or one you already run (Entra ID, Okta, Google Workspace, …). oauth2-proxy sits in front of the portal and Dagster and sends anyone not signed in to the provider

From the person’s groups at the provider

Either way, the platform trusts only the proxy about who someone is. In oidc mode oauth2-proxy passes the signed-in person to the portal in X-Auth-Request-* headers, and Traefik removes those headers from every request that arrives, before anything else, so nobody can claim to be someone by sending them.

Roles and permissions#

The platform checks permissions, never roles. Five roles come with it, each granted by a group:

Role

Group

Permissions

So they can

viewer

ods-viewer

catalog:read

browse the portal and catalog, read settings

analyst

ods-analyst

+ proof:run

also launch a round-trip proof

engineer

ods-engineer

+ dagster:use

also use Dagster’s own UI

operator

ods-operator

+ settings:write, audit:read

everything, including rebuild paces

auditor

ods-auditor

catalog:read, audit:read

read the catalog and, as they’re built, the records for audit

Someone in several groups has every permission of each. Someone in none can sign in and is shown what they’re missing. Controls a person may not use are hidden, and a page that needs a permission they lack says which one.

Dagster has no permissions of its own: whoever can open its UI can launch and stop anything. So only dagster:use reaches it, and the auditor doesn’t get it (Dagster can’t be made read-only).

Using your own groups#

The roles and groups are in apps/portal/roles.yml. To change them for an install, copy that file to the portal’s config directory (shared/portal/roles.yml on a release deploy; onprem/apps/portal/roles.yml beside a checkout) and edit the copy. It replaces the platform’s file whole. The usual change is to point a role at a group your directory already has:

roles:
  engineer:
    groups: [ods-engineer, data-engineering]   # any of these grants the role
    permissions: [catalog:read, proof:run, dagster:use]

An unknown role or permission stops the portal at start, with the file and the name, rather than quietly granting or refusing.

Signing in with Keycloak#

The install signs people in with oidc mode: oauth2-proxy sits in front of the portal and Dagster and sends anyone not signed in to an OpenID Connect provider. The provider can be one you already run (see another provider), or the Keycloak that comes with the platform as the optional identity profile.

Keycloak runs in its own keycloak database on the stack’s Postgres, is capped at 1 GB of memory (ODS_KEYCLOAK_MEMORY; it idles around 600 MB), and is served at /auth on the install’s own address, so a LAN install needs no domain for it. On its first start it imports the ods realm (onprem/core/keycloak/ods-realm.json):

  • the five role groups, ods-viewer, ods-analyst, ods-engineer, ods-operator and ods-auditor (what each role may do is in apps/portal/roles.yml);

  • the client oauth2-proxy signs in with, which only sends people back to the install’s own address and puts a person’s groups in a groups claim;

  • a password policy (at least 12 characters, not the username or email), and lockout after repeated failures;

  • no self-registration: people are added by an operator, or come from a directory.

To turn it on, in the core stack’s .env:

  1. Run onprem/core/scripts/init_traefik.sh. It generates the secrets the mode needs when they’re missing, without printing them: ODS_OIDC_CLIENT_SECRET (shared by oauth2-proxy and the realm), ODS_OIDC_COOKIE_SECRET and ODS_KEYCLOAK_ADMIN_PASSWORD.

  2. Set ODS_PUBLIC_URL to the address people type, as their browser sees it, with scheme and port, for example https://192.0.2.10:8443. The host must be one the install’s certificate names (init_traefik.sh makes it for the host’s names and addresses).

  3. Set ODS_AUTH=oidc, add oidc,identity to COMPOSE_PROFILES, and start the stack. The apps stack’s portal needs ODS_AUTH=oidc too.

Leave ODS_OIDC_ISSUER and ODS_OIDC_CLIENT_ID unset: they default to the bundled realm.

Sessions: a person stays signed in while they use the platform. Every ODS_OIDC_COOKIE_REFRESH (5 minutes unless set), oauth2-proxy renews the session with Keycloak, so a change of groups applies within that time, and a disabled account, or a Keycloak session ended by an administrator or by 30 minutes without use, is sent back to sign in. Sign out (in the portal’s top bar) ends both the platform’s session and Keycloak’s.

Note

The realm is imported once, on the first start, and an existing realm is never overwritten. Changing ODS_PUBLIC_URL or ODS_OIDC_CLIENT_SECRET later means changing them in Keycloak’s admin console as well: Clients → ods → Valid redirect URIs and Web origins, or Credentials.

The admin console#

Keycloak’s admin console is at <ODS_PUBLIC_URL>/auth/admin/. The first sign-in uses admin (or ODS_KEYCLOAK_ADMIN_USER) and ODS_KEYCLOAK_ADMIN_PASSWORD from .env. That account is temporary, and Keycloak says so: in the master realm, create a permanent administrator for yourself, add it to the admin role, sign in as it, and delete the temporary one. Then remove ODS_KEYCLOAK_ADMIN_PASSWORD from .env: it only ever creates that first account, and an unused secret is better gone (init_traefik.sh generates it again if it’s run once more). People who use the platform belong in the ods realm, never in master.

Administrators and the people who use the platform sign in on different pages: the console’s is the master realm’s (its address has /auth/realms/master/), the platform’s is the ods realm’s (/auth/realms/ods/). They look alike. A master account typed into the platform’s page is refused as an unknown user, and the other way round.

The sign-in page#

The ods realm’s sign-in pages carry the platform’s brand: the mark, the wordmark and the palette, on Keycloak’s own theme (onprem/core/keycloak/themes/ods/, which extends keycloak.v2). The mark is the docs’ own copy (docs/_static), mounted into the theme. Keycloak’s admin console keeps its stock look.

A realm imported before the theme existed keeps Keycloak’s default: in the admin console, in the ods realm, Realm settings → Themes → Login theme → ods, then Save.

Adding people#

In the admin console, switch to the ods realm, then:

  1. Users → Add user: a username, their email (tick Email verified if you know it’s theirs), and their name.

  2. Join groups: the group of their role, for example ods-analyst. Someone in no role group can sign in but sees a refusal page.

  3. Credentials → Set password: a first password, with Temporary on, so they choose their own when they first sign in.

A person’s role changes when their groups do, from their next sign-in.

Two-factor sign-in#

Two-factor sign-in with an authenticator app is available and off by default. Anyone who sets it up (from their account page, <ODS_PUBLIC_URL>/auth/realms/ods/account/) is asked for a code at every sign-in. To require it:

  • for one person: their user page → Required user actions → Configure OTP;

  • for everyone added from now on: Authentication → Required actions → Configure OTP → Set as default action.

Connecting a directory you already have#

People can come from an existing directory instead of being added by hand. Either way, their roles come from groups: either put them in the ods-<role> groups, or point roles.yml at groups your directory already has (copy it to the portal’s config directory and change groups:).

LDAP or Active Directory#

User federation → Add LDAP providers:

  • Vendor: Active Directory (or your server’s), and its Connection URL (ldaps:// where you can);

  • Bind DN and Bind credentials: a service account that can read users;

  • Users DN: where the people are;

  • Edit mode: READ_ONLY, so the directory stays the authority;

  • then, under Mappers, Add mapper of type group-ldap-mapper with the LDAP Groups DN of the groups to bring in. The groups appear in the realm, and a person’s directory groups go into their groups claim.

Passwords stay in the directory: Keycloak checks them there at each sign-in.

Entra ID, Okta or Google#

Keycloak can send people on to another provider to sign in (“brokering”), keeping the platform’s roles and groups in one place. Identity providers:

  • Entra ID or Okta: OpenID Connect v1.0, with the provider’s discovery address (Entra: https://login.microsoftonline.com/<tenant id>/v2.0/.well-known/openid-configuration; Okta: https://<your org>.okta.com/.well-known/openid-configuration), and the client id and secret of an app registration made there.

  • Google: the built-in Google provider, with an OAuth client id and secret from Google Cloud.

Register <ODS_PUBLIC_URL>/auth/realms/ods/broker/<alias>/endpoint as the redirect address at the provider (<alias> is the name the provider has in Keycloak). Then give brokered people their role, under the provider’s Mappers: Hardcoded group (everyone from this provider gets one role), or Advanced claim to group (a claim the provider sends, for example Entra’s groups, mapped to an ods-<role> group). Entra sends group ids, not names, unless the app registration is set to send names.

Another provider#

Keycloak is optional. oauth2-proxy can sign people in with any OpenID Connect provider directly: Entra ID, Okta, Google Workspace, Authentik, or a Keycloak you already run. Leave out the identity profile and set:

  • ODS_OIDC_ISSUER: the provider’s issuer address;

  • ODS_OIDC_CLIENT_ID and ODS_OIDC_CLIENT_SECRET: the client registered there, with <ODS_PUBLIC_URL>/oauth2/callback as its redirect address;

  • ODS_OIDC_GROUPS_CLAIM, if the provider puts groups in a claim other than groups;

  • ODS_OIDC_LOGOUT_URL: the provider’s logout address, with {id_token} where the session’s ID token goes, so signing out ends the provider’s session too (for example https://idp.example.com/logout?id_token_hint={id_token}); set it empty if the provider has none;

  • and in roles.yml, the provider’s group names (or ids) for each role.

The provider must send the person’s groups: without them, everyone signs in with no role. It should also issue refresh tokens (often the offline_access scope, or a setting on the client; add a scope with ODS_OIDC_SCOPE): without them, sessions aren’t renewed, so a change of groups or a disabled account applies only when the person next signs in.

Break-glass#

When the identity provider is down, or misconfigured, nobody can sign in through it. The break-glass login is the operator’s way in regardless: the install’s shared login (ODS_ADMIN_USER and ODS_ADMIN_PASSWORD, from init_traefik.sh) on a port of its own, ODS_BREAKGLASS_PORT (8444 unless set), at the same address: https://<host>:8444/. It doesn’t touch oauth2-proxy or the provider, and it works in either sign-in mode.

  • Who gets in: whoever has the shared login, as an operator (shown as admin (break-glass)). Keep the password where operators can find it in an emergency, and no one else.

  • From where: only the networks in ODS_BREAKGLASS_ALLOW (comma-separated, e.g. 127.0.0.0/8,192.168.1.0/24). Unset, that’s the host itself and private networks: loopback, 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. The host’s own requests arrive through Docker’s gateway, which is in 172.16.0.0/12, so keep that range (or Docker’s network) if you narrow the list and still want to use it from the host. A request from anywhere else is refused before the login is asked for.

  • What it reaches: the portal and Dagster. Keycloak’s admin console has its own login at /auth/admin/ on the usual port.

  • The record: every request on this port, refused or not, is in Traefik’s access log, as JSON, with the login’s name (ClientUsername), the address it came from, the path and the answer: /var/log/traefik/access.log on the traefik-logs volume. To read it: docker compose exec traefik tail /var/log/traefik/access.log. The usual HTTPS port is not logged there. Settings changed through the portal also record who changed them on the run (ods/requested_by): the person’s email when they signed in with the provider, admin (break-glass) through this login.

Don’t publish the break-glass port to the internet. If the install sits behind a router that forwards ports, forward only ODS_HTTPS_PORT.

When the provider is down#

  • Signed-in people keep working until their next session renewal (ODS_OIDC_COOKIE_REFRESH), then are sent to sign in.

  • Signing in shows Sign-in is unavailable (for the bundled Keycloak, which is served by the install itself) instead of the provider’s page.

  • Operators use the break-glass login, and look at the provider: docker compose ps keycloak and docker compose logs keycloak.

Turning on oidc for an install#

A checklist, for an install in basic mode today:

  1. Run init_traefik.sh (on a release deploy with ODS_ENV_FILE and ODS_TRAEFIK_SECRETS pointing at shared/) for the new secrets.

  2. In .env: ODS_PUBLIC_URL, ODS_AUTH=oidc, and oidc,identity added to COMPOSE_PROFILES (oidc only, with another provider and its settings).

  3. Deploy. Keycloak takes about a minute on its first start; until oauth2-proxy has found it, pages answer with an error (Traefik can’t reach the sign-in yet). Use break-glass meanwhile if needed.

  4. In Keycloak’s admin console: your permanent administrator (see the admin console), then the people and their groups, or a directory.

  5. Sign in as each kind of person once. Check break-glass from the host.

To go back, set ODS_AUTH=basic and deploy: the shared login applies again, and Keycloak’s data stays in its database for next time.

Known limits#

  • Domain mode (auth.<ODS_DOMAIN>) comes with the platform’s domain mode; for now Keycloak is served at /auth on the install’s address.

  • A deploy restarts Keycloak. Each release runs from its own directory, so Compose recreates Keycloak on every deploy, even one that doesn’t change it. People signed in to the platform stay signed in (Keycloak keeps sessions in its database), but the admin console signs you out: avoid deploying while someone is in the middle of administering Keycloak.