Keycloak IAM 第三方软件集成指南 | IDaaS Book
Keycloak 接入开源软件时,先判断目标软件是否原生支持 OIDC/SAML;只有不支持或需要统一保护多个 Web 应用时,才把 oauth2-proxy 放在入口。Grafana、GitLab、Vault、Harbor 等常见软件的配置字段并不相同,本节只给出协议边界和最小可验证配置,不把“能跳转到登录页”当成集成完成。
前置:先在 Keycloak 创建一个
confidentialClient,记录client_id、client_secret、redirect_uri。除非特别说明,统一用 OIDC + 授权码模式 + PKCE。
通用模式:OAuth2 Proxy
绝大多数本身不支持 OIDC 的 Web 服务,都可以用
oauth2-proxy 在前置反代中统一接入。它是 Keycloak 集成的瑞士军刀。
# oauth2-proxy.cfg
provider = "keycloak-oidc"
oidc_issuer_url = "https://kc.example.com/realms/myrealm"
client_id = "oauth2-proxy"
client_secret = "SECRET"
redirect_url = "https://app.example.com/oauth2/callback"
email_domains = "example.com"
cookie_secret = "32字节随机"
code_challenge_method = "S256"
set_xauthrequest = true
reverse_proxy = true
trusted_proxy_ips = ["10.42.0.0/16"] # 替换为实际 Ingress 出口网段
cookie_secure = true
cookie_samesite = "lax"
pass_access_token = false默认不把 Access Token 转发给后端:入口会话通过,只说明认证代理接受了该会话,不等于后端 API 的 aud、scope 和资源权限已经验证。确实需要后端代表用户调用其他服务时,才同时开启 pass_access_token 和 Ingress 的响应头转发;后端仍须独立校验签名、iss、面向自身的 aud、exp 与 scope。trusted_proxy_ip 必须填写 oauth2-proxy 实际看到的代理来源,不能直接照抄示例网段。
# Nginx 前置
location /oauth2/ { proxy_pass http://127.0.0.1:4180; }
location / {
auth_request /oauth2/auth;
error_page 401 = /oauth2/start;
proxy_pass http://upstream_app;
}下面各软件若原生支持 OIDC 则优先用原生,否则用此模式。
Grafana
Grafana 原生支持 OIDC,配置 grafana.ini:
[auth.generic_oauth]
enabled = true
name = Keycloak
client_id = grafana
client_secret = SECRET
scopes = openid email profile
auth_url = https://kc.example.com/realms/myrealm/protocol/openid-connect/auth
token_url = https://kc.example.com/realms/myrealm/protocol/openid-connect/token
api_url = https://kc.example.com/realms/myrealm/protocol/openid-connect/userinfo
login_attribute_path = preferred_username
groups_attribute_path = groups
role_attribute_path = contains(groups[*], 'grafana-admin') && 'Admin' || 'Viewer'
allow_sign_up = true要点:
- 在 Keycloak Client 的 Protocol Mappers 加
groupsmapper(把用户 Realm/Client 角色映射成groupsclaim),即可用角色驱动 Grafana 权限。 role_attribute_path把 Keycloak 角色映射为 GrafanaAdmin/Editor/Viewer。
GitLab
GitLab 通过 OmniAuth 支持 OIDC(gitlab.rb):
gitlab_rails['omniauth_providers'] = [
{
name: "openid_connect",
label: "Keycloak",
args: {
name: "openid_connect",
scope: ["openid", "profile", "email"],
response_type: "code",
issuer: "https://kc.example.com/realms/myrealm",
discovery: true,
client_auth_method: "basic",
uid_field: "preferred_username",
send_scope_to_token_endpoint: true,
pkce: true,
client_options: {
identifier: "gitlab",
secret: "SECRET",
redirect_uri: "https://gitlab.example.com/users/auth/openid_connect/callback",
gitlab: {
groups_attribute: "groups",
required_groups: ["/platform/gitlab-users"]
}
}
}
}
]
gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect']
gitlab_rails['omniauth_block_auto_created_users'] = false要点:
- Keycloak Client 的
redirect_uri精确填https://gitlab.example.com/users/auth/openid_connect/callback;GitLab 只与 HTTPS 的 Keycloak 通信。 - scope 里不要加
groups:Keycloak 没有同名 client scope,未知 scope 会在授权端点直接 400。组是否出现在 claim 里由 mapper 开关决定。 - 组相关键(
groups_attribute、required_groups、external_groups、admin_groups)必须写在client_options.gitlab里,写到args顶层不会被读取,而且没有任何日志或报错。 groupsclaim 的值必须与 IdP 返回的字符串逐字符相等:Keycloak 的Group Membershipmapper 默认full.path=true,输出是/platform/gitlab-users这种带前导斜杠的路径。- 组门禁属于 Premium/Ultimate 能力;Free/CE 上这些键不生效且无提示。OIDC 也不会把 IdP 组同步成 GitLab 组——组同步要走 Group SAML 或 SCIM。
完整配置边界、验证顺序、报错对照与回滚见 GitLab 接入 Keycloak OIDC:IAM 单点登录与 required_groups 静默失效。
Jenkins
Jenkins 接 Keycloak 用
OAuth2 / OIDC 插件(oic-auth),不是同名的
Keycloak 插件——后者的配置入口是把 keycloak.json 粘进 Jenkins,插件页挂着三条安全公告(CSRF、session fixation 影响 2.3.0 及更早,open redirect 影响 2.4.1 及更早),且 2023-05 之后停更到 2026-09 才恢复发布。oic-auth 从 discovery 文档取端点、claim 用 JMESPath 映射,可被 JCasC 声明式管理:
- 安装 OpenID Connect Authentication 插件(4.727 起要求 Jenkins ≥ 2.539)。
- Keycloak 侧建 client:Client authentication
ON,Valid redirect URIs 填${JENKINS_ROOT_URL}/securityRealm/finishLogin,并把${JENKINS_ROOT_URL}/OicLogout加进 Valid post logout redirect URIs。 - Manage Jenkins → Security → Security Realm = OpenID Connect,填 well-known 地址与 client secret;
groupsFieldName用realm_access.roles,或改用自建Group Membershipmapper 输出的用户组 claim。 - 授权仍在 Jenkins 侧:在 Matrix/Project-based 策略里新建与 claim 值逐字符一致的组并勾权限——插件只负责把组名挂到用户身上,不会自动建组或授权。
认证方式一改,Jenkins 原有的数据库 / LDAP 登录同时失效,切换前先配好
escapeHatch兜底。 CI 场景的「机器账号」用 Service Account + Client Credentials,不要给流水线人工账号;Jenkins 的远程调用用 API token,oic-auth 下密码登录不工作。
字段语义、groups claim 的两种来源(microprofile-jwt 里的 groups 是 realm 角色)、userinfo 与 ID token 的取值顺序、报错对照表与回滚步骤见
Jenkins 接入 Keycloak OIDC:IAM 单点登录与组权限映射排错。
Kubernetes / NGINX Ingress
Kubernetes 体系下,最常见的是在 NGINX Ingress Controller 前置 oauth2-proxy,按 Ingress 注解启用:
# oauth2-proxy Deployment + Service(kube-system)
apiVersion: apps/v1
kind: Deployment
metadata: { name: oauth2-proxy, namespace: kube-system }
spec:
replicas: 1
selector: { matchLabels: { app: oauth2-proxy } }
template:
metadata: { labels: { app: oauth2-proxy } }
spec:
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.5
args:
- --provider=keycloak-oidc
- --oidc-issuer-url=https://kc.example.com/realms/myrealm
- --client-id=oauth2-proxy
- --client-secret=SECRET
- --cookie-secret=xxxxxxxxxxxxxxxx
- --email-domain=*
- --set-authorization-header
ports: [{ containerPort: 4180 }]# 业务 Ingress,启用认证
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://$host/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://$host/oauth2/start?rd=$escaped_request_uri"
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: app, port: { number: 80 } } }这是「网关层 SSO」模式:业务代码无需感知身份,鉴权在入口完成。详见 第18章 · 集成模式。
Keycloak + oauth2-proxy 生产排错清单
oauth2-proxy 与 Keycloak 对接时,高频问题往往不是「OIDC 不通」,而是 issuer、audience、回调地址或反向代理头不一致。上线前建议按下表逐项核对:
| 症状 / 报错 | 常见根因 | 修正方式 |
|---|---|---|
expected audience / invalid aud,日志里只有 account | oauth2-proxy 校验的 ID Token aud 没有包含其 client_id | 在 Keycloak Client 增加 Audience mapper,至少勾选 Add to ID token,把 Included Client Audience 设为 oauth2-proxy;只有后端确实验证 Access Token 时才额外勾选 Add to access token。也可在 oauth2-proxy 显式配置 --oidc-extra-audience,但不要用它掩盖错误的 Token 受众。 |
登录后反复跳转 / csrf cookie not found | redirect_url、Ingress auth-signin、Cookie Domain / SameSite 与实际访问域名不一致 | redirect_url 固定为外部入口 https://app.example.com/oauth2/callback;Ingress 使用 $host 与 $escaped_request_uri;跨子域共享时再设置 --cookie-domain=.example.com。反向代理还必须覆盖而不是盲目追加 X-Forwarded-*,并限制 Keycloak 只接受可信代理;完整排查见
Keycloak 重定向循环与 401 排错指南。 |
/oauth2/auth 返回 401,但用户已登录 | 业务 Ingress 没把认证响应头传给后端,或 oauth2-proxy 未开启 header 输出 | oauth2-proxy 开启 --set-xauthrequest=true;NGINX Ingress 用 auth-response-headers 透传 X-Auth-Request-User、X-Auth-Request-Email、X-Auth-Request-Groups。若后端确实需要 Bearer Token,另行配置 --pass-access-token=true,并确认入口复制的是 X-Auth-Request-Access-Token;不要把 --pass-authorization-header(代理直接转发给其 upstream)误当成 auth_request 响应头。 |
Keycloak 回调到 http:// 或错误 host | Keycloak / oauth2-proxy 后面有反向代理,但 X-Forwarded-* 头或 proxy 配置缺失 | 入口层保留 X-Forwarded-Proto、X-Forwarded-Host;Keycloak 侧按生产反向代理章节配置 hostname/proxy headers。 |
| Keycloak 17+ 后 issuer 不匹配 | 仍沿用旧 WildFly 路径 /auth/realms/<realm> | 新部署默认使用 https://kc.example.com/realms/<realm>;只有旧版本或保留兼容路径时才使用 /auth/realms/<realm>。 |
一个较稳的最小配置如下,重点是 issuer、audience、PKCE、cookie secret 与 header 输出都显式写清:
# 只有业务确实需要时,才额外启用 pass-access-token 并配置 auth-response-headers。
# 不要默认把 ID Token 放入 Authorization 头。
oauth2-proxy \
--provider=keycloak-oidc \
--oidc-issuer-url=https://kc.example.com/realms/myrealm \
--client-id=oauth2-proxy \
--client-secret=$OAUTH2_PROXY_CLIENT_SECRET \
--redirect-url=https://app.example.com/oauth2/callback \
--cookie-secret=$OAUTH2_PROXY_COOKIE_SECRET \
--email-domain='*' \
--code-challenge-method=S256 \
--set-xauthrequest=true验证顺序不要反:先访问 https://kc.example.com/realms/myrealm/.well-known/openid-configuration 确认 issuer;再登录一次并解码 access token,确认 aud 包含 oauth2-proxy;最后用浏览器开发者工具检查 /oauth2/callback 是否设置了同站点可用的 cookie。生产回滚最简单:移除业务 Ingress 的认证注解或 Traefik ForwardAuth middleware,保留 oauth2-proxy Deployment 以便排查,不要在事故中先删 Keycloak Client。需要完整的 Ingress / ForwardAuth 配置、验证命令和回滚步骤,可参考
Keycloak + oauth2-proxy 集成实战指南。
Vault
Vault 用 JWT/OIDC Auth Method,让 Keycloak 签发的 token 直接换取 Vault token,实现云原生密钥获取:
vault auth enable oidc
vault write auth/oidc/config \
oidc_discovery_url="https://kc.example.com/realms/myrealm" \
client_id="vault" \
client_secret="SECRET" \
default_role="engineer"
vault write auth/oidc/role/engineer \
bound_audiences="vault" \
allowed_redirect_uris="https://vault.example.com/ui/vault/auth/oidc/oidc/callback" \
user_claim="sub" \
groups_claim="groups" \
policies="default,engineer" \
ttl="1h"配合外部组映射(identity/group)即可按 Keycloak 角色决定 Vault 权限。
Harbor
Harbor 原生支持 OIDC(Configuration → Authentication → OIDC):
| 字段 | 值 |
|---|---|
| OIDC Provider Name | Keycloak |
| OIDC Endpoint | https://kc.example.com/realms/myrealm |
| OIDC Client ID | harbor |
| OIDC Client Secret | SECRET |
| OIDC Scope | openid,profile,email,offline_access |
| Group Claim Name | groups |
要点:groups 不是 Keycloak 的默认 client scope,写进 OIDC Scope 会被授权端点以 invalid_scope(Invalid scopes: groups)拒绝——组 claim 是否输出只取决于 mapper,不取决于这个 scope 名。offline_access 要留,Harbor 的 CLI secret 依赖 refresh token。用 groups claim 映射 Harbor 项目成员角色即可实现按组授权镜像仓库;Harbor 侧的字段语义(OIDC Admin Group 的逐字符比较、Group Filter 是非锚定正则、组记录在成员首次登录时才落库)与完整排错路径见
Harbor 接入 Keycloak OIDC:IAM 单点登录与组权限映射排错。
MinIO
MinIO 原生支持 OIDC,但它的授权模型和上面的软件不一样:不认 groups,也不认 realm_access.roles,只认一个 claim,把 claim 的值当成 MinIO 本机的策略名去比对。所以 Keycloak 侧要产出的不是「角色映射」,而是一个策略名 claim。
mc admin config set myminio identity_openid \
config_url="https://kc.example.com/realms/myrealm/.well-known/openid-configuration" \
client_id="minio" \
client_secret="SECRET" \
claim_name="policy" \
scopes="openid,profile,email"
mc admin service restart myminio三个边界值得先记下:claim 必须出现在 ID token 里(mapper 的 Add to ID token 打开即可,access token 里有没有它 MinIO 不关心);claim_prefix 已废弃但拼接逻辑仍在,旧教程里的 claim_prefix="customer1/" 会让 MinIO 去找 customer1/policy;如果所有 SSO 用户共用一套权限,改用 role_policy 更省事,Keycloak 侧不需要任何 mapper。claim 名与策略名的对应关系、role_policy 与 claim_name 的互斥边界、Invalid parameter: redirect_uri 与 policy claim missing 的完整定位路径见
MinIO 接入 Keycloak OIDC:IAM 策略映射与 SSO 排错。
Nextcloud
Nextcloud 用 Social login / OIDC 插件:
'oidc_login_provider' => [
'clientId' => 'nextcloud',
'clientSecret' => 'SECRET',
'oidcIssuer' => 'https://kc.example.com/realms/myrealm',
'authEndpoint' => 'https://kc.example.com/realms/myrealm/protocol/openid-connect/auth',
'tokenEndpoint'=> 'https://kc.example.com/realms/myrealm/protocol/openid-connect/token',
'userInfoEndpoint' => 'https://kc.example.com/realms/myrealm/protocol/openid-connect/userinfo',
],
'oidc_login_auto_redirect' => true,
'oidc_login_button_text' => 'Keycloak 登录',OpenSearch / OpenSearch Dashboards
OpenSearch 是唯一的「两个组件各自有一套配置」的接入对象:Dashboards 的 security 插件发起 OIDC 登录并代表用户请求集群,OpenSearch 的 Security 插件在 config.yml 里验签取角色。
# opensearch-security/config.yml
openid_auth_domain:
http_enabled: true
transport_enabled: false
order: 1
http_authenticator:
type: openid
challenge: false
config:
subject_key: preferred_username
roles_key: roles
openid_connect_url: https://kc.example.com/realms/myrealm/.well-known/openid-configuration
required_audience: os-dashboards
authentication_backend:
type: noop# opensearch_dashboards.yml:前缀是 opensearch_security,不是 plugins.security
opensearch_security.auth.type: "openid"
opensearch_security.openid.connect_url: "https://kc.example.com/realms/myrealm/.well-known/openid-configuration"
opensearch_security.openid.client_id: "os-dashboards"
opensearch_security.openid.client_secret: "SECRET"
opensearch_security.openid.base_redirect_url: "https://osd.example.com"唯一的硬前提:浏览器 SSO 路径携带的是 ID token(源码里是 Bearer ${tokenResponse.idToken}),而 Keycloak 内置 roles scope 的 realm roles mapper 默认只写 access token——所以必须显式勾上 Add to ID token,否则登录成功但角色取不到。roles_key 取到的值只是 backend_roles,还要再映射到 OpenSearch 角色才有权限。完整排错路径见
OpenSearch Dashboards 接入 Keycloak OIDC。
Apache Superset
Superset 的 OAuth 由 Flask-AppBuilder(FAB)实现,Keycloak 走的是 FAB 内置 provider 分支,与前面几个软件有三处结构性差异:provider 名必须命中 FAB 内置名单(写成 keycloak-oidc 会抛 OAuthProviderUnknown,浏览器在 Keycloak 认证成功后又被弹回登录页);用户名与组都从 /userinfo 端点读(role_keys 取的是 groups claim),所以要勾的是 mapper 的 Add to userinfo,ID token 里的组它不看;角色由 AUTH_ROLES_MAPPING 从 claim 值映射到 FAB 角色,而 AUTH_ROLES_SYNC_AT_LOGIN 默认为 False,不改它的话角色只在首次注册时算一次、之后在 Keycloak 里改组不生效。
OAUTH_PROVIDERS = [{
"name": "keycloak",
"token_key": "access_token",
"remote_app": {
"client_id": "superset",
"client_secret": "SECRET",
"server_metadata_url": "https://kc.example.com/realms/myrealm/.well-known/openid-configuration",
"api_base_url": "https://kc.example.com/realms/myrealm/protocol/openid-connect",
"client_kwargs": {"scope": "openid email profile"},
},
}]
AUTH_ROLES_MAPPING = {"/superset_admins": ["Admin"], "/superset_users": ["Gamma"]}
AUTH_ROLES_SYNC_AT_LOGIN = Trueapi_base_url 不是可选项:FAB 请求的是相对路径 openid-connect/userinfo,Authlib 只在 api_base_url 存在时才做 urljoin,而 discovery(server_metadata_url)不会回填它;结尾少一个斜杠还会把 protocol 段吃掉,变成 404。scope 里的 openid 也不能省——Keycloak 的 /userinfo 会对缺 openid 的 access token 返回 Missing openid scope。字段语义、报错对照与回滚顺序见
Apache Superset 接入 Keycloak OIDC。
通用接入步骤速查
不论软件用原生还是 oauth2-proxy,通用五步:
- 在 Keycloak 创建
confidentialClient,开启Client Authentication与Authorization Code + PKCE。 - 精确配置
Valid redirect URI(含回调路径),避免通配。 - 在 Client 的 Protocol Mappers 加
email、profile、groups等所需 claim。 - 软件侧填 Issuer / Auth / Token / UserInfo / 客户端凭证。
- 联调一次登录流程,确认回调、token、用户属性、角色映射正确。
小结
Keycloak 与开源生态的集成主要分三条路:原生 OIDC(Grafana、GitLab、Harbor、Vault、MinIO、Nextcloud、OpenSearch Dashboards、Apache Superset 等)、前置 OAuth2 Proxy(任意 Web 服务、K8s Ingress),以及 LDAP/AD 用户联邦(保留现有目录服务作为权威用户源)。掌握这三条路、五步通用流程与 Protocol Mapper 的 claim 映射,即可把一整套开源软件和目录服务统一纳入 SSO。还有一类软件必须单独走一条路:只实现 SAML 2.0、不提供 OIDC 的存量系统(OA、ERP、采购的 SaaS、SAML 版报表与监控平台)。它在 Keycloak 侧建的是 SAML 客户端,字段与上面五步完全不同——Keycloak 用请求里的 Issuer 匹配 Client ID,断言投递地址要在 Valid Redirect URIs 与 Fine Grain 端点之间做回退,NameID 的生成规则与签名开关也另有语义。这条路径的字段对应关系、错误对照表与回滚顺序见
Keycloak 作为 SAML IdP 接入应用。
集成中遇到具体报错,参见 常见问题排查。关于 LDAP/AD 联邦的完整配置步骤,参见 Keycloak LDAP / AD 用户联邦实战指南。