1. Introduction
1.1 What is SAML 2.0?
SAML (Security Assertion Markup Language) 2.0 is an XML-based open standard for exchanging authentication and authorization data between two parties: an Identity Provider (IdP) that authenticates users, and a Service Provider (SP) that hosts the application. Instead of every application managing its own username/password database, SAML lets you delegate authentication to a central IdP. When a user logs in once at the IdP, they get access to all connected SPs without entering credentials again — this is Single Sign-On (SSO).
In practical terms: the user clicks "Login with SSO" on the application, gets redirected to the IdP login page, authenticates there, and is sent back to the application with a cryptographically signed XML document (the "SAML assertion") that proves who they are and what groups they belong to.
1.2 Why Keycloak?
There are several IdP options available (Okta, Azure AD, ADFS, Ping Identity, etc.), so why Keycloak?
- Open-source and free — No per-user licensing costs, which matters at scale
- Self-hosted — Full control over your identity infrastructure; no dependency on external SaaS providers
- Protocol versatility — Supports SAML 2.0, OpenID Connect, and OAuth 2.0 in a single platform
- LDAP/AD federation — Connects directly to Active Directory without migrating users
- Kubernetes-native — Runs well as a containerized deployment with built-in clustering
- CNCF project — Active community, regular releases, and long-term sustainability
1.3 What We Will Build
Enterprise environments demand centralized identity management. When you operate HPE Morpheus Enterprise as your cloud management platform, integrating it with a dedicated Identity Provider (IdP) via SAML 2.0 eliminates password sprawl and gives you single sign-on (SSO) across the entire infrastructure stack.
In this post, we walk through the entire journey: understanding how Morpheus handles SAML under the hood, deploying Keycloak on Kubernetes as your IdP, wiring the two together, and diagnosing issues when things do not go as planned. Every configuration value and YAML snippet comes from a real lab deployment, so you can replicate it in your own environment.
Lab environment: Kubernetes (single-node / Minikube compatible), Keycloak 26.x, Morpheus Enterprise 8.x, Active Directory for user federation.
2. Morpheus SAML Architecture
HPE Morpheus Enterprise supports SAML 2.0 as an Identity Source type within its Administration panel. Understanding how Morpheus participates in the SAML exchange is essential before configuring anything on the Keycloak side.
📸 IMAGE: Morpheus > Login Screen
📸 IMAGE: Morpheus > Forward Keycloak Login Screen
2.1 Morpheus as a SAML Service Provider (SP)
Morpheus acts exclusively as a SAML Service Provider. It does not function as an Identity Provider itself. When a user attempts to log in via SSO, Morpheus generates a SAML AuthnRequest, redirects the browser to the configured IdP, and then consumes the SAML Response (assertion) returned by the IdP.
2.2 Key SAML Endpoints in Morpheus
When you create a SAML Identity Source in Morpheus, the platform automatically generates two critical values:
| Endpoint | Description |
|---|---|
| SP Entity ID | A unique identifier for Morpheus as a Service Provider. Auto-generated from the hostname, e.g. https://morpheus-url/saml/<uniqueID>
|
| SP ACS URL | The callback URL where the IdP posts the SAML Response after authentication, e.g. https://morpheus-url/externalLogin/callback/<uniqueID>
|
| Login Redirect URL | The IdP's SAML SSO endpoint where Morpheus sends the AuthnRequest |
| SAML Logout Redirect URL | The IdP's SAML SLO endpoint for single logout |
Note: The SP Entity ID and ACS URL are generated only after you save the Identity Source for the first time. You must save first, then copy these values to configure the IdP.
2.3 SAML Request and Response Configuration
Morpheus provides granular control over how SAML requests are signed and responses are validated:
| Setting | Options & Description |
|---|---|
| SAML Request | No Signature / Self Signed / Custom RSA Signature — Controls whether AuthnRequest messages are signed |
| SAML Response | Do Not Validate / Validate Assertion Signature — Controls signature validation on the IdP's assertion |
| POST Binding Mode | ON/OFF — Uses HTTP-POST binding instead of HTTP-Redirect |
| Includes SAML Request Parameter | Yes/No — Whether the SAML request is included in the redirect |
2.4 Assertion Attribute Mappings
Morpheus maps SAML assertion attributes to internal user fields:
| Morpheus Field | Expected SAML Attribute |
|---|---|
| Given Name | firstName |
| Surname | lastName |
email (or NameID) |
2.5 Role Mapping Mechanism
Morpheus supports role-based access control through SAML group assertions:
| Role Mapping Field | Description |
|---|---|
| Default Role | The role assigned to all authenticated users (e.g., Standard User) |
| Role Attribute Name | The SAML attribute containing group/role info (e.g., groups) |
| Required Role Attribute Value | A group name the user must belong to for authorization (e.g., mspusers) |
📸 IMAGE: Morpheus > Identity Sources > Keycloak
3. Keycloak-SAML Ecosystem: How It Works
Keycloak is an open-source Identity and Access Management (IAM) solution maintained by the CNCF. It supports OpenID Connect, OAuth 2.0, and SAML 2.0 protocols natively. In our setup, Keycloak serves as the SAML Identity Provider (IdP) that authenticates users against an Active Directory backend via LDAP federation.
3.1 Core Keycloak Concepts
| Concept | Description |
|---|---|
| Realm | A tenant-level isolation boundary. Each realm has its own users, clients, roles, and identity providers. Our realm: morpheus-lab
|
| Client | An application that delegates authentication to Keycloak. Morpheus is registered as a SAML client |
| User Federation | Allows Keycloak to pull users from LDAP/Active Directory without duplicating credentials |
| Protocol Mappers | Transform user attributes and group memberships into SAML assertions |
| Roles & Groups | Realm-level and client-level roles. AD groups can be synced and mapped into assertions |
3.2 SAML 2.0 Authentication Flow (SP-Initiated SSO)
The diagram below illustrates the SP-Initiated SAML SSO flow between Morpheus and Keycloak:
Step-by-step:
- The user navigates to the Morpheus login page and clicks the SSO login button.
- Morpheus generates a SAML AuthnRequest and redirects the user's browser to Keycloak's SAML endpoint:
https://keycloak-server:30443/realms/morpheus-lab/protocol/saml - Keycloak presents the login form. The user enters their AD credentials.
- Keycloak authenticates the user against the federated LDAP/AD backend.
- On successful authentication, Keycloak constructs a SAML Response containing signed assertions with user attributes (
firstName,lastName) and group memberships (groups). - Keycloak POSTs the SAML Response to the Morpheus ACS URL.
- Morpheus validates the assertion signature, maps attributes and roles, creates or updates the user session, and grants access.
Step 5 is where group resolution happens, and it is the step most likely to fail on a real Active Directory. Section 5.2 covers how to scope the group mapper so the assertion can actually be built.
3.3 Authentication vs Authorization
Authentication (Who are you?)
- Keycloak acts as the authentication broker, sitting between the application (Morpheus) and the identity store (Active Directory).
- User Federation (LDAP) allows Keycloak to verify credentials against AD without storing passwords locally. Keycloak performs LDAP BIND operations to authenticate users.
- MFA can be layered on top through Keycloak's authentication flow configuration. Step 10 in section 5 covers this.
- Session Management: Once authenticated, Keycloak creates a session. Subsequent SAML requests within the session's lifetime do not require re-authentication (SSO behavior).
Authorization (What can you do?)
- Keycloak syncs AD groups via the LDAP Group Mapper (
mspusers,apparchitech,selfservice). - The SAML Group List Mapper serializes group memberships into the SAML assertion as a
groupsattribute. - Morpheus reads the
groupsattribute and maps it to internal roles:-
mspusers→ authorized user (Required Role) -
apparchitech→ Application Architect role -
selfservice→ Self-Service User role
-
- Users not in the required group are denied access even if authentication succeeds.
3.4 Single Logout (SLO) Flow
User clicks Logout → Morpheus sends LogoutRequest → Keycloak terminates session
→ Keycloak POSTs LogoutResponse to /login/auth → User lands on login page
Warning: The Logout Service POST Binding URL must be set to
/login/auth(the login page), NOT the ACS callback URL. Morpheus's ACS handler cannot process LogoutResponse objects and throws aGroovyCastException.
3.5 Session Lifetime: Two Clocks, Not One
There are two independent sessions in this architecture, and forgetting the second one produces surprising behaviour. Morpheus keeps its own browser session with its own inactivity timer, while Keycloak keeps an SSO session backed by a browser cookie. Closing the browser ends the Morpheus session but not necessarily the Keycloak one, so clicking the SSO button again can put the user straight back in without a password prompt.
Keycloak's SSO Session Idle must therefore be equal to or shorter than the Morpheus session timeout. Section 5.11 gives the values we run.
4. Deploying Keycloak on Kubernetes
This section walks through deploying a production-grade Keycloak cluster on Kubernetes using a single YAML manifest.
The complete Kubernetes manifest used in this guide is available on GitHub:
keycloak.yaml on GitHub
4.1 Architecture Overview
The deployment stack:
- Keycloak StatefulSet (2 replicas) with Infinispan clustering for HA
- PostgreSQL Deployment with PersistentVolumeClaim (10Gi)
- Kubernetes Secret for admin and database passwords
- Self-signed TLS certificates generated by init containers
- NodePort Service for external HTTPS access on port 30443
- Headless Service for Infinispan/JGroups cluster discovery
4.2 Prerequisites
Infrastructure:
- A running Kubernetes cluster or Minikube instance
-
kubectlconfigured and connected to your cluster - At least 4GB RAM and 2 CPU cores available for the Keycloak + PostgreSQL pods
- A storage provisioner (default StorageClass or Rook-Ceph). For Minikube, enable it with:
minikube addons enable default-storageclass
minikube addons enable storage-provisioner
Minikube users: Start Minikube with sufficient resources:
minikube start --cpus=4 --memory=8192 --driver=docker
Check what your cluster actually offers before applying anything:
kubectl get sc
If no StorageClass is marked (default), the PVC in this manifest must name one explicitly. Step 2 shows where.
Network & DNS requirements:
- The Kubernetes node IP must be reachable from the Morpheus server (for SAML redirects)
- The Morpheus server hostname (e.g.,
morpheus-server) must be resolvable from both the user's browser and the Keycloak pods. If you're using a local domain, add entries to/etc/hostson the machines or configure your internal DNS - Firewall rules: Ensure these ports are open between the components:
| Source | Destination | Port | Protocol | Purpose |
|---|---|---|---|---|
| User Browser | Morpheus Server | 443 | HTTPS | Access Morpheus UI |
| User Browser | K8s Node | 30443 | HTTPS | Keycloak login page (SAML redirect) |
| Morpheus Server | K8s Node | 30443 | HTTPS | SAML backchannel (POST binding) |
| K8s Pod Network | AD Domain Controller | 389 | LDAP | User federation / authentication |
Active Directory requirements:
- A dedicated service account for Keycloak LDAP binding (e.g.,
svc-keycloak). This account needs read-only access to the Users container (CN=Users,DC=yourdomain,DC=local). It does not need Domain Admin privileges — basic "Read all user information" permission is sufficient - AD groups that will map to Morpheus roles (e.g.,
mspusers,apparchitech,selfservice) must exist and users must be members of the appropriate groups - Ideally, put those three groups in a dedicated OU rather than leaving them in
CN=Usersalongside the built-in groups. Section 5.2 explains why this matters for group resolution
4.3 Step 1: Prepare Secrets
The YAML uses a Kubernetes Secret to store sensitive credentials. The passwords are Base64-encoded:
# Encode your passwords
echo -n 'YourAdminPassword!' | base64
# Output: WW91*****UGFzc3***cmQh
echo -n 'YourDBPassword!' | base64
# Output: WW9************mQh
The Secret resource in the YAML:
apiVersion: v1
kind: Secret
metadata:
name: keycloak-secret
namespace: keycloak
type: Opaque
data:
admin-password: <base64-encoded-admin-password>
db-password: <base64-encoded-db-password>
Note: Never commit plain-text passwords to version control. Base64 is encoding, not encryption — anyone with the file has the password. Use a secrets manager (Vault, Sealed Secrets) in production.
4.4 Step 2: Namespace and Storage
apiVersion: v1
kind: Namespace
metadata:
name: keycloak
labels:
app.kubernetes.io/part-of: sso-stack
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: keycloak
spec:
accessModes: [ReadWriteOnce]
storageClassName: rook-ceph-block
resources:
requests:
storage: 10Gi
Two things in that snippet are easy to get wrong and both fail quietly.
Every object needs its own --- separator. Without it, two objects merge into a single YAML document with duplicate apiVersion and kind keys, and one of them silently disappears (or the parser rejects the file outright, depending on your kubectl version).
storageClassName should name the class from kubectl get sc. Leave it out only if your cluster has a class marked (default). When neither is true the PVC sits in Pending forever, PostgreSQL never schedules, and the Keycloak pod stays in Init:0/1 waiting on a database that will never come up. The value above is what we run on Rook-Ceph; substitute yours.
Note that storageClassName is immutable. If you need to change it after the fact, delete and recreate the PVC:
kubectl -n keycloak scale deploy/postgres --replicas=0
kubectl -n keycloak delete pvc postgres-pvc
kubectl apply -f keycloak.yaml
4.5 Step 3: PostgreSQL Database
Keycloak requires an external database in production mode. Key points from our manifest:
containers:
- name: postgres
image: postgres:17
env:
- name: POSTGRES_USER
value: keycloak
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-secret
key: db-password
- name: POSTGRES_DB
value: keycloak
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
readinessProbe:
exec:
command: ["pg_isready", "-U", "keycloak"]
initialDelaySeconds: 10
periodSeconds: 5
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
This database is not just configuration storage. Once you enable MFA (Step 10), it also holds every user's OTP secret, which means losing this volume forces every user to re-enrol their authenticator. Back up the PVC accordingly.
4.6 Step 4: Keycloak StatefulSet
The Keycloak deployment uses a StatefulSet with 2 replicas for high availability.
Init Containers:
- wait-for-postgres: Polls PostgreSQL port 5432 before Keycloak starts
- generate-tls-cert: Generates a self-signed TLS certificate (10 years validity)
initContainers:
- name: generate-tls-cert
image: alpine:3.19
command: ["sh", "-c"]
args:
- |
apk add --no-cache openssl
openssl req -x509 -nodes -days 3650 \
-newkey rsa:2048 \
-keyout /certs/tls.key \
-out /certs/tls.crt \
-subj "/CN=keycloak-morpheus-lab/O=Lab/C=TR"
That init container pulls the openssl package at runtime, so the pod network needs outbound access to the Alpine repositories. In an air-gapped cluster, bake the certificate into a Secret instead.
Key Environment Variables:
| Variable | Purpose |
|---|---|
KC_BOOTSTRAP_ADMIN_USERNAME |
Initial admin username (admin) |
KC_BOOTSTRAP_ADMIN_PASSWORD |
Admin password from Secret |
KC_DB / KC_DB_URL_HOST
|
Database type and host (postgres) |
KC_HTTPS_CERTIFICATE_FILE |
Path to TLS certificate |
KC_HTTPS_CERTIFICATE_KEY_FILE |
Path to TLS key |
KC_CACHE |
Clustering mode (ispn = Infinispan) |
KC_HOSTNAME_STRICT |
Disabled for NodePort/self-signed setups |
KC_FEATURES |
Additional features (token-exchange) |
KC_HEALTH_ENABLED |
Enables /health/* endpoints for probes |
The KC_BOOTSTRAP_ADMIN_* variables apply only on first boot against an empty database. After the admin user exists, editing them and re-applying the manifest changes nothing; rotate the password from the admin console or kcadm.sh instead.
Health Probes:
-
startupProbe:
/health/started— 1s interval, 600 retries (10-minute startup window) -
readinessProbe:
/health/ready— Every 10s, 3 failures to mark unready -
livenessProbe:
/health/live— Every 10s, 3 failures to restart pod
4.7 Step 5: Services
| Service | Type & Purpose |
|---|---|
keycloak (ClusterIP) |
Internal access: ports 8080 (HTTP) and 8443 (HTTPS) |
keycloak-discovery (Headless) |
JGroups cluster member discovery on port 7800 |
keycloak-nodeport (NodePort) |
External access: port 30080 (HTTP) and 30443 (HTTPS) |
4.8 Step 6: Deploy
# Apply the complete manifest
kubectl apply -f keycloak.yaml
# Watch the rollout
kubectl rollout status statefulset/keycloak -n keycloak --timeout=10m
# Verify all pods are running
kubectl get pods -n keycloak
# Access the admin console
# https://<node-ip>:30443/admin
Stop here and confirm the PVC bound before moving on. If it did not, everything downstream will look like a Keycloak problem when it is a storage one:
kubectl -n keycloak get pvc postgres-pvc
Also confirm the two replicas formed a single Infinispan cluster, since split members break login flows in ways that are hard to attribute later:
kubectl -n keycloak logs -l app=keycloak --prefix | grep "Received new cluster view"
Tip: For Minikube, use
minikube ipto get the node IP. For a multi-node cluster, any node's IP will work with NodePort.
📸 IMAGE: Terminal output of kubectl get pods -n keycloak showing all pods running
5. Keycloak-Morpheus SAML Integration Guide
With Keycloak deployed and running, we can now configure the SAML integration. The configuration involves both the Keycloak side (IdP) and the Morpheus side (SP), and the order matters.
Important — Configuration Order:
There is a chicken-and-egg situation here. Keycloak needs the SP Entity ID and ACS URL to create the SAML client, but these values are auto-generated by Morpheus only after you save an Identity Source. The correct order is:
- Create the Keycloak Realm and LDAP Federation first (Steps 1-2)
- Create a preliminary Identity Source in Morpheus to obtain the SP Entity ID and ACS URL (Step 3)
- Use those values to create the SAML Client in Keycloak (Steps 4-7)
- Come back to Morpheus and complete the Identity Source configuration (Step 8)
5.1 Step 1: Create the Keycloak Realm
- Log in to Keycloak Admin Console at
https://keycloak-server:30443/admin - Click the realm dropdown (top-left) and select "Create Realm"
- Set Realm Name to:
morpheus-lab - Click Create
📸 IMAGE: Keycloak Admin Console > Manage Realm > Create Realm
5.2 Step 2: Configure LDAP User Federation
Navigate to morpheus-lab > User Federation > Add New provider > LDAP and configure:
| Setting | Value |
|---|---|
| Name | LAB AD |
| Vendor | Active Directory |
| Connection URL | ldap://active-directory:389 |
| Bind Type | simple |
| Bind DN | CN=svc-keycloak,CN=Users,DC=domain,DC=domain |
| Users DN | CN=Users,DC=domain,DC=domain |
| Username LDAP Attribute | sAMAccountName |
| Edit Mode | READ_ONLY |
| Import Users | ON |
Add Group Mapper (Mappers > Add mapper):
| Setting | Value |
|---|---|
| Mapper Type | group-ldap-mapper |
| Groups DN | CN=Users,DC=domain,DC=domain |
| Group Name LDAP Attribute | cn |
| Membership LDAP Attribute | member |
| Mode | READ_ONLY |
| Preserve Group Inheritance | OFF |
| User Roles Retrieve Strategy | LOAD_GROUPS_BY_MEMBER_ATTRIBUTE |
Preserve Group Inheritance deserves a paragraph, because leaving it on is the single most likely reason this integration fails after everything else is correct.
With inheritance enabled, Keycloak resolves the full parent/child tree for every group under Groups DN, and every DN listed as a member has to resolve to a group. CN=Users on a default Active Directory holds users and built-in groups side by side, and several of those built-in groups list user objects as members. Administrator inside Schema Admins is the usual one. The tree cannot be resolved, the whole group sync aborts, and the failure surfaces at SAML assertion time rather than at sync time.
Turning inheritance off flattens the hierarchy, which is all Morpheus needs: it reads groups as a flat list, and the SAML Group List mapper in Step 6 is configured with Full Group Path off anyway. If you do want the hierarchy, scope Groups DN to a dedicated OU that contains only your role groups, or add an LDAP Filter to the mapper:
(|(cn=mspusers)(cn=apparchitech)(cn=selfservice))
Note: After saving, click 'Sync all users' and 'Sync LDAP groups to Keycloak' to import users and groups from Active Directory.
📸 IMAGE: Keycloak > User Federation > LAB AD settings
📸 IMAGE: Keycloak > User Federation > LAB AD > Mappers > group mapper, showing Preserve Group Inheritance set to OFF
5.3 Step 3: Create Preliminary Identity Source in Morpheus (Get SP Entity ID)
Before creating the SAML client in Keycloak, we need to obtain the SP Entity ID and ACS URL from Morpheus. These values are auto-generated and unique to your Morpheus instance.
- Log in to Morpheus at
https://morpheus-server - Navigate to Administration > Identity Sources
- Click + Add Identity Source
- Set Type to
SAML SSO - Set Name to
Keycloak-SSO - For now, enter any placeholder URL in Login Redirect URL (e.g.,
https://placeholder.local) — we will update this later - Click Save Changes
After saving, Morpheus generates and displays two critical values in the Identity Source list:
-
SP Entity ID — e.g.,
https://morpheus-server/saml/N3h***B***O -
SP ACS URL — e.g.,
https://morpheus-server/externalLogin/callback/N***3***O
Copy both of these values! You will need them in the next step to configure the SAML client in Keycloak. The
N3****BOpart is a unique identifier generated by your Morpheus instance — yours will be different.
📸 IMAGE: Morpheus > Identity Sources list showing the auto-generated SP Entity ID and ACS URL
5.4 Step 4: Create the SAML Client in Keycloak
Now go back to the Keycloak Admin Console and create the SAML client using the values from the previous step:
- Navigate to morpheus-lab > Clients > Create Client
- Set Client Type to SAML
- Set Client ID to the SP Entity ID you copied from Morpheus:
https://morpheus-server/saml/N3h**K**5** - Set Name to:
Morpheus Enterprise
Access Settings:
| Setting | Value |
|---|---|
| Root URL | https://morpheus-server |
| Valid Redirect URIs (ACS URL) | https://morpheus-server/externalLogin/callback/N3h**K**5** |
| Master SAML Processing URL | https://morpheus-server/externalLogin/callback/N3h**K**5** |
| IDP-Initiated SSO URL Name | morpheus |
SAML Capabilities:
| Setting | Value |
|---|---|
| Name ID Format | username |
| Force POST Binding | ON |
| Include AuthnStatement | ON |
Signature & Encryption:
| Setting | Value |
|---|---|
| Sign Documents | ON |
| Sign Assertions | ON |
| Signature Algorithm | RSA_SHA256 |
| Client Signature Required | OFF (CRITICAL!) |
| Encrypt Assertions | OFF |
Warning:
Client Signature Requiredmust be OFF. Morpheus signs requests with a self-signed certificate that does not match the certificate registered in Keycloak. If this is ON, every SAML request from Morpheus will be rejected.
Keep Encrypt Assertions off as well. Turning it on makes Keycloak look for a client encryption certificate that Morpheus does not supply, and assertion building fails.
📸 IMAGE: Keycloak > Clients > Morpheus Enterprise > Settings
5.5 Step 5: Configure Logout (Advanced Settings)
Navigate to Advanced tab > Fine Grain SAML Endpoint Configuration:
| Setting | Value |
|---|---|
| Logout Service POST Binding URL | https://morpheus-server/login/auth |
| Logout Service Redirect Binding URL | https://morpheus-server/login/auth |
This is crucial! Setting these URLs to
/login/authprevents theGroovyCastExceptionbug. The Morpheus ACS callback handler cannot process SAML LogoutResponse objects.
Fill in both bindings. When these fields are empty, Keycloak falls back to the Master SAML Processing URL, which is the ACS callback, and the LogoutResponse lands on a handler that only understands AuthnResponse. Which binding gets used depends on how Morpheus sends the LogoutRequest, so setting only the POST field leaves the other path broken.
📸 IMAGE: Keycloak > Clients > Morpheus Enterprise > Advanced > Fine Grain SAML Endpoint Configuration, with both logout binding URLs filled in
5.6 Step 6: Configure SAML Mappers
Navigate to Client Scopes > dedicated > Mappers and add:
Mapper 1: Groups (Group List)
| Setting | Value |
|---|---|
| Mapper Type | Group list |
| SAML Attribute Name | groups |
| SAML Attribute NameFormat | Basic |
| Single Group Attribute | OFF |
| Full Group Path | OFF |
Mapper 2: firstName (User Attribute)
| Setting | Value |
|---|---|
| Mapper Type | User Attribute |
| User Attribute | firstName |
| SAML Attribute Name | firstName |
Mapper 3: lastName (User Attribute)
| Setting | Value |
|---|---|
| Mapper Type | User Attribute |
| User Attribute | lastName |
| SAML Attribute Name | lastName |
📸 IMAGE: Keycloak > Client Scopes > dedicated > Mappers list
5.7 Step 7: Copy the Realm Certificate
- Navigate to morpheus-lab > Realm Settings > Keys
- Find the RS256 key row and click the Certificate button
- Copy the entire certificate string
📸 IMAGE: Keycloak > Realm Settings > Keys > RS256 certificate
5.8 Step 8: Complete the Morpheus Identity Source Configuration
Now go back to the preliminary Identity Source you created in Step 3 and update it with the real values. Navigate to Administration > Identity Sources > Keycloak-SSO > Edit:
| Setting | Value |
|---|---|
| Login Redirect URL | https://keycloak-server:30443/realms/morpheus-lab/protocol/saml |
| SAML Logout Redirect URL | https://keycloak-server:30443/realms/morpheus-lab/protocol/saml |
| Includes SAML Request Parameter | Yes |
| POST Binding Mode | ON |
| SAML Request | Self Signed |
| SAML Response | Validate Assertion Signature |
| SAML Response Public Key | (paste RS256 certificate from Keycloak) |
Assertion Attribute Mappings:
| Morpheus Field | Value |
|---|---|
| Given Name Attribute Name | firstName |
| Surname Attribute Name | lastName |
Role Mappings:
| Morpheus Field | Value |
|---|---|
| Default Role | Standard User |
| Role Attribute Name | groups |
| Required Role Attribute Value | mspusers |
| Application Architect Role | apparchitech |
| Self Service User Role | selfservice |
Click Save Changes.
📸 IMAGE: Morpheus > Identity Sources > Keycloak-SSO configuration
5.9 Step 9: Test the Integration
- Open a new browser / incognito window
- Navigate to
https://morpheus-server - Click the SSO login option (Keycloak-SSO should appear)
- You will be redirected to Keycloak's login page
- Enter an AD username (e.g.,
firstname.lastname) and password - On success, you will be redirected back to Morpheus and logged in
Test logout too, not just login. A broken logout binding does not show up anywhere in the login path.
5.10 Step 10: Enable MFA (Optional)
MFA is entirely an IdP concern here. Morpheus receives a signed assertion and has no interest in how many factors produced it, so nothing on the Morpheus side changes. Keycloak's built-in option is OTP (TOTP/HOTP), which works with Google Authenticator, Microsoft Authenticator or FreeOTP. WebAuthn is also available, though it is awkward on a NodePort endpoint with a self-signed certificate, so start with OTP.
The obvious route — setting Configure OTP as a default required action under Authentication > Required Actions — does not work with LDAP federation. Default actions attach to users created through Keycloak, and federated users do not go through that path, so nobody is ever prompted to enrol. The default browser flow compounds this: its Browser - Conditional OTP subflow is gated on Condition - user configured, meaning OTP is requested only from users who already have it. Nobody has it, so nobody is asked.
Bind a flow that requires it instead:
- Go to morpheus-lab > Authentication > Flows > browser and click Duplicate. Name it
browser-mfa. - In the copy, change the
Browser - Conditional OTPsubflow requirement from Conditional to Required. - Delete the
Condition - user configuredexecution inside that subflow. - Confirm
OTP Formis set to Required. - Back on the flow list, open Action > Bind flow on
browser-mfaand bind it as Browser flow.
📸 IMAGE: Keycloak > Authentication > Flows > browser-mfa, showing the Conditional OTP subflow set to Required and bound as the Browser flow
Users without an enrolled authenticator now get the QR enrolment screen on their next login, including users who have never logged in before. Because the SAML flow uses the bound browser flow, test through the Morpheus SSO button rather than the Keycloak console directly.
📸 IMAGE: Keycloak OTP enrolment screen with the QR code, as the user sees it after entering AD credentials
One consequence worth planning for: OTP secrets live in the Keycloak database, not in Active Directory. Losing the PostgreSQL volume means every user re-enrols.
5.11 Step 11: Align Session Timeouts
Morpheus has its own inactivity timer under Administration > Settings > Appliance, with Session Expires and Session Warning in minutes. Ours is set to 20 and 15.
📸 IMAGE: Morpheus > Administration > Settings > Appliance, showing Session Expires and Session Warning
Keycloak's SSO session is independent, and if it outlives the Morpheus one, a user whose Morpheus session expired gets logged straight back in without a password prompt when they click the SSO button.
Set Keycloak's idle timeout slightly below the Morpheus value under morpheus-lab > Realm Settings > Sessions:
| Setting | Value |
|---|---|
| SSO Session Idle | 18 minutes |
| SSO Session Max | 8 hours |
| SSO Session Idle Remember Me | 0 |
| SSO Session Max Remember Me | 0 |
📸 IMAGE: Keycloak > Realm Settings > Sessions, showing SSO Session Idle and SSO Session Max
Then turn Remember me off under morpheus-lab > Realm Settings > Login. While it is on, the Remember Me lifespans replace the values above and the idle timeout you just set does nothing.
The same change from the CLI, with lifespans in seconds:
kubectl -n keycloak exec -it keycloak-0 -- /opt/keycloak/bin/kcadm.sh config credentials \
--server http://localhost:8080 --realm master --user admin
kubectl -n keycloak exec -it keycloak-0 -- /opt/keycloak/bin/kcadm.sh update realms/morpheus-lab \
-s ssoSessionIdleTimeout=1080 \
-s ssoSessionMaxLifespan=28800 \
-s rememberMe=false
Existing sessions keep their original lifespans, so clear them before testing: morpheus-lab > Sessions > Action > Sign out all active sessions. Test in a private window, since browsers that restore tabs on startup also restore session cookies and will make the old behaviour look like it never changed.
SSO Session Max is an absolute ceiling that forces re-authentication even during active use. Eight hours covers a working day; shortening it mostly annoys people mid-afternoon.
6. Troubleshooting SAML Integration
SAML integrations can fail silently or produce cryptic errors. This section covers the most common issues.
6.1 Diagnostic Tools
| Tool | Usage |
|---|---|
| SAML Tracer (Browser Extension) | Captures SAML requests/responses in real-time. Available for Firefox and Chrome |
| Keycloak Events | Enable in Realm Settings > Events. Shows auth attempts and errors |
| Keycloak Logs | kubectl logs -l app=keycloak -n keycloak --prefix -f |
| Morpheus Logs | /var/log/morpheus/morpheus-ui/current |
| Base64 Decoder | `echo '' \ |
With two replicas, always read logs with the label selector and {% raw %}--prefix. Targeting a single pod by name will hide half the failures, since the browser can land on either replica.
6.2 Common Issues and Solutions
Issue 1: 'Invalid Requester' Error
| Symptom | Keycloak returns 'Invalid requester' or 'Client not found' |
| Cause | SP Entity ID in Morpheus doesn't match Client ID in Keycloak |
| Solution | Copy the exact SP Entity ID from Morpheus Identity Source and use it as Client ID in Keycloak |
Issue 2: Signature Validation Failure
| Symptom | 'Signature validation failed' or 'Invalid signature' |
| Cause | SAML Response Public Key in Morpheus doesn't match Keycloak's RS256 certificate |
| Solution | Copy fresh certificate from Keycloak > Realm Settings > Keys > RS256 > Certificate. Must be updated after every Keycloak redeployment |
Issue 3: User Authenticated but Access Denied
| Symptom | User authenticates at Keycloak but gets 'Access Denied' in Morpheus |
| Cause | User not in the required group, or Group List mapper missing |
| Solution | 1) Verify user is in mspusers AD group. 2) Check Group List mapper in client scopes. 3) Use SAML Tracer to verify groups attribute in assertion |
Issue 4: GroovyCastException on Logout
| Symptom | Login fails after logout with GroovyCastException: Cannot cast object ... LogoutResponseImpl ... to class ... Response
|
| Cause | LogoutResponse sent to ACS URL instead of login page, because the logout binding fields are empty and Keycloak falls back to Master SAML Processing URL |
| Solution | Set both Logout Service POST Binding URL and Logout Service Redirect Binding URL to https://morpheus-server/login/auth in Keycloak Advanced settings |
Issue 5: Login Loop / Redirect Loop
| Symptom | Browser keeps redirecting between Morpheus and Keycloak |
| Cause | Mixed HTTP/HTTPS, clock skew, or invalid ACS URL |
| Solution | 1) Ensure both use HTTPS. 2) Verify NTP sync (SAML assertions are time-sensitive). 3) Check Valid Redirect URIs matches exactly |
Issue 6: Attributes Not Mapped
| Symptom | firstName/lastName are empty, roles not assigned |
| Cause | SAML mappers missing or attribute names don't match |
| Solution | 1) Add User Attribute mappers for firstName and lastName. 2) Names are case-sensitive. 3) Use SAML Tracer to verify attributes in assertion XML |
Issue 7: LDAP Users Not Appearing
| Symptom | No users appear after LDAP federation setup |
| Cause | Wrong Bind DN, Users DN, or network connectivity |
| Solution | 1) Test connectivity from pod. 2) Verify Bind DN has read access. 3) Click 'Sync all users'. 4) Check Keycloak logs for LDAP errors |
Issue 8: 'Failed to process response' After Successful Login
| Symptom | Credentials are accepted, then Keycloak's own error page appears with 'We are sorry... Failed to process response'. The browser never returns to Morpheus |
| Cause | Assertion building failed. The log shows ModelException: Couldn't resolve groups from LDAP with a GroupTreeResolveException naming a user object referenced as a member of a built-in AD group |
| Solution | Set Preserve Group Inheritance to OFF on the LDAP group mapper and re-run 'Sync LDAP groups to Keycloak'. Alternatively scope Groups DN to an OU holding only your role groups. Restart the pods afterwards to clear the cached user: kubectl -n keycloak rollout restart sts/keycloak
|
📸 IMAGE: Keycloak error page reading 'We are sorry... Failed to process response'
Read the stack trace before assuming this one. The same error page appears for any failure during assertion building, including an enabled Encrypt Assertions with no client certificate, so the exception in the log is what tells them apart.
Issue 9: Keycloak Pod Stuck in Init
| Symptom |
keycloak-0 sits at Init:0/1, the init container logs repeat 'Waiting for PostgreSQL', and the postgres pod is Pending
|
| Cause | The PVC is unbound: kubectl -n keycloak describe pvc postgres-pvc reports 'no persistent volumes available for this claim and no storage class is set' |
| Solution | Set storageClassName in the PVC to a class from kubectl get sc, or mark one class as the cluster default. The field is immutable, so delete and recreate the PVC |
Issue 10: MFA Enabled but No QR Code
| Symptom |
Configure OTP is set as a default required action, but users log in without ever being prompted to enrol |
| Cause | Default required actions do not attach to LDAP-federated users, and the stock browser flow only asks for OTP from users who already configured it |
| Solution | Duplicate the browser flow, set the Conditional OTP subflow to Required, remove the Condition - user configured execution, and bind the new flow as the Browser flow (Step 10) |
6.3 SAML Assertion Debugging Checklist
When debugging, use SAML Tracer to capture the assertion and verify:
-
NameID: Present and in expected format (
username)? - Issuer: Matches the Keycloak realm URL?
- AudienceRestriction: Audience matches Morpheus SP Entity ID?
- Conditions/NotBefore/NotOnOrAfter: Valid timestamps? Check for clock skew
- AuthnStatement: Present? (Required by Morpheus)
-
AttributeStatement:
firstName,lastName,groupsattributes present with correct values? - Signature: Assertion signed? Certificate matches?
6.4 Useful Debug Commands
# Check Keycloak pod logs across both replicas
kubectl logs -l app=keycloak -n keycloak --prefix -f
# Find the exception behind a Keycloak error page
kubectl logs -l app=keycloak -n keycloak --prefix --tail=400 | grep -B5 -A40 "ERROR"
# Verify storage bound before blaming Keycloak
kubectl -n keycloak get pvc postgres-pvc
# Confirm the two replicas formed one Infinispan cluster
kubectl -n keycloak logs -l app=keycloak --prefix | grep "Received new cluster view"
# Decode a SAML Response from browser
echo '<base64-saml-response>' | base64 -d | xmllint --format -
# Test LDAP connectivity from inside the cluster
kubectl exec -it keycloak-0 -n keycloak -- \
sh -c 'nc -zv domain-controller 389'
# Check Keycloak health
kubectl exec -it keycloak-0 -n keycloak -- \
curl -sk https://localhost:8443/health/ready
7. Conclusion
Integrating Morpheus Enterprise with Keycloak via SAML provides a robust, centralized authentication solution for enterprise cloud management. Key takeaways:
- Morpheus acts as a SAML SP; Keycloak acts as the SAML IdP with AD backend
- The Kubernetes deployment uses a StatefulSet with Infinispan clustering for HA
- Self-signed TLS certificates are generated by init containers — no external cert-manager required for lab environments
- Name a
storageClassNamein the PVC unless your cluster has a default class, or the whole stack stalls on unbound storage - Set Preserve Group Inheritance to OFF when Groups DN points at a container that mixes users and built-in groups
-
Client Signature Requiredmust be OFF in Keycloak for Morpheus compatibility - Both Logout Service binding URLs must point to
/login/authto avoid theGroovyCastExceptionbug - For MFA with federated users, bind a browser flow with OTP set to Required rather than relying on default required actions
- Keep Keycloak's SSO Session Idle at or below the Morpheus session timeout, with Remember Me off
- Always update the SAML Response Public Key in Morpheus after redeploying Keycloak
Have questions or ran into a different issue? Drop a comment below!
Written by Emre Baykal — March 2026 - Update September 2026





















Top comments (0)