Keycloak 接入开源软件时,先判断目标软件是否原生支持 OIDC/SAML;只有不支持或需要统一保护多个 Web 应用时,才把 oauth2-proxy 放在入口。Grafana、GitLab、Vault、Harbor 等常见软件的配置字段并不相同,本节只给出协议边界和最小可验证配置,不把“能跳转到登录页”当成集成完成。

前置:先在 Keycloak 创建一个 confidential Client,记录 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 加 groups mapper(把用户 Realm/Client 角色映射成 groups claim),即可用角色驱动 Grafana 权限。
  • role_attribute_path 把 Keycloak 角色映射为 Grafana Admin/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 顶层不会被读取,而且没有任何日志或报错。
  • groups claim 的值必须与 IdP 返回的字符串逐字符相等:Keycloak 的 Group Membership mapper 默认 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 声明式管理:

  1. 安装 OpenID Connect Authentication 插件(4.727 起要求 Jenkins ≥ 2.539)。
  2. Keycloak 侧建 client:Client authentication ON,Valid redirect URIs 填 ${JENKINS_ROOT_URL}/securityRealm/finishLogin,并把 ${JENKINS_ROOT_URL}/OicLogout 加进 Valid post logout redirect URIs。
  3. Manage Jenkins → Security → Security Realm = OpenID Connect,填 well-known 地址与 client secret;groupsFieldName 用 realm_access.roles,或改用自建 Group Membership mapper 输出的用户组 claim。
  4. 授权仍在 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,日志里只有 accountoauth2-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 foundredirect_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:// 或错误 hostKeycloak / 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 NameKeycloak
OIDC Endpointhttps://kc.example.com/realms/myrealm
OIDC Client IDharbor
OIDC Client SecretSECRET
OIDC Scopeopenid,profile,email,offline_access
Group Claim Namegroups

要点: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 = True

api_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,通用五步:

  1. 在 Keycloak 创建 confidential Client,开启 Client Authentication 与 Authorization Code + PKCE。
  2. 精确配置 Valid redirect URI(含回调路径),避免通配。
  3. 在 Client 的 Protocol Mappers 加 email、profile、groups 等所需 claim。
  4. 软件侧填 Issuer / Auth / Token / UserInfo / 客户端凭证。
  5. 联调一次登录流程,确认回调、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 用户联邦实战指南。