vitrinka.docs

Single sign-on

OIDC against your own directory, configured in-product rather than in a file, with MFA that vitrinka verifies rather than assumes.

Configure it

Settings → Organization → SSO. Enter your issuer, client id and secret, and map your IdP's groups to roles. Two things help before you commit: Check health probes your issuer, and test sign-in shows exactly which claims arrive.

  • Entra ID, Keycloak, Okta — all three are in use, but no per-provider walkthrough is written yet — ask, and you will get the exact issuer, scopes and claim mapping for yours. A provider on your internal network needs the operator first — below.
  • Group-to-role mapping — reapplied at every login, so revoking a group in the directory revokes access here.
  • Default workspaces — a plain organization member reaches only the workspaces they are explicitly a member of, so pick the shared workspaces every person who signs in through SSO for the first time should join. Each first sign-in seats them as a member there (audited as member.join); a role someone already holds in one of those workspaces is kept, never lowered. Owners and admins reach every workspace already and are not seated by this.

An identity provider on your own network

By default the SSO client reaches identity providers on public addresses only: discovery, the key set, the token exchange and Check health refuse loopback, private and link-local targets, so no organization can probe your network through its connection. An internal Keycloak or ADFS — and every identity provider of an air-gapped install — takes two steps.

  1. The operator lists the provider's address in VITRINKA_SSO_PRIVATE_NETWORKS and restarts: comma-separated CIDRs, where a bare IP counts as its /32 or /128. List the provider's own address rather than a subnet. Loopback is reachable only when listed. Link-local, unspecified, multicast and broadcast addresses never are — an entry overlapping them refuses the boot — and known cloud-metadata addresses stay closed even inside a listed range. AIR_GAP=1 opens nothing by itself and takes public addresses away: the listed internal ranges are then all SSO reaches, and an entry outside loopback, RFC 1918, ULA, CGNAT or class E refuses the boot. A provider whose certificate comes from your own CA also needs VITRINKA_SSO_CA: a PEM file path or the PEM text, parsed at boot and trusted beside the system roots by the SSO client alone.
  2. Deployment authority saves the connection. Only a deployment-managed connection reaches a listed range: one saved in Settings → Organization → SSO by a platform admin, or an admin of the VITRINKA_OPS_ORG organization, who is also an admin of that connection's organization (the first admin). Any save by such an admin marks the connection, even one that only changes groups or roles, and the issuer field's hint then says deployment-managed. An ordinary organization admin who saves the issuer, client ID or client secret makes it public-only again, so rotating the secret of an internal provider takes deployment authority too; deleting the connection clears the mark.
VITRINKA_SSO_PRIVATE_NETWORKS=10.20.30.40/32
VITRINKA_SSO_CA=/etc/vitrinka/sso-ca.pem

Until both are done, Check health names what is missing and who has to act. On an air-gapped install a connection nobody with deployment authority saved reaches nothing, and a public issuer is refused — Check health names AIR_GAP and the fix. After an upgrade, a connection that existed before stays public-only until deployment authority saves it once.

There is no cross-organization door: a platform admin marks connections only in organizations they administer, and /admin lists no SSO connections. The usual shape is one organization whose admins are the operators — name it in VITRINKA_OPS_ORG, or make the operator accounts platform admins.

The mark vouches for the issuer that was saved, and trust then goes to whoever runs that issuer: its DNS picks the address, its discovery document picks the key-set and token endpoints, and its redirects are followed — anywhere inside the listed ranges. Save only connections to an identity provider you run or trust, and keep the list to that provider's own address. Revoking someone's platform admin, or their seat in the VITRINKA_OPS_ORG organization, does not clear marks they already set.

Forced SSO

Turn it on to require SSO for your organization's people. Who exactly that binds depends on the deployment:

  • Members of your organization (the default, VITRINKA_SSO_DOMAINS off — the hosted service and any self-host without domain claims). The sign-in page keeps its password form for everyone; the check runs after a credential is proven, at the completion step every local path funnels through (password, email code, reset link, passkey, GitHub). A member of the forcing organization is refused there with a page that routes to your SSO, and the refusal is audit-logged. Anyone who is not a member — a user of another organization, a brand-new sign-up — signs in normally. Deciding post-credential is deliberate: the shared page never reveals who belongs to your organization.
  • Your claimed email domains when VITRINKA_SSO_DOMAINS=on (below): those addresses are refused up front, on the page and at every local POST.
  • The whole deployment when VITRINKA_SSO_DOMAINS=on and yours is the only enabled connection — the enterprise self-host shape, where you own the deployment. The lock lasts only while it is the only one: once any organization enables a second connection, it narrows to your claimed domains, so claim yours too.

With VITRINKA_SSO_DOMAINS=on, turning forced SSO on binds addresses on the sign-in page every organization shares, so it is a deployment-wide setting: it takes a platform admin, or an admin of the VITRINKA_OPS_ORG organization, who is also an admin of this one. An organization admin without that authority keeps a forced setting that is already on and can turn it off; while it is off, their switch reads unavailable with the reason.

Without domain claims the server refuses to save forced SSO while another organization's connection is enabled, and refuses to enable a second connection while one organization forces SSO — the setting is never accepted and then silently ignored.

There is a documented break-glass escape hatch at /auth/login?local=1 for when your IdP is down; every use is audit-logged, and VITRINKA_BREAK_GLASS decides who may take it (below).

Email domains

Self-hosted deployments can set VITRINKA_SSO_DOMAINS=on to let each organization claim the email domains its identity provider owns. A claim is exact-match and exclusive across the deployment, shared mail providers cannot be claimed, and sign-ins for a claimed address route straight to that organization's connection. The hosted service keeps this off.

Because a claim routes addresses on the sign-in page every organization shares, adding one is a deployment-wide setting: it takes a platform admin, or an admin of the VITRINKA_OPS_ORG organization, who is also an admin of the claiming organization. An organization admin without that authority sees the reason in place of the field's hint and can still re-save or release the claims the organization holds. The refusal comes before any check against other organizations' claims, so it never reveals them. Claims saved before an upgrade to this rule stay as they were.

MFA is verified, not assumed

MFA is enforced at your IdP — Entra conditional access, Keycloak required actions — and checked here. Turn on Require MFA and the server refuses any sign-in whose id_token does not assert a multi-factor amr claim.

The point is that a silently relaxed IdP policy fails loudly instead of quietly dropping the second factor. Entra v2.0 tokens need the amr optional claim configured on the app registration; v1.0 emits it by default.

Break-glass is owner-only and passkey-first

/auth/login?local=1 arms a five-minute emergency grant for local sign-in. Two rules apply, in this order:

  1. Who may use it — VITRINKA_BREAK_GLASS (owners by default): only an owner of the forcing organization may complete a break-glass sign-in; a member's attempt is refused as forced SSO and leaves an auth.break_glass audit row marked denied (reason: not an owner). off disables the door entirely — ?local=1 sets no grant and a forced refusal has no bypass. all lets anyone the posture binds use it (the pre-2026-09 behaviour). Like forced SSO itself, the rule is decided after the credential is proven.
  2. Passkey-first: an account with a registered passkey must sign in with it. The password form only works for accounts that never enrolled one, and that weaker fallback writes its own audit row.

Enrol a passkey for at least one admin before you need the emergency path.

Session and header posture

Every response carries a baseline header set — nosniff, frame-ancestors 'self', Referrer-Policy, Permissions-Policy — and HSTS is added automatically when BASE_URL is https.

SettingDefaultOverride
Absolute session lifetime720 hours; the enterprise bundle sets 12 hoursVITRINKA_SESSION_TTL
Idle timeoutoff; the enterprise bundle sets 30 minutesVITRINKA_SESSION_IDLE

Both are enforced server-side. Combine them with forced SSO so your IdP's conditional-access policy gates every sign-in.

On this page