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 theemailoptional claim added to the app registration (checklist below). Don’t skip this one: the default (sub) logs in without any error, but Entra’ssubis 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 whenuser-name-attributenames a claim its response lacks. Skipping UserInfo reads all claims from the ID token and drops the runtime dependency on Graph (and itsUser.Readpermission) 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:
- Platform: Web — redirect URI
https://<your-host>/login/oauth2/code/fogwall- API permissions:
openid,profile,- Token configuration → add Groups claim → select “Security groups”
- Token configuration → add optional claim → ID token →
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.
| Role | Dashboard access |
|---|---|
READER | View pushes, repositories, providers, and fetch and SCM API activity |
USER | View and act on pushes awaiting approval |
AUDITOR | View pushes + read users, groups and permissions; changes nothing, even with ADMIN |
ADMIN | All USER permissions + create/delete users, reset passwords, manage identities |
SELF_CERTIFY | The 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-role | A user with no mapped session role |
|---|---|
NONE (default) | Is refused sign-in |
READER | Signs in, sees what is going on and changes nothing |
USER | Signs in and acts, subject to repository permissions like any user |
AUDITOR | Signs 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 untildefault-roleis set to a role; - users admitted by
require-role-mapping: falsenow sign in asREADER; - users matched only by a
SELF_CERTIFYmapping are refused, or sign in asREADERwithrequire-role-mapping: false; - users matched only by an
AUDITORmapping 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.