本文记录了我怎样给 OpenBao 接上 Okta 登录,再用 Okta 的组决定登录后的权限。

前情提要

第一篇把 OpenBao 装好并解封,第二篇加固了 Caddy 入口,结尾说下一步要创建日常用户,不再一直用 root。

这篇就干这个:Okta 负责确认您是谁,OpenBao 决定您能干什么。

本文接着前两篇的环境操作,默认您有一个 Okta 组织,也会用 bao 命令行。下文的域名、Okta 组织地址和应用名都是虚构的,各种 ID 都换成了占位符;CLIENT_ID、CLIENT_SECRET 需要从自己的 Okta 应用取得。

C 目标

整条路径是这样的:

---
config:
  theme: base
  themeVariables:
    background: "#faf4ed"
    primaryColor: "#fffaf3"
    primaryTextColor: "#464261"
    primaryBorderColor: "#286983"
    secondaryColor: "#f2e9e1"
    secondaryTextColor: "#464261"
    tertiaryColor: "#faf4ed"
    tertiaryTextColor: "#464261"
    lineColor: "#286983"
    textColor: "#464261"
    edgeLabelBackground: "#faf4ed"
---
flowchart TB
    A["浏览器 / OpenBao CLI"] --> B["Okta OIDC 应用"]
    B -- "认证通过,返回 sub 和 baogroups" --> C["OpenBao OIDC role:okta"]
    C --> D["OpenBao Identity Entity"]
    D --> E["Group alias:bao-admin"]
    D -. "预留,尚未验证" .-> F["Group alias:bao-user"]
    E --> G["External group:bao-admin"]
    F --> H["External group:bao-user"]
    G --> I["OpenBao Policy"]
    H --> I

    classDef step fill:#fffaf3,stroke:#286983,color:#464261;
    classDef pending fill:#f2e9e1,stroke:#907aa9,color:#464261;
    class A,B,C,D,E,G,I step;
    class F,H pending;

这里分三层:

  • Okta Group:控制谁能访问这个应用,并通过 claim 告诉 OpenBao 用户属于哪些组。
  • OpenBao OIDC role:规定怎样校验登录信息,用哪个字段当用户、哪个字段当组,以及默认给什么策略。
  • OpenBao Identity Group 和 Policy:根据外部组成员身份,给出真正的资源权限。

openbao-okta-oidc-cover_20261108

配置里会有 default_role=okta,它只表示没指定 role 时用哪个登录配置,不代表所有人都是管理员。危险的是把 admin 策略直接配给一个不限制用户和组的公共 role,所以下面的 role 只给 default。

Dm Okta 这边

先用 Okta 管理员建这几样东西:

对象 名称 用途
用户 baoadmin OpenBao 日常管理账号
组 bao-admin OpenBao 管理员组
组 bao-user 普通用户组,目前是空的
OIDC Web 应用 OpenBao Prod OpenBao 的登录入口

操作路径如下,页面名称可能随版本变化:

  1. Directory → Groups:创建 bao-admin、bao-user。
  2. Directory → People:创建用户 baoadmin,加入 bao-admin。
  3. Applications → Applications → Create App Integration:选择 OIDC - OpenID Connect 和 Web Application。
  4. 应用名填 OpenBao Prod,使用 Authorization Code,并分配给上面两个组。

Assignments of Okta App

Sign-in redirect URI 填:

https://openbao.miyunda.com/ui/vault/auth/oidc/oidc/callback

Sign-out redirect URIs :

https://openbao.miyunda.com/ui

光把组分配给应用还不够,OpenBao 并不知道用户属于哪个组,还要在 Okta 的 token claims 里加一条:

Name: baogroups
Value: user.getGroups({'group.profile.name': 'bao-', 'operator': 'STARTS_WITH'}).![profile.name]

bao- 这个前缀让它只返回跟 OpenBao 有关的组。语法见 Okta 表达式语言文档。管理员登录时,token 里应该是这样:

{
  "sub": "<OKTA_USER_ID>",
  "baogroups": ["bao-admin"]
}

Em OpenBao 这边

先指定地址,启用 OIDC 认证:

export BAO_ADDR='https://openbao.miyunda.com'
bao auth enable oidc

然后写 OIDC 配置,oidc_discovery_url 填自己的 Okta 组织地址:

bao write auth/oidc/config \
  oidc_discovery_url='https://<okta-org>.okta.com' \
  oidc_client_id="$CLIENT_ID" \
  oidc_client_secret="$CLIENT_SECRET" \
  default_role='okta'

再建一个中性的 role:

bao write auth/oidc/role/okta \
  role_type='oidc' \
  bound_audiences="$CLIENT_ID" \
  allowed_redirect_uris='https://openbao.miyunda.com/ui/vault/auth/oidc/oidc/callback' \
  user_claim='sub' \
  groups_claim='baogroups' \
  oidc_scopes='openid,profile,email' \
  policies='default'

这样谁通过这个 role 登录,都只有 default 策略,管理员身份留给后面的组映射决定。

各参数的含义见 OpenBao 的 JWT/OIDC 文档,Okta 专门的说明在这里。

F 第一次登录失败

openbao-okta-oidc-provider

打开 https://openbao.miyunda.com,已经出现 Sign in with Okta。点进去也跳到了 Okta,可就是登录不了,OpenBao 还提示 provider window 在认证结束前关闭了。

跳转成功,不代表 Okta 同意放行。去 Okta 后台 Reports → System Log 找到了这条:

{
  "eventType": "application.policy.sign_on.deny_access",
  "displayMessage": "Access has been denied because the policy requirements could not be satisfied by the users’ current set of available authenticator enrollments",
  "outcome": {
    "result": "SUCCESS",
    "reason": "The policy requirements could not be satisfied by the users’ current set of available authenticator enrollments"
  }
}

注意 result 是 SUCCESS。这个成功说的是拒绝访问这件事执行成功了,不是 Bao Admin 登录成功了。

账号其实已经注册了 Okta Verify、Push 和 FastPass,所以不是没有认证器,而是这个应用的登录策略要求的条件,当前环境满足不了。

openbao-okta-oidc-Catch-all-Rule

最后我在这个应用对应的 Authentication Policy 里,把要求的两项认证改成一项,同时禁止只用密码登录。这下能登录了。

再说一次,我这是过家家,不是正经生产环境,在我这里我就喜欢“手机上点一下确认就能用了”这种流畅体验。想要严肃 MFA 策略的请自行研究。

正经企业的组通常是 Active Directory 与 Okta 映射, 然后 Okta 与 HashiCorp Vault 映射。

G 登录之后

登录成功后,用这个身份执行 bao token lookup,关键信息如下:

token_policies       [default]
identity_policies    []
policies             [default]
token_meta_role      okta

认证通了,role 是 okta,组策略还没有。

在网页的 Identity 页面能看到新建的 Entity,别名是一串 Okta User ID:

<OKTA_USER_ID>

因为 role 里写了 user_claim='sub'。这个 ID 不会变,比可以修改的邮箱地址稳妥。

openbao-okta-oidc-entities

两个终端

调试的时候我在同一台 LXC 里开了两个终端,一个操作 root,另一个用 baoadmin 登录。这样是不行的。

同一个系统用户下的多个终端,共用默认 token helper 保存的凭据。后一个窗口登录时换掉了凭据,前一个窗口下次请求读到的就是新 token,root 就这样被顶掉了。

调试时可以给每个 shell 单独设 BAO_TOKEN,或者用不同的系统用户。想看当前是谁,执行 bao token lookup,不需要打印 token 本身。

要么干脆用两个不同的 Linux 机器当客户端。

Am 映射外部组

OpenBao 这边要建 external group,不用手工维护成员列表:

用 root 运行

bao write identity/group \
  name='bao-admin' \
  type='external'

记下返回的 Group ID,再找到 OIDC 认证的 accessor:

bao auth list -format=json

建立 group alias:

bao write identity/group-alias \
  name='bao-admin' \
  mount_accessor='<OIDC_ACCESSOR>' \
  canonical_id='<GROUP_ID>'

检查一下:

bao list identity/group-alias/id
bao read identity/group/id/<GROUP_ID>

alias 的关键字段:

canonical_id   <GROUP_ID>
mount_accessor <OIDC_ACCESSOR>
mount_path     auth/oidc/
mount_type     oidc
name           bao-admin
namespace_id   root

name 必须和 Okta baogroups 传来的组名一致。mount_accessor 绑定的是这一处认证挂载,以后重建这个挂载,accessor 会变,映射也要重新检查。

openbao-okta-oidc External group alias

验证整条链

先写一个几乎没有权限的策略 bao-admin-test.hcl:

path "sys/health" {
  capabilities = ["read"]
}

挂到组上:

bao policy write bao-admin bao-admin-test.hcl

bao write identity/group/id/<GROUP_ID> \
  policies='bao-admin'

这时 bao-admin 策略只是个测试用的名字。重新用 Okta 登录:

identity_policies    [bao-admin]
policies             [default]

通了。policies 是 token 自己的策略,identity_policies 是通过组继承来的,两个列表不需要一样。

折腾这么久,我的“管理员”目前能做的只有看健康状态,不过从 Okta 用户到 OpenBao 策略这条链算是验证过了。

bao-user 组还是空的,普通用户登录、权限边界,以及离组后权限多久收回,这些我都还没试过。

Bdim 管理员策略

接下来要把测试策略换成真正的管理员策略。我一开始写的是这样:

# 权限过宽,不要照抄
path "auth/*" {
  capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}

path "identity/*" {
  capabilities = ["create", "read", "update", "delete", "list", "patch"]
}

path "sys/policies/acl/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

看着不是 root,实际上差不多。能改策略、改 role、改组和别名,就能给自己扩权。这个策略没有叫 root,但它能把自己变成 root。排除 sys/raw/*、sys/generate-root* 这些路径也解决不了这个问题。反正只有我自己用,无所谓了。

人多的可以按职责拆开,没试过,只是想象:

策略 范围
bao-operator 健康状态、审计信息、有限的运维操作
bao-secrets-admin 指定 KV 挂载的管理,不是所有秘密
bao-pki-admin 指定 PKI 挂载的管理
bao-iam-admin 严格限制的身份与认证配置管理,单独授权和审计
应急账号 极少使用的高风险操作,单独保管

小结

baoadmin 已经能通过 Okta 登录 OpenBao,bao-admin 组也映射到了 OpenBao 中的我们自定义的管理员策略。