Keycloak
Dashy supports using a Keycloak (V17+) authentication server.
Keycloak is a Java-based open source, high-performance, secure authentication system, supported by RedHat. It can be deployed with Docker, and enables you to secure multiple self-hosted applications with single-sign-on using standard protocols (OpenID Connect, OAuth 2.0, SAML 2.0 and social login).
Contentsβ
- 1. Deploy Keycloak
- 2. Configure Keycloak
- 3. Enabling Keycloak in Dashy
- 4. Groups and Roles
- Troubleshooting
- How it Works
1. Deploy Keycloakβ
If you've not already done so, spin up a Keycloak instance. You can do this by following the Keycloak Docs, or use the following Docker examples:
docker run -d \
-p 9100:8080 \
--name keycloak \
-e KEYCLOAK_ADMIN=kc-admin \
-e KEYCLOAK_ADMIN_PASSWORD=KeycloakAdmin2026! \
quay.io/keycloak/keycloak:25.0 start-dev
Example docker-compose.yml
KEYCLOAK_ADMIN=kc-admin
KEYCLOAK_ADMIN_PASSWORD=KeycloakAdmin2026!
name: dashy-keycloak
services:
keycloak:
image: quay.io/keycloak/keycloak:25.0
command:
- start-dev
- --http-port=9100
- --hostname-strict=false
- --health-enabled=true
restart: unless-stopped
ports:
- "9100:9100"
- "4000:8080"
volumes:
- keycloak-data:/opt/keycloak/data
environment:
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN}
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
KC_HTTP_ENABLED: "true"
KC_HOSTNAME_STRICT: "false"
KC_HEALTH_ENABLED: "true"
healthcheck:
test: ["CMD-SHELL", "timeout 2 bash -c '</dev/tcp/127.0.0.1/9100'"]
start_period: 30s
interval: 10s
timeout: 5s
retries: 15
dashy:
image: lissy93/dashy:4.1.0
network_mode: service:keycloak
restart: unless-stopped
depends_on:
keycloak:
condition: service_healthy
environment:
NODE_ENV: production
HOST: 0.0.0.0
PORT: 8080
volumes:
- ./user-data:/app/user-data
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz >/dev/null 2>&1"]
start_period: 30s
interval: 10s
timeout: 5s
retries: 15
volumes:
keycloak-data:
You should now be able to access the Keycloak web interface at http://127.0.0.1:9100, log in with your admin credentials above, and create a new password when prompted.
2. Configure Keycloakβ
Create the Keycloak realmβ
- Open
http://localhost:9100. - Click Administration Console.
- Log in as
kc-admin. - Open the realm selector in the top-left.
- Click Create realm.
- Set Realm name to
dashy. - Click Create.
Allow Dashy's local originβ
- In the
dashyrealm, open Realm settings. - Open Security defenses.
- Open Headers.
- Clear X-Frame-Options.
- Set Content-Security-Policy to:
frame-src 'self' http://localhost:4000 http://127.0.0.1:4000; frame-ancestors 'self' http://localhost:4000 http://127.0.0.1:4000; object-src 'none';
- Click Save.
Create the Dashy clientβ
- In the
dashyrealm, open Clients. - Click Create client.
- Set Client type to
OpenID Connect. - Set Client ID to
dashy. - Click Next.
- Turn Client authentication off. Dashy is a SPA using PKCE, so it must be a public client.
- Leave Standard flow on.
- Leave Direct access grants on or off; Dashy does not require it.
- Click Next.
- Set Valid redirect URIs to:
http://localhost:4000/*http://127.0.0.1:4000/*
- Set Valid post logout redirect URIs to:
http://localhost:4000/*http://127.0.0.1:4000/*
- Set Web origins to:
http://localhost:4000http://127.0.0.1:4000
- Click Save.
Add the realm-role mapperβ
Dashy uses adminRole: dashy-admin in user-data/conf.yml. For server-side admin checks to work, Keycloak must include realm roles in the ID token
- Open Clients
- Click the
dashyclient - Open Client scopes
- Click the dedicated scope row, usually named
dashy-dedicated - Click Add mapper
- Click By configuration
- Select User Realm Role
- Set Name to
dashy-realm-roles - Set Token Claim Name to
realm_access.roles - Turn Multivalued on
- Turn Add to ID token on
- Turn Add to access token on
- Turn Add to userinfo on
- Click Save
Create the Dashy admin roleβ
- In the
dashyrealm, open Realm roles - Click Create role
- Set Role name to
dashy-admin - Click Save
(Alternative) Use a group for adminβ
To grant admin via group membership instead of a realm role, add a second mapper of type Group Membership with claim name groups, create the group under Groups, assign your admin users to it, then set adminGroup: <group-name> (in place of adminRole) in your Dashy config later.
Create test usersβ
Create an admin user:
- Open Users
- Click Add user
- Set Username to
keycloak-admin - Set Email verified to on
- Set First name to
Keycloak - Set Last name to
Admin - Click Create
- Open the Credentials tab
- Click Set password
- Set a password, turn Temporary off, and save it
- Open the Role mapping tab
- Click Assign role
- Filter by realm roles, select
dashy-admin, and assign it
Create a normal user:
- Open Users
- Click Add user
- Set Username to
keycloak-user - Set Email verified to on
- Set First name to
Keycloak - Set Last name to
User - Click Create
- Open the Credentials tab
- Set a non-temporary password
- Do not assign
dashy-admin
Summaryβ
Keycloak should now be configured, and ready to go!
The Keycloak UI is not super intuitive, so if you're struggling to find where to configure any of the above options, below is a full start-to-end walkthrough video:
https://github.com/user-attachments/assets/12b6a596-1ec6-453a-9ff7-d4e2c3aa69f7
If you need to, you can make a backup of your Keycloak config, with their built-in backup tool. Something like:
docker compose run --rm --no-deps \
-v ./backup:/backup \
keycloak export \
--realm dashy \
--dir /backup \
--users realm_file \
--optimized
Here's an example of a configured dashy-realm.json backup.
3. Enabling Keycloak in Dashyβ
Finally, you need to tell Dashy to use the new Keycloak setup, and only allow access from authorized Keycloak users. This is done in the appConfig.auth section of your main /user-data/conf.yml file.
As an example, you can view this conf.yml, which is fully-configured with Keycloak auth using the info from the above steps.
appConfig:
...
disableConfigurationForNonAdmin: true
auth:
enableKeycloak: true
keycloak:
serverUrl: http://localhost:9100
realm: dashy
clientId: dashy
adminRole: dashy-admin
Where:
disableConfigurationForNonAdmin- Prevent read/write config access to non-admin Keycloak usersauth.enableKeycloak- Set the auth mode to KeycloakserverUrl- The host (no paths) to your Keycloak instance, accessible from the Dashy containerrealm- The name (case sensitive) of the Keycloak realm to useclientId- Client ID that you created for DashyadminRole- The role name that grants adminadminGroup- (Alternative toadminRole) Group name that grants adminidpHint- (Optional) Alias of an external IdP federated through Keycloak; skips the Keycloak login page and redirects straight to that provider
Note that a restart is required for these changes to take effect.
If Keycloak runs on a different host or behind a reverse proxy, make sure serverUrl is reachable from inside the Dashy container, and that the realm's redirect URIs and Web Origins match Dashy's public URL.
Everything should now be fully configured and working π
Now, when you load Dashy, you'll be redirected to your Keycloak login page, after logging in you will then land back on Dashy's homepage with full access! All Dashy's client, server and asset endpoints will be blocked, and return a 403 until you are authenticated.
4. Groups and Rolesβ
Keycloak allows you to assign users roles and groups. Dashy supports using these roles, to configure who can access various sections or items in Dashy's frontend.
For example, to make any given section only visible to admins, simply add the following to the section's displayData section:
displayData:
showForKeycloakUsers:
roles:
- dashy-admin # ID of the admin role you created earlier
Keycloak server administration and configuration is a deep topic; please refer to the server admin guide to see details about creating and assigning roles and groups.
Once you have groups or roles assigned to users you can configure access under each section or item displayData.showForKeycloakUser and displayData.hideForKeycloakUser.
Both show and hide configurations accept a list of groups and roles that limit access. If a users data matches one or more items in these lists they will be allowed or excluded as defined.
sections:
- name: DeveloperResources
displayData:
showForKeycloakUsers:
roles: ['canViewDevResources']
hideForKeycloakUsers:
groups: ['ProductTeam']
items:
- title: Not Visible for developers
displayData:
hideForKeycloakUsers:
groups: ['DevelopmentTeam']
Troubleshooting common Keycloak Issuesβ
Client Authentication Issueβ
Problem: Redirect loop, if client authentication is enabled.
Solution: Switch off "Client authentication" in the dashy client's "Advanced" settings.
Double URLβ
Problem: If you get redirected to "https://dashy.my.domain/#iss=https://keycloak.my.domain/realms/dashy"
Solution: Turn on "Exclude Issuer From Authentication Response" in the dashy client's "Advanced" -> "OpenID Connect Compatibility Modes".
Problems with multiple Dashy Pagesβ
Problem: Refreshing or logging out of dashy results in an "invalid_redirect_uri" error.
Solution: In the dashy client's "Access settings", set "Root URL" to https://dashy.my.domain/, and make sure the valid redirect URIs end in /*.
403 on login-status-iframe.html/initβ
Problem: Browser console shows a 403 from Keycloak when the SPA loads.
Solution: Open the dashy client's "Web origins" and remove any trailing /*. Web Origins must be bare origins (e.g. http://localhost:4000), not http://localhost:4000/*.
CSP error for /3p-cookies/step1.html or "Authentication failed (Keycloak)"β
Problem: The hidden Keycloak iframe is blocked by frame-ancestors.
Solution: In the dashy realm (not master), open Realm settings -> Security defenses -> Headers. Clear X-Frame-Options and set the Content-Security-Policy as described earlier in this section.
Dashy server can't reach Keycloakβ
Problem: SPA loads fine but every authenticated API call returns 401, and the Dashy server logs show [auth-oidc] token verification failed or fetch errors for .well-known/openid-configuration.
Solution: serverUrl must be reachable from inside the Dashy container, not just from the browser. In Docker, put both services on the same network and use the service name (e.g. http://keycloak:8080). Test with docker exec <dashy-container> wget -qO- "$SERVER_URL/realms/dashy/.well-known/openid-configuration".
Logged in but config saves return 403β
Problem: User authenticates fine, but saving the dashboard returns 403.
Solution: The id_token doesn't carry the admin claim. Confirm the Add to ID token toggle on the Step 2 mapper is on. Paste the token (from localStorage, key ID_TOKEN) into jwt.io and look for realm_access.roles (or groups if you're using adminGroup).
Issuer mismatch behind a reverse proxyβ
Problem: Server logs show unexpected "iss" claim value. The browser reaches Keycloak over HTTPS but Keycloak advertises an HTTP issuer in its discovery document.
Solution: Set KC_HOSTNAME=<public-host> on Keycloak so the issuer matches the public URL, and ensure your reverse proxy forwards X-Forwarded-Proto: https.
Audience mismatch on token verificationβ
Problem: Server logs show unexpected "aud" claim value. Every auth'd API call returns 401.
Solution: clientId in conf.yml must exactly match the Keycloak client's Client ID. Check for trailing whitespace, case mismatches, or accidentally using the client's internal UUID instead of the Client ID string.
Self-signed Keycloak certificate rejectedβ
Problem: Dashy server logs show TLS errors (self-signed certificate, UNABLE_TO_VERIFY_LEAF_SIGNATURE) when fetching JWKS or discovery.
Solution: Use a real certificate on Keycloak (Let's Encrypt, or your homelab CA), or mount your CA bundle into the Dashy container and set NODE_EXTRA_CA_CERTS=/path/to/ca.pem.
Token expired / clock skewβ
Problem: 401s with "exp" claim timestamp check failed or "iat" claim timestamp check failed, even just after login.
Solution: Dashy allows 30 seconds of drift. Sync clocks on both hosts with NTP. Container clocks follow their host, so it's almost always the host that's drifted.
Mixed content blocked by the browserβ
Problem: Dashy served over HTTPS, Keycloak over HTTP. The browser blocks the token or JWKS endpoint with a mixed-content error.
Solution: Terminate both behind HTTPS. For local testing, use HTTP on both, but never mix schemes in the same flow.
Numeric Client ID truncatedβ
Problem: Token verification fails with audience mismatch when clientId in conf.yml is a long numeric string.
Solution: Wrap numeric Client IDs in quotes (e.g. clientId: "12345678901234567"). Without quotes YAML parses the value as a JS number and loses precision past about 15 digits.
Logout lands on a broken pageβ
Problem: Clicking Logout in Dashy ends on a Keycloak error or 404 instead of returning to Dashy.
Solution: Add Dashy's URL to Valid post logout redirect URIs on the dashy client. Keycloak v18+ requires registered, exact-match post-logout URIs.
check-sso hangs in strict browsersβ
Problem: First load spins indefinitely. Browser console reports blocked third-party cookies. Common in Safari, Brave, or Firefox with strict tracking protection.
Solution: Serve Dashy and Keycloak under the same registrable domain (e.g. dashy.example.com and auth.example.com) so the session cookie is first-party.
Config change to auth.keycloak not picked upβ
Problem: Updated serverUrl, realm, clientId, adminRole, or adminGroup in conf.yml, but Dashy still authenticates against the old values.
Solution: The server reads the auth config only at boot. Restart the Dashy container after any change to fields under auth.keycloak.
How it Worksβ
If you're a developer or contributor looking to understand or make changes to Dashy's Keycloak implementation, the following outlines how it's wired together.
The same OIDC pipeline backs both Keycloak and generic OIDC providers. The only Keycloak-specific code is the SPA adapter, which uses keycloak-js so it can take advantage of check-sso and silent token renewal.
Client sideβ
Boot starts in src/main.js. After the initial /conf.yml fetch parses the auth block, isKeycloakEnabled() decides whether to lazily import keycloak-js and call initKeycloakAuth().
src/utils/auth/KeycloakAuth.js wraps the adapter. On load it calls keycloakClient.init({ onLoad: 'check-sso', responseMode: 'query' }). If a Keycloak session already exists the user is silently authenticated; otherwise the SPA redirects to the login page with PKCE. On return, storeKeycloakInfo() persists the raw id_token, the user's groups and roles, preferred_username, and a derived isAdmin flag to localStorage, then hard-redirects to / so the SPA boots a second time with the Bearer token in place.
src/utils/auth/getApiAuthHeader.js builds the Authorization header for every internal API call. It does a client-side exp check and returns null for missing or expired tokens, so the next request triggers a fresh login rather than a 401.
The localStorage keys (ID_TOKEN, KEYCLOAK_INFO, USERNAME, ISADMIN) live in src/utils/config/defaults.js. src/utils/IsVisibleToUser.js reads KEYCLOAK_INFO when evaluating showForKeycloakUsers and hideForKeycloakUsers rules.
Server sideβ
services/auth-oidc.js contains the entire server-side auth surface, in five small pieces:
loadOidcSettings()readsauth.keycloak(orauth.oidc) at boot and returns a normalised{ issuer, clientId, adminGroup, adminRole }. For Keycloak the issuer is<serverUrl>/realms/<realm>.createOidcMiddleware()returns a Connect middleware. Permissive on no-token requests so the SPA can bootstrap; otherwise it verifies the Bearer token against the realm's JWKS usingjose. Checks cover signature, issuer (against the canonical value from the discovery doc), audience (must equalclientId), and expiry, with a 30-second clock-skew tolerance. Setsreq.auth = { user, isAdmin, claims }on success,401on failure.getIssuerContext()lazily fetches.well-known/openid-configurationon first use and wrapsjwks_uriincreateRemoteJWKSet, which handles JWKS caching and on-demand key rotation. The result is memoised per-issuer for the life of the process.deriveIsAdmin()checks the token'sgroupsclaim againstadminGroup, and the union ofrealm_access.rolesandresource_access.<clientId>.rolesagainstadminRole. Either match returns true.maybeBootstrapConfig()is the stripped-response helper. When auth is configured, guest access is off, and an unauthenticated request hits the root/conf.yml, it returns a minimal copy with onlyappConfig.auth,appConfig.enableServiceWorker, and apageInfo.titleofLogin | <your title>. Sections, items, hostnames and any other secrets never leave the server.
services/app.js wires it all together. The middleware mounts as protectConfig in front of every YAML route and config-mutating route. The /*.yml handler sets Cache-Control: private, no-store and Vary: Authorization whenever auth is configured (so intermediate caches can never mix auth states), then calls maybeBootstrapConfig; a stripped result is sent as-is, otherwise res.sendFile serves the full file. POST /config-manager/save is additionally guarded by requireAdmin, which returns 401 if req.auth is unset and 403 if req.auth.isAdmin is false.
Why the mapper mattersβ
The server's admin check reads from the id_token only. Keycloak's default mapper adds realm roles to the access token but not the id token, so without the Step 2 Add to ID token toggle, realm_access.roles is absent and every user is treated as non-admin. The same applies to groups if you use adminGroup instead. This is the single most common cause of "logged in fine, but can't save changes".