Identity & SSO
The identity fabric is what turns golemsec from "a pile of running apps" into "a company people can actually log into." Active Directory is the source of truth; Keycloak federates it; apps authenticate employees via OIDC SSO.
Active Directory
- Forest
corp.golemsec.io, NetBIOS GOLEMSEC, 2 DCs (DC01 root, DC02 replica + NPS/RADIUS). - 200 users under
OU=<dept>,OU=golemsec,DC=corp,DC=golemsec,DC=io. - Verified healthy:
ldapsearchreturns all 200 users with mail attributes; user and admin binds succeed.
Keycloak federation
- Version: Keycloak 24.0.5 (Docker,
start-dev, H2), containerkeycloak, port 8080. - Realm:
golemsec· LDAP provider:ad-corp(component ID39a545bb-c6a7-47df-b540-91aa9786e6f2). - Bind:
Administrator@corp.golemsec.io(UPN bind), users DNOU=golemsec,DC=corp,DC=golemsec,DC=io, subtree scope,editMode=READ_ONLY,importEnabled=true. - Result (verified): 200 users federated (
192 added / 8 updated / 0 failed). Login confirmed by username and by email (itai.williams,david.cohen,linda.miller@golemsec.io), withpreferred_username / email / name / given_name / family_nameclaims sourced from AD.
⚠️ Critical build gotcha (do not repeat)
Creating the realm and the LDAP provider with separate REST calls leaves the realm unresolvable inside Keycloak's background sync transaction → NullPointerException in LDAPStorageProviderFactory.getLDAPConfigDecorators ("realm is null"). The symptom is maddening: sync returns {"errorMessage":"UnknownError"} with no server log, and logins silently fail — even though AD LDAP is perfectly healthy.
Fix: create the realm with the LDAP provider embedded in the realm representation's components/subComponents as a single atomic POST /admin/realms import (provisioning/linux/kc-realm-import.sh). This bug is not version-specific (reproduced on 24.0.5 and 26.0.8).
Other traps:
GET /componentsmasksbindCredentialas**********; editing-then-PUTting the component verbatim overwrites the real password with the mask. Test the stored credential viatestLDAPConnectionwithcomponentIdand no inline credential.- To surface the swallowed exception, set
fullSyncPeriod=15so the Timer path logs the full stack at ERROR (the manual-sync endpoint swallows it).
OIDC clients (realm golemsec)
| Client ID | App | Redirect URI |
|---|---|---|
grafana | Grafana | http://10.50.50.213:3000/login/generic_oauth |
gitlab | GitLab | http://10.50.20.107/users/auth/openid_connect/callback |
nextcloud | Nextcloud | http://10.50.10.51/apps/user_oidc/code |
glpi | GLPI | (client ready; app-side wiring pending) |
Client secrets are in Credentials & Break-Glass. Discovery URL for apps:
http://10.50.20.133:8080/realms/golemsec/.well-known/openid-configuration
Issuer derives from the request host in
start-devmode — keep all clients and apps pointing at10.50.20.133:8080so issuer validation matches.
App SSO status
| App | Status | How |
|---|---|---|
| Grafana (NOC) | ✅ Live & verified | GF_AUTH_GENERIC_OAUTH_* env; "golemsec SSO" button, 302→Keycloak w/ PKCE, token issued for AD user |
| GitLab (dev) | ✅ Live & verified | omnibus gitlab.rb OmniAuth openid_connect block + gitlab-ctl reconfigure; SSO button rendered (raw GET → 404 is normal GitLab CSRF protection, button POSTs) |
| Nextcloud (files) | ✅ Live & verified | installed (SQLite) + user_oidc app + user_oidc:provider golemsec |
| Zabbix (NOC) | ✅ Live & verified | backend DB repaired, then direct AD LDAP (authentication.update, 6.0 model); itai.williams + david.cohen login verified |
| ERPNext (ERP/finance) | ✅ Live & verified | Frappe built-in LDAP Settings → AD; itai.williams + david.cohen login + JIT auto-provisioned as ERPNext users |
| Jenkins (CI) | ✅ Live & verified | LDAP plugin + init.groovy.d AD security realm (setup wizard bypassed); itai.williams authenticates and AD groups resolve (GG-Executive) |
| GLPI (helpdesk/assets) | ✅ Live & verified | installed (was at wizard) on data-tier MySQL, then AD LDAP directory via GLPI AuthLDAP (GLPIKey-encrypted bind pw); 200 employees imported via glpi:ldap:synchronize_users |
| SuiteCRM (CRM) | ✅ Live | config_override.php LDAPAuthenticate + auto-create → AD; Sales/Marketing sign in with company creds |
| Wazuh dashboard (SIEM) | ✅ Live & verified | OpenSearch-security LDAP authc/authz in config.yml + roles_mapping (GG-IT/GG-Executive→all_access); verified eitan.cohen → all_access |
| Akaunting (accounting) | Local auth | native LDAP/SSO is a paid module — standalone local auth by decision |
| OrangeHRM (HR) | Local (LDAP pending) | installed & working on local auth; AD LDAP = a 1-form Admin-UI step |
Three integration styles, by design. OIDC via Keycloak (Grafana, GitLab, Nextcloud), direct AD LDAP (Zabbix, ERPNext, Jenkins, GLPI, SuiteCRM, Wazuh), and framework-native LDAP — all resolving to the same AD identities.
Authorization (group → role)
Beyond authentication, AD-group → app-role mapping is wired where it matters:
- ERPNext —
GG-Sales→Sales roles,GG-IT→System Manager,GG-Finance→Accounts,GG-Executive→manager roles, etc. (all 10 departments; least-privilege default). - Jenkins — AD-group matrix authz (
GG-IT=admin,GG-RnD=build, others read-only). - Grafana — Keycloak realm roles
admin/editor→ IT/NOC get Admin, dev/exec get Editor. - Wazuh —
GG-IT/GG-Executive→all_access(dashboard admin).
Hardening backlog
Keycloak currently runs in start-dev/H2. Before treating it as a permanent service: set a fixed hostname, switch to production mode, and migrate H2 → PostgreSQL.