Skip to content

Commit e353717

Browse files
committed
ACM: Keycloak install on OCP and create of hub realm
Signed-off-by: Matthias Wessendorf <[email protected]>
1 parent f675703 commit e353717

File tree

17 files changed

+1956
-0
lines changed

17 files changed

+1956
-0
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,4 @@ local-env-teardown: ## Tear down the local Kind cluster
153153

154154
# Include build configuration files
155155
-include build/*.mk
156+
-include build/openshift/*.mk

build/openshift/keycloak-acm.mk

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Keycloak ACM Integration for OpenShift
2+
#
3+
# This file contains targets for setting up Keycloak with V1 token exchange
4+
# for ACM multi-cluster environments on OpenShift.
5+
#
6+
# Prerequisites:
7+
# - OpenShift 4.19+ or 4.20+ cluster
8+
# - ACM installed
9+
# - Cluster-admin access
10+
#
11+
# Initial Setup (Hub Only):
12+
# make keycloak-acm-setup-hub # Deploy Keycloak and configure hub realm
13+
# make keycloak-acm-generate-toml # Generate MCP server configuration
14+
#
15+
# Environment Variables:
16+
# HUB_KUBECONFIG - Path to hub cluster kubeconfig (default: $KUBECONFIG)
17+
# KEYCLOAK_URL - Keycloak URL (auto-detected from route if not set)
18+
# ADMIN_USER - Keycloak admin username (default: admin)
19+
# ADMIN_PASSWORD - Keycloak admin password (default: admin)
20+
21+
##@ Keycloak ACM Integration
22+
23+
.PHONY: keycloak-acm-setup-hub
24+
keycloak-acm-setup-hub: ## Deploy Keycloak on OpenShift with V1 token exchange for ACM hub
25+
@echo "==========================================="
26+
@echo "Keycloak ACM Hub Setup"
27+
@echo "==========================================="
28+
@echo ""
29+
@echo "This will:"
30+
@echo " 1. Enable TechPreviewNoUpgrade feature gate (if needed)"
31+
@echo " 2. Deploy Keycloak with V1 token exchange features"
32+
@echo " 3. Create hub realm with mcp user and clients"
33+
@echo " 4. Configure same-realm token exchange"
34+
@echo " 5. Fix CA trust for cross-realm token exchange"
35+
@echo " 6. Create RBAC for mcp user"
36+
@echo " 7. Save configuration to .keycloak-config/"
37+
@echo ""
38+
@bash ./hack/keycloak-acm/setup-hub.sh
39+
@echo ""
40+
@echo "✅ Hub Keycloak setup complete!"
41+
@echo ""
42+
@echo "Configuration saved to: .keycloak-config/hub-config.env"
43+
@echo ""
44+
@echo "Next steps:"
45+
@echo " 1. Run: make keycloak-acm-generate-toml"
46+
@echo " 2. Start MCP server with: ./kubernetes-mcp-server --config acm-kubeconfig.toml"
47+
48+
.PHONY: keycloak-acm-generate-toml
49+
keycloak-acm-generate-toml: ## Generate acm-kubeconfig.toml from saved Keycloak configuration
50+
@echo "==========================================="
51+
@echo "Generating MCP Server Configuration"
52+
@echo "==========================================="
53+
@echo ""
54+
@bash ./hack/keycloak-acm/generate-toml.sh
55+
@echo ""
56+
@echo "Next: Start MCP server with: ./kubernetes-mcp-server --port 8080 --config acm-kubeconfig.toml"
57+
58+
.PHONY: keycloak-acm-status
59+
keycloak-acm-status: ## Show Keycloak ACM configuration status
60+
@echo "==========================================="
61+
@echo "Keycloak ACM Configuration Status"
62+
@echo "==========================================="
63+
@echo ""
64+
@if [ -f .keycloak-config/hub-config.env ]; then \
65+
echo "✅ Hub configuration found:"; \
66+
echo ""; \
67+
source .keycloak-config/hub-config.env && \
68+
echo " Keycloak URL: $$KEYCLOAK_URL"; \
69+
echo " Hub Realm: $$HUB_REALM"; \
70+
echo " MCP User: $$MCP_USERNAME"; \
71+
echo ""; \
72+
kubectl get pods -n keycloak -l app=keycloak 2>/dev/null && echo "" || echo " ⚠️ Keycloak pod not found"; \
73+
kubectl get route keycloak -n keycloak -o jsonpath='{.spec.host}' 2>/dev/null && echo "" || echo " ⚠️ Keycloak route not found"; \
74+
else \
75+
echo "❌ Hub configuration not found"; \
76+
echo " Run: make keycloak-acm-setup-hub"; \
77+
fi
78+
@echo ""
79+
@if [ -f acm-kubeconfig.toml ]; then \
80+
echo "✅ MCP configuration found: acm-kubeconfig.toml"; \
81+
echo ""; \
82+
echo "Configured clusters:"; \
83+
grep '^\[cluster_provider_configs.acm-kubeconfig.clusters' acm-kubeconfig.toml | \
84+
sed 's/\[cluster_provider_configs.acm-kubeconfig.clusters."\(.*\)"\]/ - \1/'; \
85+
else \
86+
echo "❌ MCP configuration not found"; \
87+
echo " Run: make keycloak-acm-generate-toml"; \
88+
fi
89+
@echo ""
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# ACM Keycloak Declarative Configuration
2+
3+
This directory contains declarative JSON configuration files for setting up Keycloak for ACM (Advanced Cluster Management) multi-realm token exchange.
4+
5+
## Architecture
6+
7+
- **Hub Realm**: Central realm where users authenticate
8+
- **Managed Cluster Realms**: One realm per managed cluster
9+
- **Token Exchange**: V1 token exchange using `subject_issuer` parameter
10+
- Same-realm: `mcp-sts``mcp-server` within hub realm
11+
- Cross-realm: Hub realm token → Managed cluster realm token
12+
13+
## Directory Structure
14+
15+
```
16+
dev/acm/config/keycloak/
17+
├── realm/
18+
│ ├── hub-realm-create.json # Hub realm configuration
19+
│ └── managed-realm-create.json # Template for managed cluster realms
20+
├── clients/
21+
│ ├── mcp-server.json # OAuth client (confidential)
22+
│ ├── mcp-client.json # Browser OAuth client (public)
23+
│ └── mcp-sts.json # STS client for token exchange
24+
├── client-scopes/
25+
│ ├── openid.json # OpenID Connect scope
26+
│ └── mcp-server.json # MCP audience scope
27+
├── mappers/
28+
│ ├── mcp-server-audience-mapper.json # Adds mcp-server to aud claim
29+
│ └── sub-claim-mapper.json # Maps user ID to sub claim
30+
├── users/
31+
│ └── mcp.json # Test user (mcp/mcp)
32+
└── identity-providers/
33+
└── hub-realm-idp-template.json # IDP config for cross-realm trust
34+
```
35+
36+
## Configuration Files
37+
38+
### Hub Realm (`realm/hub-realm-create.json`)
39+
40+
- Realm name: `hub`
41+
- User registration: disabled
42+
- Password reset: enabled
43+
- Brute force protection: enabled
44+
- Token lifespans configured for security
45+
46+
### Clients
47+
48+
#### `mcp-server` (Confidential Client)
49+
- Used by MCP server for OAuth authentication
50+
- Direct access grants enabled (password flow)
51+
- Service accounts enabled
52+
- Default scopes: `openid`, `profile`, `email`, `mcp-server`
53+
54+
#### `mcp-client` (Public Client)
55+
- Used by browser-based tools (e.g., MCP Inspector)
56+
- PKCE enabled for security
57+
- Authorization code flow only
58+
- No service accounts
59+
60+
#### `mcp-sts` (STS Client)
61+
- Used for token exchange operations
62+
- Service accounts only (no user login)
63+
- No redirect URIs (not for browser flows)
64+
65+
### Client Scopes
66+
67+
#### `openid`
68+
- Standard OpenID Connect scope
69+
- Provides basic user claims (sub, iss, aud, exp, iat)
70+
71+
#### `mcp-server`
72+
- Custom audience scope
73+
- Adds `mcp-server` to the `aud` claim in access tokens
74+
- Required for token validation
75+
76+
### Protocol Mappers
77+
78+
#### `mcp-server-audience`
79+
- Type: `oidc-audience-mapper`
80+
- Adds `mcp-server` to the audience claim
81+
- Applied to `mcp-server` client scope
82+
83+
#### `sub`
84+
- Type: `oidc-sub-mapper`
85+
- Maps user ID to `sub` claim
86+
- Used for federated identity linking
87+
88+
### Users
89+
90+
#### `mcp` User
91+
- Username: `mcp`
92+
- Password: `mcp`
93+
94+
- Full name: MCP User
95+
- Used for testing and development
96+
97+
### Identity Provider
98+
99+
#### Hub Realm IDP Template
100+
- Provider: `oidc` (generic OIDC, not keycloak-oidc)
101+
- Trust email: enabled
102+
- Store token: disabled
103+
- Sync mode: IMPORT (create local users)
104+
- Signature validation: enabled via JWKS URL
105+
106+
## Variable Substitution
107+
108+
JSON templates use `${VARIABLE_NAME}` placeholders that are replaced at runtime:
109+
110+
- `${KEYCLOAK_URL}`: Base Keycloak URL (e.g., `https://keycloak-keycloak.apps.example.com`)
111+
- `${HUB_CLIENT_SECRET}`: Secret for mcp-server client in hub realm
112+
- `${MANAGED_REALM}`: Name of managed cluster realm (e.g., `managed-cluster-one`)
113+
114+
## Usage
115+
116+
These JSON files are applied via the Keycloak Admin REST API using the setup scripts:
117+
118+
1. **Hub Setup**: `hack/acm/acm-keycloak-setup-hub-declarative.sh`
119+
- Creates hub realm
120+
- Creates clients (mcp-server, mcp-client, mcp-sts)
121+
- Creates client scopes (openid, mcp-server)
122+
- Adds protocol mappers
123+
- Creates test user
124+
- Configures same-realm token exchange permissions
125+
126+
2. **Managed Cluster Registration**: `hack/acm/acm-register-managed-cluster-declarative.sh`
127+
- Creates managed cluster realm
128+
- Registers identity provider (hub realm)
129+
- Creates federated user link
130+
- Configures cross-realm token exchange permissions
131+
132+
## Token Exchange Configuration
133+
134+
### Same-Realm Token Exchange (Hub)
135+
136+
Allows `mcp-sts` client to exchange tokens for `mcp-server` audience within the hub realm.
137+
138+
**Steps** (applied by setup script):
139+
1. Enable management permissions on `mcp-server` client
140+
2. Get token-exchange permission ID
141+
3. Create client policy allowing `mcp-sts`
142+
4. Link policy to token-exchange permission
143+
144+
**Test Command**:
145+
```bash
146+
source .keycloak-config/hub-config.env
147+
./hack/acm/test-same-realm-token-exchange.sh
148+
```
149+
150+
### Cross-Realm Token Exchange (Hub → Managed)
151+
152+
Allows exchanging hub realm token for managed cluster realm token.
153+
154+
**Steps** (applied by setup script):
155+
1. Create identity provider in managed realm pointing to hub realm
156+
2. Create federated identity link (hub user → managed user via `sub` claim)
157+
3. Enable fine-grained permissions on IDP
158+
4. Create client policy allowing hub realm's `mcp-sts`
159+
5. Link policy to token-exchange permission on IDP
160+
161+
**Test Command**:
162+
```bash
163+
source .keycloak-config/hub-config.env
164+
source .keycloak-config/clusters/managed-cluster-one.env
165+
./hack/acm/test-cross-realm-token-exchange.sh
166+
```
167+
168+
## Keycloak Admin API Endpoints
169+
170+
Configuration is applied using these endpoints:
171+
172+
- **Realm**: `POST /admin/realms`
173+
- **Clients**: `POST /admin/realms/{realm}/clients`
174+
- **Client Scopes**: `POST /admin/realms/{realm}/client-scopes`
175+
- **Protocol Mappers**: `POST /admin/realms/{realm}/client-scopes/{scope-id}/protocol-mappers/models`
176+
- **Users**: `POST /admin/realms/{realm}/users`
177+
- **Identity Providers**: `POST /admin/realms/{realm}/identity-provider/instances`
178+
- **Client Permissions**: `PUT /admin/realms/{realm}/clients/{client-id}/management/permissions`
179+
- **Authorization Policies**: `POST /admin/realms/{realm}/clients/{client-id}/authz/resource-server/policy/client`
180+
181+
## Benefits of Declarative Approach
182+
183+
1. **Version Control**: Configuration as code
184+
2. **Repeatability**: Same configuration every time
185+
3. **Testability**: Easy to test in different environments
186+
4. **Documentation**: Self-documenting via JSON structure
187+
5. **Validation**: JSON schema validation possible
188+
6. **Idempotency**: Can reapply without side effects
189+
7. **Debugging**: Easy to compare configurations
190+
191+
## References
192+
193+
- Keycloak Admin REST API: https://www.keycloak.org/docs-api/26.0/rest-api/index.html
194+
- Token Exchange: https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange
195+
- Identity Brokering: https://www.keycloak.org/docs/latest/server_admin/#_identity_broker
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"name": "mcp-server",
3+
"description": "MCP Server audience scope",
4+
"protocol": "openid-connect",
5+
"attributes": {
6+
"display.on.consent.screen": "false",
7+
"include.in.token.scope": "true"
8+
}
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "openid",
3+
"description": "OpenID Connect scope",
4+
"protocol": "openid-connect",
5+
"attributes": {
6+
"display.on.consent.screen": "true",
7+
"include.in.token.scope": "true",
8+
"consent.screen.text": "${openidScopeConsentText}"
9+
}
10+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"clientId": "mcp-client",
3+
"name": "MCP Client",
4+
"description": "Public OAuth client for browser-based authentication (inspector)",
5+
"enabled": true,
6+
"publicClient": true,
7+
"consentRequired": false,
8+
"standardFlowEnabled": true,
9+
"implicitFlowEnabled": false,
10+
"directAccessGrantsEnabled": false,
11+
"serviceAccountsEnabled": false,
12+
"authorizationServicesEnabled": false,
13+
"protocol": "openid-connect",
14+
"redirectUris": ["*"],
15+
"webOrigins": ["*"],
16+
"fullScopeAllowed": false,
17+
"defaultClientScopes": ["openid", "profile", "email", "mcp-server"],
18+
"optionalClientScopes": [],
19+
"attributes": {
20+
"pkce.code.challenge.method": "S256",
21+
"oauth2.device.authorization.grant.enabled": "false",
22+
"oidc.ciba.grant.enabled": "false",
23+
"backchannel.logout.session.required": "false",
24+
"backchannel.logout.revoke.offline.tokens": "false",
25+
"display.on.consent.screen": "false"
26+
}
27+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"clientId": "mcp-server",
3+
"name": "MCP Server",
4+
"description": "OAuth client for MCP server authentication",
5+
"enabled": true,
6+
"publicClient": false,
7+
"consentRequired": false,
8+
"standardFlowEnabled": true,
9+
"implicitFlowEnabled": false,
10+
"directAccessGrantsEnabled": true,
11+
"serviceAccountsEnabled": true,
12+
"authorizationServicesEnabled": false,
13+
"protocol": "openid-connect",
14+
"redirectUris": ["*"],
15+
"webOrigins": ["*"],
16+
"fullScopeAllowed": false,
17+
"defaultClientScopes": ["openid", "profile", "email", "mcp-server"],
18+
"optionalClientScopes": [],
19+
"attributes": {
20+
"oauth2.device.authorization.grant.enabled": "false",
21+
"oidc.ciba.grant.enabled": "false",
22+
"backchannel.logout.session.required": "true",
23+
"backchannel.logout.revoke.offline.tokens": "false",
24+
"client.secret.creation.time": "0",
25+
"display.on.consent.screen": "false",
26+
"saml.artifact.binding": "false",
27+
"saml.server.signature": "false",
28+
"saml.server.signature.keyinfo.ext": "false",
29+
"saml.assertion.signature": "false",
30+
"saml.client.signature": "false",
31+
"saml.encrypt": "false",
32+
"saml.authnstatement": "false",
33+
"saml.onetimeuse.condition": "false",
34+
"saml_force_name_id_format": "false",
35+
"saml.multivalued.roles": "false",
36+
"saml.force.post.binding": "false",
37+
"exclude.session.state.from.auth.response": "false",
38+
"tls.client.certificate.bound.access.tokens": "false",
39+
"access.token.lifespan": "300"
40+
}
41+
}

0 commit comments

Comments
 (0)