Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Authentication

The dashboard supports four authentication providers, selected via auth.provider.

auth:
  provider: local # local | ldap | ad | oidc (default: local)

  # Maximum idle time before a session expires and the user must re-authenticate.
  # Default: 86400 (24 hours). Tighten to 28800 (8 hours) or less for compliance environments.
  session-timeout-seconds: 86400

  # Git credentials fogwall issues for server-mode pushes on providers with oauth.brokered-push. How long a credential
  # works after it is issued or rotated, as an ISO-8601 duration such as P90D; unset (default) for no limit. See SCM
  # OAuth → Brokered pushes.
  git-credentials:
    max-lifetime: P90D

  # Shared key for REST automation, sent in the X-Api-Key header. It carries no user identity. Takes api-key or
  # api-key-path; see Secrets. The FOGWALL_API_KEY environment variable is a deprecated alias for api-key.
  api-key-path: /run/secrets/fogwall-api-key

Local (default)

Usernames and BCrypt password hashes are defined directly in the users: block. See Provisioning users in the administrator guide.

Every push must resolve to a fogwall user, whatever the authentication provider: users from the users: block and users in the database (created on IdP login or in the dashboard) both count. A push that resolves to none is refused as “Identity not linked”. users: [] with no other user source refuses every push.

LDAP

Authenticates users against a generic LDAP directory using a bind operation.

auth:
  provider: ldap
  ldap:
    # LDAP server URL including base DN.
    url: ldap://ldap.example.com:389/dc=example,dc=com

    # User DN pattern — {0} is substituted with the login username.
    user-dn-patterns: cn={0},ou=users

    # Optional bind credentials for group search / attribute lookup.
    bind-dn: cn=admin,dc=example,dc=com
    bind-password: secret

    # Base DN (relative to url base) to search for group membership.
    # When set, group names are mapped to roles via auth.role-mappings below.
    group-search-base: ou=groups

    # LDAP filter for group membership. {0} = user full DN, {1} = username.
    group-search-filter: "(member={0})"

  # Map fogwall role names to lists of LDAP group CNs.
  # When a user is a member of any listed group, the role is granted.
  role-mappings:
    ADMIN:
      - fogwall-admins
      - security-team

Active Directory

Authenticates users against an on-premises Active Directory domain using UPN bind (user@domain.com). Unlike the generic LDAP provider, no user-dn-patterns is required — Spring Security constructs the UPN automatically from the domain and the submitted username.

auth:
  provider: ad
  ad:
    # AD domain name — used to form user@domain UPN for bind.
    domain: corp.example.com

    # Domain controller URL. When omitted, Spring Security resolves the DC via DNS SRV records.
    url: ldap://dc.corp.example.com:389

    # Optional: base DN for group search. When set, group membership is used for role mapping.
    group-search-base: DC=corp,DC=example,DC=com

    # LDAP filter for group membership. {0} = user full DN.
    group-search-filter: "(member={0})"

  role-mappings:
    ADMIN:
      - CN=fogwall-admins,OU=Groups,DC=corp,DC=example,DC=com

Tip

The AD provider understands Active Directory error sub-codes on bind failure 49 (expired passwords, locked accounts, etc.) and maps them to specific Spring Security exceptions.

OIDC

Authenticates users via OpenID Connect authorization code flow (Keycloak, Okta, Entra ID, Dex, etc.).

auth:
  provider: oidc
  oidc:
    # OIDC issuer URI — Spring Security fetches {issuerUri}/.well-known/openid-configuration at startup.
    issuer-uri: https://accounts.example.com

    client-id: fogwall-client
    client-secret: fogwall-secret

    # Optional: path to a PKCS#8 PEM RSA private key for private_key_jwt client auth.
    # When set, client-secret is not required.
    # private-key-path: /run/secrets/fogwall-oidc-private-key.pem

    # Optional: path to the X.509 certificate (PEM) matching private-key-path.
    # Required for Entra ID — Entra matches registered certificates by x5t thumbprint, not kid.
    # Without this, every token exchange fails with AADSTS700027.
    # Generate: openssl req -new -x509 -key private.pem -out cert.pem -days 365
    # cert-path: /run/secrets/fogwall-oidc-cert.pem

    # Optional: explicit kid to embed in the private_key_jwt assertion header.
    # Use this for providers that match the assertion against a registered JWKS by kid
    # (Keycloak, Okta, Auth0, Dex). Without it, a random UUID kid is generated on each
    # restart, which breaks authentication with those providers.
    # Find the kid by inspecting your provider's JWKS endpoint and matching it to the
    # public key you registered.
    # Not needed when cert-path is set (Entra ID uses x5t instead of kid).
    # key-id: my-registered-kid

    # Claim used as the principal name (the fogwall username). Defaults to "sub" — the only
    # claim the OIDC spec guarantees in every ID token. Claims like preferred_username and
    # email are voluntary: point this at one only if your IdP actually sends it, otherwise
    # login fails with a clear "claim not present" error.
    # user-name-attribute: email

    # Read all claims from the ID token and never call the UserInfo endpoint. Needed when
    # user-name-attribute names a claim your IdP's UserInfo response omits (see the Entra ID
    # section below); harmless otherwise.
    # skip-user-info: true

    # Endpoint overrides. OIDC discovery ALWAYS runs at startup against issuer-uri; these
    # replace individual discovered endpoints for split-egress setups (e.g. an internal JWKS
    # mirror). They have no effect on token validation.
    # authorization-uri: ...
    # token-uri: ...
    # user-info-uri: ...
    # jwk-set-uri: ...

  # OIDC claim containing the user's group memberships. Defaults to "groups",
  # which is standard for Keycloak, Okta, and most Entra ID configurations.
  groups-claim: groups

  # Map fogwall role names to lists of OIDC group values from the claim above.
  role-mappings:
    ADMIN:
      - fogwall-admins

Entra ID (Azure AD)

Entra ID works with standard OIDC discovery and full stock token validation — no bypass and no endpoint overrides. What it does need is two settings that account for its unusual (but spec-conformant — all profile/email claims are voluntary, only sub is guaranteed) claims behaviour:

  • user-name-attribute: email — with the email optional claim added to the app registration (checklist below). Don’t skip this one: the default (sub) logs in without any error, but Entra’s sub is an opaque generated string, so every downstream record — permissions, push history, the admin UI — ends up keyed to an unreadable identifier. Nothing breaks loudly; it is just miserable to operate.
  • skip-user-info: true (recommended). Entra’s UserInfo endpoint is Microsoft Graph, which returns HTTP 200 with a minimal claim set and has historically broken Spring’s principal construction when user-name-attribute names a claim its response lacks. Skipping UserInfo reads all claims from the ID token and drops the runtime dependency on Graph (and its User.Read permission) entirely.
auth:
  provider: oidc
  oidc:
    # The /v2.0 suffix matters: it selects the v2 endpoints, and ID-token format follows the
    # endpoint — v2 tokens carry iss=https://login.microsoftonline.com/{tenant-id}/v2.0, which
    # matches discovery and validates cleanly.
    issuer-uri: https://login.microsoftonline.com/{tenant-id}/v2.0
    client-id: <app-registration-client-id>
    client-secret: <client-secret>
    skip-user-info: true
    user-name-attribute: email

  # Requires "Group claims" to be enabled in the app registration (Token configuration → Groups claim).
  # Group values will be object IDs (GUIDs) unless "Group names" is selected in the manifest.
  groups-claim: groups

  role-mappings:
    ADMIN:
      - <object-id-of-admin-group>

Important

App registration checklist:

  1. Platform: Web — redirect URI https://<your-host>/login/oauth2/code/fogwall
  2. API permissions: openid, profile, email (delegated)
  3. Token configuration → add Groups claim → select “Security groups”
  4. Token configuration → add optional claim → ID token → email (feeds fogwall’s locked-email provisioning)

Legacy v1-token tenants. An app registration reached through the v1 endpoints issues v1-format tokens with iss=https://sts.windows.net/{tenant-id}/, which fails validation against the v2 discovery issuer. The fix is to use the v2 issuer URI above. If your tenant genuinely cannot, point issuer-uri directly at https://sts.windows.net/{tenant-id}/ — it serves a self-consistent discovery document — and set user-name-attribute to a claim v1-format tokens actually carry (check a decoded token; upn is the usual candidate).

To tell which case you are in, decode an ID token from a real login and check its ver claim ("1.0" or "2.0") — that is authoritative. Don’t infer it from the app manifest: Microsoft documents requestedAccessTokenVersion as applying to access tokens issued to your app when it acts as an API, and in deployments verified so far a tenant with that setting unset still issued v2 ID tokens through the /v2.0 endpoints — but tenant configurations vary, so check the token.

Role mappings

auth.role-mappings applies to LDAP, AD, and OIDC. Keys are role names (without the ROLE_ prefix); values are lists of group names or claim values from the IdP.

RoleDashboard access
READERView pushes, repositories, providers, and fetch and SCM API activity
USERView and act on pushes awaiting approval
AUDITORView pushes + read users, groups and permissions; changes nothing, even with ADMIN
ADMINAll USER permissions + create/delete users, reset passwords, manage identities
SELF_CERTIFYThe capability to self-approve; admits no one on its own

A mapping grants the role it names and nothing else, except that ADMIN brings USER with it. A user needs USER or ADMIN to act, so map the group whose members push and review to USER.

auth:
  role-mappings:
    USER:
      - fogwall-users
    ADMIN:
      - fogwall-admins
    AUDITOR:
      - fogwall-auditors
    SELF_CERTIFY:
      - fogwall-trusted
  default-role: NONE

Default role

auth.default-role decides what happens to a user whose mappings give them none of READER, USER, AUDITOR or ADMIN: one matching no group, or one matched only by SELF_CERTIFY. The default role is added to whatever they did match.

default-roleA user with no mapped session role
NONE (default)Is refused sign-in
READERSigns in, sees what is going on and changes nothing
USERSigns in and acts, subject to repository permissions like any user
AUDITORSigns in read-only, and also reads users, groups and permissions

Startup fails when role-mappings is empty and default-role is NONE, since no one could sign in. It also warns about each mapping that admits no one alone, such as SELF_CERTIFY, unless default-role is USER.

auth.require-role-mapping is deprecated in favour of default-role and still accepted with a startup warning: true reads as NONE, false as READER. Setting both fails startup.

Upgrading from 1.4

Earlier releases granted USER with every mapping and to every user admitted without one. A deployment that relied on that loses the ability to act until it maps a USER group or sets default-role: USER:

  • with an empty role-mappings, startup fails until default-role is set to a role;
  • users admitted by require-role-mapping: false now sign in as READER;
  • users matched only by a SELF_CERTIFY mapping are refused, or sign in as READER with require-role-mapping: false;
  • users matched only by an AUDITOR mapping are unaffected, since an auditor never acts.

A configured local user with no roles: is still a USER, and one with a list holds exactly that list. A local user listed as roles: [SELF_CERTIFY] alone is now refused sign-in and needs roles: [USER, SELF_CERTIFY]; startup logs a warning naming each such user.