OpenBao 实战笔记:从上手到生产部署(已更新至 v2.7.2)

数据库密码、API Key、TLS 证书这些敏感信息,散落在环境变量、配置文件和 CI/CD Pipeline 里,几乎是所有攻击者的第一目标。密钥管理这件事,迟早要做,早做比晚做好。

OpenBao 是 HashiCorp Vault 的开源分叉,挂在 Linux Foundation 的 LF Edge 下面,MPL-2.0 许可证。起因是 2023 年 8 月 HashiCorp 把 Vault 从 MPL-2.0 改成了 BSL,限制竞争性使用,社区一怒之下 fork 了一份。现在它的 API 和 Vault 高度兼容,CLI 命令是 bao,用法几乎照搬 Vault。

这篇文章是我把 OpenBao 从上手到生产跑通一遍的记录,覆盖安装、各类 Secret Engine、认证授权、Seal/Unseal、HA 集群和 Kubernetes 集成。内容已按 OpenBao v2.7.2(2026 年 10 月 26 日发布)核对过,2.6 和 2.7 之间的破坏性变更都写进来了。

升级前先看看 2.6 → 2.7.2 改了什么

如果你已经在跑 2.5.x 或 2.6.x,升级之前这几条必须先过一遍,不然会踩坑。

破坏性变更:

  • 云厂商 Auto Unseal 不再内置 。awskms、gcpckms、azurekeyvault、alicloudkms、ocikms、pkcs11 全部移出了主二进制,要去 openbao-plugins 装对应的 KMS 插件,再用 plugin "kms" 声明式注册。独立的 HSM 发行版也停发了。
  • LDAP / Kerberos / RADIUS 认证引擎、LDAP 密钥引擎移出主二进制,同样需要外部插件。
  • file 存储后端被移除 ,新的替代是非 HA 的 PebbleDB 后端,Linux 软件包的默认配置已经切过去了。还在用 storage "file" 的配置在 2.7 上起不来。
  • 容器镜像默认以非 root 的 openbao 用户运行(2.6 起),依赖 root 的自定义启动脚本需要适配。
  • PKI 不再接受以 . 结尾的域名(example.com. 这种会被拒)。

值得用的新东西:

  • External Keys:PKI 和 Transit 可以直接调用 KMS/HSM 里的密钥做运算,密钥材料不进 OpenBao。
  • ML-DSA 后量子签名:PKI 签发和 Transit 签名都支持 NIST FIPS 204 的 ML-DSA(44/65/87 三个参数集)。监听器还能配置纯 PQC 的 TLS 密钥交换。
  • 强一致性控制 :写请求响应带 X-Vault-Index 头,读请求可以声明自己见到过的最新写入,节点数据落后时可以选择直接 429、转发给活跃节点或者本地等待追平。治"写完马上从 Standby 读到旧值"这个老毛病。
  • Control Groups:ACL 策略里可以写审批控制,敏感路径的操作要第二个人批了才执行。
  • PebbleDB 存储后端 :替代 file,性能好不少。
  • 声明式自初始化增强(2.6):支持 CEL 表达式和模板,Dev 模式也能用。
  • tls_auto_reload:证书文件变了自动重载,不用再发 SIGHUP。

为什么要立刻升到 2.7.2:2.7.1 和 2.7.2 一共修了 11 个安全漏洞,包括 AppRole 的 Secret ID 过期后仍能认证、Kubernetes Auth 在 Service Account 被吊销后还能续租、Transit 导入备份时的路径遍历、SSH 证书模板默认允许通配符等。低于 2.7.2 的部署都应该尽快升。

核心概念

术语不多,先把这几个记住就行:

术语 说明
Secret Engine 存储、生成或加密数据的插件化组件,按路径挂载(secret/、database/、pki/)
Auth Method 认证方式,验证身份后发 Token
Token 认证成功后拿到的凭证,带策略信息
Policy ACL 策略,定义 Token 对路径的权限
Lease 密钥有效期,到期自动撤销,可续租
Seal / Unseal 加密保护机制,启动时处于 Sealed,要 Unseal Key 才能解密数据
Barrier 加密屏障,写进存储的数据都经过它加密
Integrated Storage 内置 Raft 共识存储,不依赖外部组件
PebbleDB 2.7 新增的非 HA 持久化后端
External Key 2.7 新增,存于外部 KMS/HSM、由 OpenBao 调用的密钥

架构

OpenBao 的安全核心是多层加密。理解这条链基本就理解了一半:

复制代码
Unseal Key(操作员持有 / 外部 KMS 插件托管)
    └── 加密 → Root Key
            └── 加密 → Encryption Keyring
                    └── 加密 → 存储里的所有数据

几个要点:

  1. Unseal Key 不存在任何地方。Shamir 模式下它分散在多个人手里,Auto Unseal 模式下由外部 KMS 插件托管。
  2. 就算攻击者拖走了整个存储后端的数据,没有 Unseal Key 也解不开。
  3. 数据加密密钥可以在线轮换,旧密钥留在 Keyring 里解密历史数据,不用停机。

请求进来时的处理路径是:Listener 终止 TLS(2.7 可选纯 PQC 密钥交换)→ 一致性检查(X-Vault-Index)→ 审计 → Token 验证 → ACL 评估(可能触发 Control Group 审批)→ Secret Engine 处理 → Barrier 加密落盘。文末有一张完整的架构图。

安装

macOS 最省事:

bash 复制代码
brew install openbao
bao version
# OpenBao v2.7.2 ...

Linux:

bash 复制代码
# Arch
pacman -Sy openbao

# Fedora / RHEL
dnf install -y epel-release
dnf install -y openbao

# Debian / Ubuntu:从 https://openbao.org/downloads/ 下载
wget https://github.com/openbao/openbao/releases/download/v2.7.2/bao_2.7.2_linux_amd64.deb
sudo dpkg -i bao_2.7.2_linux_amd64.deb

顺带一提,2.6 起 Release 制品的命名统一了(架构统一 amd64,校验和合并成一个 checksums.txt),写自动下载脚本的话注意一下。

Docker:

bash 复制代码
docker pull ghcr.io/openbao/openbao:latest

docker run --rm -p 8200:8200 \
  --memory-swappiness=0 \
  --name openbao-dev \
  ghcr.io/openbao/openbao:latest server -dev

装完记得加固:禁用 Swap(密钥数据不能进交换空间),systemd 服务文件默认有 MemorySwapMax=0,Docker 要加 --memory-swappiness=0。macOS 的 Swap 默认加密,不用管。另外 2.6 起容器默认非 root 运行,镜像里也没有 VOLUME 指令了,数据目录要自己显式挂载。

Dev 模式上手

Dev 模式内存存储、自动 Unseal、预生成 Root Token,学习用很合适,但绝对不要拿去跑生产。

bash 复制代码
bao server -dev
# ==> OpenBao server configuration:
#                  Storage: inmem
#                  Version: OpenBao v2.7.2
# ...
# Root Token: hvs.xxxxxxxxxxxxxxxxxxxxxxxx

另开一个终端:

bash 复制代码
export BAO_ADDR='http://127.0.0.1:8200'
export BAO_TOKEN='hvs.xxxxxxxxxxxxxxxxxxxxxxxx'
bao status
bao status -field=version   # 2.7 起 status 支持 -field

2.6 起 Dev 模式也支持声明式自初始化了,启动时可以自动执行一串预定义的 API 请求,搭演示环境很方便。

KV 引擎:第一个 Secret

Dev 模式默认挂了 KV v2 在 secret/ 路径,最常用的命令跑一遍:

bash 复制代码
# 写入
bao kv put secret/myapp/config \
  db_host="postgres.prod.internal" \
  db_port="5432" \
  db_user="app_user" \
  db_password="S3cur3P@ssw0rd!"

# 读取
bao kv get secret/myapp/config

# 只取一个字段
bao kv get -field=db_password secret/myapp/config

# JSON 输出给脚本用
bao kv get -format=json secret/myapp/config | jq '.data.data'

# 更新(会产生新版本)
bao kv put secret/myapp/config db_password="N3wS3cur3P@ss!"

# 读历史版本
bao kv get -version=1 secret/myapp/config

# 列出路径
bao kv list secret/myapp/

# 软删除 / 恢复 / 永久销毁
bao kv delete secret/myapp/config
bao kv undelete -versions=2 secret/myapp/config
bao kv destroy -versions=1 secret/myapp/config

KV v1 和 v2 的区别:v2 有版本控制、软删除、元数据和 CAS(check-and-set,防止并发写互相覆盖),API 路径多一层 secret/data/。生产上直接用 v2,v1 只适合不在乎版本的小场景。

不走 CLI 的话,所有操作都有 HTTP API:

bash 复制代码
curl -s \
  --header "X-Vault-Token: $BAO_TOKEN" \
  --request POST \
  --data '{"data": {"api_key": "sk-abc123def456"}}' \
  $BAO_ADDR/v1/secret/data/myapp/api-keys | jq

这里说一下 2.7 的强一致性头:写请求响应会带 X-Vault-Index,后续读请求带上这个值再加 X-Vault-Inconsistent 声明策略------fail(数据没追平就返回 429 + Retry-After)、forward-active-node(转发给活跃节点)、await-state(本地等待追平),还能组合。HA 集群里"刚写完读不到"的问题可以靠它缓解。

Secret Engines

先看全景:

引擎 类型 用途 2.7 状态
KV 存储 键值对 内置
Database 动态 数据库临时账号 内置,MySQL 2.6 起支持多主机故障转移
PKI 动态 X.509 证书签发 内置,2.7 支持 ML-DSA 和外部密钥
Transit 加密 加密即服务 内置,2.7 支持 ML-DSA 和外部密钥
SSH 动态 SSH 证书签发 内置,2.7.2 起模板默认禁止通配符
TOTP 动态 TOTP 码 内置
Transform 加密 FPE / 脱敏 内置
LDAP 动态 LDAP 凭证 已移出主二进制,需外部插件

Database:动态数据库凭证

动态密钥是这类系统最有价值的能力------按需生成有时效的数据库账号,到期自动删,不用再把一个万能密码贴得到处都是。

bash 复制代码
bao secrets enable database

# 配置 PostgreSQL 连接
bao write database/config/my-postgresql-db \
  plugin_name="postgresql-database-plugin" \
  allowed_roles="readonly,readwrite" \
  connection_url="postgresql://{{username}}:{{password}}@postgres.internal:5432/myapp?sslmode=require" \
  username="vault_admin" \
  password="vault_admin_password"

# 只读角色
bao write database/roles/readonly \
  db_name="my-postgresql-db" \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
    GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
  revocation_statements="DROP ROLE IF EXISTS \"{{name}}\";" \
  default_ttl="1h" \
  max_ttl="24h"

# 动态获取凭证
bao read database/creds/readonly
# lease_id           database/creds/readonly/abcd1234...
# lease_duration     1h
# username           v-token-readonly-xxxxxxxxx
# password           A1a-xxxxxxxxxxxxxxxxx

# 续租 / 撤销
bao lease renew database/creds/readonly/abcd1234...
bao lease revoke database/creds/readonly/abcd1234...

几个经验:default_ttl 控制在 1 小时以内;应用要在 Lease 到期前主动续租,或者干脆交给 Agent;配置完连接后马上跑 bao write -force database/rotate-root/my-postgresql-db 把管理员密码轮换掉,防止有人拿原始密码直接登数据库。

PKI:内部 CA

OpenBao 可以当内部 CA 用,签发和管理 TLS 证书。典型做法是 Root CA + Intermediate CA 两层:

bash 复制代码
# Root CA
bao secrets enable pki
bao secrets tune -max-lease-ttl=87600h pki
bao write -field=certificate pki/root/generate/internal \
  common_name="My Organization Root CA" \
  issuer_name="root-2026" \
  ttl=87600h > root_ca.crt

# Intermediate CA:开第二个挂载点,生成 CSR,拿 Root 签,再导回去
bao secrets enable -path=pki_int pki
bao secrets tune -max-lease-ttl=43800h pki_int
bao write -field=csr pki_int/intermediate/generate/internal \
  common_name="My Organization Intermediate CA" \
  issuer_name="intermediate-2026" > pki_intermediate.csr
bao write -field=certificate pki/root/sign-intermediate \
  csr=@pki_intermediate.csr format=pem_bundle ttl=43800h > intermediate_ca.crt
bao write pki_int/intermediate/set-signed certificate=@intermediate_ca.crt

# 定义可签发的证书规范
bao write pki_int/roles/internal-service \
  allowed_domains="internal.mycompany.com,svc.cluster.local" \
  allow_subdomains=true max_ttl="720h" key_type="ec" key_bits=256

# 签发
bao write pki_int/issue/internal-service \
  common_name="api.internal.mycompany.com" ttl="72h"
# 返回里的 private_key 只有这一次能拿到,丢了只能重签

2.7 的两个新能力:一是 PKI 全流程支持 ML-DSA 后量子签名(FIPS 204,注意 Go 的 OCSP 还不支持 ML-DSA,这类 Issuer 用不了 OCSP);二是可以用外部密钥签发,私钥留在 HSM 里,OpenBao 只拿签名结果。

Transit:加密即服务

应用不用自己管加密密钥,调 API 加解密就行,数据本身不落在 OpenBao:

bash 复制代码
bao secrets enable transit
bao write -f transit/keys/my-app-key

# 加密(输入要 base64)
bao write transit/encrypt/my-app-key plaintext=$(echo -n "my-secret-data" | base64)
# ciphertext     vault:v1:xxxxxxxxxxxxxxxxxxxxxx

# 解密
bao write transit/decrypt/my-app-key ciphertext="vault:v1:xxxxxxxxxxxxxxxxxxxxxx"

# 轮换密钥、用新版本重加密旧密文
bao write -f transit/keys/my-app-key/rotate
bao write transit/rewrap/my-app-key ciphertext="vault:v1:..."

2.7 给 Transit 加了 ML-DSA 签名密钥,44/65/87 三个参数集都支持生成、导入导出:

bash 复制代码
bao write -f transit/keys/pqc-signing-key type=mldsa-65
bao write transit/sign/pqc-signing-key input=$(echo -n "message" | base64)

提醒一句:Transit 导入密钥备份的路由遍历漏洞(能覆盖任意密钥)是 2.7.2 才修的,用到这个功能的务必升上来。

External Keys(2.7 新增)

PKI 和 Transit 现在可以直接用外部 KMS/HSM 里的密钥做运算,密钥材料全程不进 OpenBao。通过 /sys/external-keys API 配映射:

bash 复制代码
# 需要先装对应的 KMS 插件(比如 kms-pkcs11)
bao write sys/external-keys/pki/hsm-root-ca \
  plugin_name="kms-pkcs11" \
  # ... 插件自己的密钥引用参数

Transit 后端密钥是内置支持的,PKCS#11 走 kms-pkcs11 插件,接口本身和 Provider 无关,想接自家 KMS 可以自己写插件。

认证与授权

常用的认证方式:Token(一切的终点)、UserPass(测试用,2.6 起支持预哈希 bcrypt 密码)、AppRole(应用首选)、Kubernetes(Pod 用 ServiceAccount 认证)、JWT/OIDC、TLS 证书(2.7 支持解析 Envoy 的 XFCC 头)。LDAP、Kerberos、RADIUS 2.7 起都挪去外部插件了。

AppRole

应用接入首选 AppRole,双因素:Role ID(可以大方放在配置里)加 Secret ID(要走安全通道下发,而且建议限一次使用):

bash 复制代码
bao auth enable approle

bao write auth/approle/role/my-app \
  token_policies="my-app-policy" \
  token_ttl=1h token_max_ttl=4h \
  secret_id_ttl=10m \
  secret_id_num_uses=1

# 拿 Role ID
bao read auth/approle/role/my-app/role-id

# 生成 Secret ID
bao write -f auth/approle/role/my-app/secret-id

# 应用侧登录
bao write auth/approle/login \
  role_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  secret_id="yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"

2.7.1 修过一个 AppRole 的洞:Secret ID 过期后、tidy 还没清理之前居然还能拿来登录。2.7.2 又修了 Role ID 路径没做规范化的问题。所以再次强调,版本要跟上。

Kubernetes Auth

bash 复制代码
bao auth enable kubernetes

bao write auth/kubernetes/config \
  kubernetes_host="https://kubernetes.default.svc:443"

bao write auth/kubernetes/role/my-app \
  bound_service_account_names=my-app-sa \
  bound_service_account_namespaces=production \
  policies=my-app-policy ttl=1h

2.7.1 之前这里有个问题:Token 续租时不会重新校验 ServiceAccount JWT,SA 被吊销了还能续。现在续租会强制校验。

Token

两种:Service Token 可续租可撤销,有持久化存储;Batch Token 轻量不可续租,适合高吞吐短任务。Root Token 权限等于 root 用户,初始化完立刻 bao token revoke 掉,真要用的时候用 bao operator generate-root 临时生成(要多个 Unseal Key 持有者共同授权)。

ACL 策略

OpenBao 是白名单模型,没写进策略的路径一律拒绝:

hcl 复制代码
# my-app-policy.hcl
path "secret/data/myapp/*" {
  capabilities = ["read", "list"]
}

path "secret/data/myapp/config" {
  capabilities = ["create", "read", "update"]
}

path "database/creds/readonly" {
  capabilities = ["read"]
}

path "sys/leases/renew"  { capabilities = ["update"] }
path "sys/leases/revoke" { capabilities = ["update"] }
path "auth/token/lookup-self" { capabilities = ["read"] }
path "auth/token/renew-self"  { capabilities = ["update"] }

# deny 优先级最高,可以覆盖其他策略
path "secret/metadata/*" {
  capabilities = ["deny"]
}

权限一共七种:create、read、update、delete、list、sudo(访问 root 级路径)、deny。

2.7 新增的 Control Groups 值得单独说:策略里可以挂审批控制,类似银行金库的双人规则。读生产根凭证、改 Root CA、导出 Transit 密钥这类操作很适合套上:

hcl 复制代码
path "secret/data/prod/root-credentials" {
  capabilities = ["read"]

  control_group {
    factor "approver" {
      identity {
        group_names = ["security-approvers"]
        approvals   = 1
      }
    }
  }
}

策略管理命令:

bash 复制代码
bao policy write my-app-policy my-app-policy.hcl
bao policy list
bao policy read my-app-policy
bao token create -policy="my-app-policy" -ttl=2h
bao token lookup -field=policies   # 2.7 起支持 -field

模板化策略也支持,{{identity.entity.name}} 这类变量可以做基于身份的动态权限,每个用户只能碰自己的命名空间。

Seal / Unseal

Shamir 分片

默认模式,用 Shamir's Secret Sharing 把 Unseal Key 切成多份,凑够阈值才能重建:

bash 复制代码
bao operator init -key-shares=5 -key-threshold=3

# Unseal 要凑 3 片
bao operator unseal   # Progress: 1/3
bao operator unseal   # Progress: 2/3
bao operator unseal   # Sealed: false

分片怎么发:生产建议 5 片阈值 3,分给不同的人物理隔离存放;小团队 3/2 也行;开发环境 1/1 就好。

Auto Unseal:2.7 改成了插件化(重点)

这是 2.7 最大的架构变化。云厂商 KMS 和 PKCS#11 的 Seal 全部从主二进制里拿出去了,只有 Transit Seal 还内置。2.7 的配置要分两步------先注册插件,再像原来一样写 seal:

hcl 复制代码
# /etc/openbao/config.hcl

# 插件二进制先放到 plugin_directory
# 官方预构建:https://github.com/openbao/openbao-plugins
plugin "kms" "awskms" {
  # 2.7 起手动安装的插件不再强制要求 sha256sum
}

seal "awskms" {
  region     = "ap-southeast-1"
  kms_key_id = "arn:aws:kms:ap-southeast-1:123456789:key/abcd-1234-..."
}

拆出去之后有几个实际好处:插件崩了会自动重启,不用整个实例跟着重启(PKCS#11 库抽风时尤其有用);SIGHUP 可以热加载 KMS 插件配置、运行中升级插件;想实现自己的 Seal 也有 SDK 了。另外 2.7 还给 Auto Seal 健康检查加了配置项,可以禁用或自定义间隔和超时。

升级时要注意:如果老版本在用云厂商 Seal,升到 2.7 之前必须先把插件装好注册好,否则升完 Unseal 不了。风险提醒还是那两条------KMS 密钥不可用就起不来,密钥被永久删除数据就永远没了,记得用 SCP 之类的机制把 KMS 密钥锁死。

Seal 类型之间迁移(比如 Shamir 换 Auto Unseal)可以做,但要停机。

生产部署

配置文件

一份生产级配置,Raft 存储加插件化的 AWS KMS Auto Unseal:

hcl 复制代码
# /etc/openbao/config.hcl(OpenBao v2.7.2)
cluster_name = "prod-openbao-cluster"

api_addr     = "https://openbao.mycompany.com:8200"
cluster_addr = "https://10.0.1.10:8201"

ui         = true
log_level  = "info"
log_format = "json"

listener "tcp" {
  address         = "0.0.0.0:8200"
  cluster_address = "0.0.0.0:8201"

  tls_cert_file = "/etc/openbao/tls/server.crt"
  tls_key_file  = "/etc/openbao/tls/server.key"

  # 证书文件变了自动重载,不用 SIGHUP
  tls_auto_reload = true

  tls_min_version    = "tls12"
  tls_cipher_suites  = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"

  # 想上后量子 TLS 的话:
  # tls_key_exchange_preferences = "X25519MLKEM768,SecP256r1MLKEM768"

  # 强一致性(可选):
  # consistency_missing_header_forward = true
  # consistency_fallback_behavior      = "forward-active-node"
  # consistency_max_index_wait         = "10s"

  max_request_duration = "90s"
  max_request_size     = 33554432
}

storage "raft" {
  path    = "/opt/openbao/data"
  node_id = "node-1"

  performance_multiplier = 1

  autopilot {
    cleanup_dead_servers               = true
    dead_server_last_contact_threshold = "24h"
    min_quorum                         = 3
  }

  retry_join {
    leader_api_addr     = "https://10.0.1.11:8200"
    leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
  }
  retry_join {
    leader_api_addr     = "https://10.0.1.12:8200"
    leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
  }
}

plugin "kms" "awskms" {
}

seal "awskms" {
  region     = "ap-southeast-1"
  kms_key_id = "arn:aws:kms:ap-southeast-1:123456789:key/abcd-1234-..."
}

telemetry {
  prometheus_retention_time = "24h"
  disable_hostname          = true
}

default_lease_ttl = "768h"
max_lease_ttl     = "768h"

单机或边缘场景不需要 HA 的话,用 PebbleDB(storage "pebbledb"),file 后端已经没有了。如果团队已经有在维护的 PostgreSQL 集群,也可以直接用 PostgreSQL 当存储后端,见下文"PostgreSQL 存储后端"一节。

PostgreSQL 存储后端

除了 Raft,OpenBao 官方支持的另一个生产级 HA 后端是 PostgreSQL(v2.1 重新引入,最低要求 PostgreSQL 9.5)。适合已经有成熟 PG 运维体系、不想再养一套 Raft 的团队:

hcl 复制代码
storage "postgresql" {
  connection_url = "postgres://openbao:secret@postgres.internal:5432/openbao?sslmode=verify-full"
  ha_enabled     = true                  # 开启 HA(基于 openbao_ha_locks 表抢锁选主)
  max_parallel   = 128                   # 最大并发请求数
  # skip_create_table = true             # 连接账号没有建表权限时要手动建表并打开这个
}

连接串也可以走 BAO_PG_CONNECTION_URL 环境变量或标准 PGHOST 系列变量,密码不用写进配置文件。表结构 OpenBao 会自己建(openbao_kv_store 和 openbao_ha_locks 两张表),如果数据库账号没有 CREATE 权限,就按官方文档的 DDL 手动建好再设 skip_create_table=true。

和 Raft 怎么选:

  • Raft:数据在本地,少一跳网络,读性能好;但写入要多数派投票,对节点间延迟敏感,且要懂 Raft 的节点管理和故障切换。
  • PostgreSQL:OpenBao 节点本身无状态,扩缩容和 failover 只需要一个连接串,不用做 raft join/remove-peer 这些操作;写性能往往更好(单活跃节点写、物理复制到从库);HA 和备份恢复直接复用 PG 生态(比如 CloudNativePG + 物理复制)。代价是多一个外部依赖,以及读默认走缓存。

2.7 的一个重要补强:PostgreSQL 后端从 v2.7.0 起支持读水平扩展 (和 Raft 在 2.5 就有的能力对齐),Standby 节点可以本地处理只读请求。需要 ha_enabled = true 且 Standby 能通过 cluster_addr 连到活跃节点;长时间失去 Leader 时 Standby 会以只读模式起来。注意只支持 PG 物理复制,逻辑复制不行。要关闭就在配置里加 disable_standby_reads=true。

备份方式也不一样:Raft 用 bao operator raft snapshot,PostgreSQL 后端没有对应的快照 API,直接用 PG 自己的备份体系(pg_dump、WAL 归档、云数据库快照等)。

systemd

ini 复制代码
# /etc/systemd/system/openbao.service
[Unit]
Description=OpenBao - A tool for managing secrets
Requires=network-online.target
After=network-online.target
ConditionFileNotEmpty=/etc/openbao/config.hcl

[Service]
Type=notify
User=openbao
Group=openbao
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=yes
PrivateDevices=yes
SecureBits=keep-caps
AmbientCapabilities=CAP_IPC_LOCK
NoNewPrivileges=yes
ExecStart=/usr/bin/bao server -config=/etc/openbao/config.hcl
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
KillSignal=SIGINT
Restart=on-failure
RestartSec=5
TimeoutStopSec=30
LimitNOFILE=65536
LimitMEMLOCK=infinity
MemorySwapMax=0

[Install]
WantedBy=multi-user.target

初始化和首次启动

bash 复制代码
sudo mkdir -p /opt/openbao/data
sudo chown openbao:openbao /opt/openbao/data

sudo mkdir -p /etc/openbao/tls
sudo cp server.crt server.key ca.crt /etc/openbao/tls/
sudo chown openbao:openbao /etc/openbao/tls/*
sudo chmod 600 /etc/openbao/tls/server.key

sudo systemctl enable --now openbao

export BAO_ADDR='https://openbao.mycompany.com:8200'
export BAO_CACERT='/etc/openbao/tls/ca.crt'

bao operator init -key-shares=5 -key-threshold=3
# Unseal Key 和 Root Token 立刻收好

bao operator unseal
bao operator unseal
bao operator unseal

bao login $ROOT_TOKEN
bao audit enable file file_path=/var/log/openbao/audit.log
bao token revoke $ROOT_TOKEN

不想手动 init 的话,2.6 起声明式自初始化支持 CEL 表达式、模板引用前序响应值、when 条件执行,IaC 全自动部署可以走这条路。

HA 集群

生产推荐 Integrated Storage(Raft),不用额外养一个 Consul。三节点为例,Node 1:

hcl 复制代码
storage "raft" {
  path    = "/opt/openbao/data"
  node_id = "node-1"

  retry_join {
    leader_api_addr     = "https://10.0.1.11:8200"
    leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
  }
  retry_join {
    leader_api_addr     = "https://10.0.1.12:8200"
    leader_ca_cert_file = "/etc/openbao/tls/ca.crt"
  }
}

Node 2、3 改一下 node_id 和 retry_join 列表就行。每个节点启动后独立 Unseal,然后在 Leader 上确认成员:

bash 复制代码
bao operator raft list-peers
# node-1     10.0.1.10:8201     leader      true
# node-2     10.0.1.11:8201     follower    true
# node-3     10.0.1.12:8201     follower    true

日常运维:

bash 复制代码
bao status                                     # 看 HA 模式
bao operator step-down                         # 手动移交领导权
bao operator raft snapshot save backup-$(date +%Y%m%d).snap
bao operator raft snapshot restore backup-20261105.snap
bao operator raft remove-peer node-3

2.7 在 Raft 上修了不少东西:auto_join 带端口地址失败的问题、批量应用的二次事务性能回归、写入量不足 snapshot_threshold 时日志无限增长的问题,都修了。另外配合 X-Vault-Index,SSCT 现在能用存储层索引做精确的读一致性控制。

经验:节点数用 3 或 5(奇数),同 Region 部署节点间延迟压在 10ms 内;读请求所有节点都能处理,写会自动转给 Leader;每天定时跑 snapshot 备份。

审计日志

三种审计设备:file、syslog、socket。最常用的:

bash 复制代码
bao audit enable file file_path=/var/log/openbao/audit.log
bao audit list -detailed

所有请求和响应都会记录,敏感字段用 HMAC-SHA256 哈希,反推不出来,但知道原始值的话可以用 bao audit hash 算哈希做比对。

两个要点:必须至少有一个审计设备可用,全部失败时 OpenBao 会拒绝处理一切请求(宁可不服务也不能留审计盲区);审计日志要实时转发到集中式日志系统。2.7.1 修过一个边角问题------未认证的 Root Token Generation / Rekey 端点(默认关闭)在审计失败时居然还会返回响应,现在审计失败就直接拒了。

Kubernetes 集成

Helm 部署

bash 复制代码
helm repo add openbao https://openbao.github.io/openbao-helm
helm repo update

values.yaml:

yaml 复制代码
global:
  enabled: true
  tlsDisable: false

server:
  image:
    repository: ghcr.io/openbao/openbao
    tag: "2.7.2"

  ha:
    enabled: true
    replicas: 3
    raft:
      enabled: true
      config: |
        ui = true

        listener "tcp" {
          address = "[::]:8200"
          cluster_address = "[::]:8201"
          tls_cert_file = "/vault/tls/tls.crt"
          tls_key_file = "/vault/tls/tls.key"
          tls_auto_reload = true
        }

        storage "raft" {
          path = "/vault/data"
          retry_join {
            leader_api_addr = "https://openbao-0.openbao-internal:8200"
            leader_ca_cert_file = "/vault/tls/ca.crt"
          }
          retry_join {
            leader_api_addr = "https://openbao-1.openbao-internal:8200"
            leader_ca_cert_file = "/vault/tls/ca.crt"
          }
          retry_join {
            leader_api_addr = "https://openbao-2.openbao-internal:8200"
            leader_ca_cert_file = "/vault/tls/ca.crt"
          }
        }

        service_registration "kubernetes" {}

  dataStorage:
    enabled: true
    size: 10Gi
    storageClass: "gp3"

  resources:
    requests:
      cpu: 500m
      memory: 256Mi
    limits:
      cpu: 2000m
      memory: 1Gi

  auditStorage:
    enabled: true
    size: 5Gi

injector:
  enabled: true
  replicas: 2

ui:
  enabled: true
  serviceType: ClusterIP
bash 复制代码
helm install openbao openbao/openbao -n openbao --create-namespace -f openbao-values.yaml

Agent Injector

Pod 上加注解,Webhook 自动注入 Sidecar 拉密钥写文件:

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    metadata:
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "my-app"
        vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/readonly"
        vault.hashicorp.com/agent-inject-template-db-creds: |
          {{- with secret "database/creds/readonly" -}}
          DB_USER={{ .Data.username }}
          DB_PASSWORD={{ .Data.password }}
          {{- end }}
        vault.hashicorp.com/agent-inject-secret-app-config: "secret/data/myapp/config"
        vault.hashicorp.com/agent-inject-template-app-config: |
          {{- with secret "secret/data/myapp/config" -}}
          API_KEY={{ .Data.data.api_key }}
          {{- end }}
    spec:
      serviceAccountName: my-app-sa
      containers:
        - name: my-app
          image: my-app:latest
          command: ["/bin/sh", "-c"]
          args:
            - |
              source /vault/secrets/db-creds
              source /vault/secrets/app-config
              exec ./my-app-binary

密钥会落在 Pod 的 /vault/secrets/ 目录。

CSI Provider

另一种思路,密钥直接当卷挂载,走标准的 secrets-store CSI:

yaml 复制代码
volumes:
  - name: secrets
    csi:
      driver: secrets-store.csi.k8s.io
      readOnly: true
      volumeAttributes:
        secretProviderClass: "openbao-secrets"
---
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: openbao-secrets
spec:
  provider: vault   # 兼容 Vault CSI Provider
  parameters:
    roleName: "my-app"
    vaultAddress: "https://openbao.openbao.svc:8200"
    objects: |
      - objectName: "db-password"
        secretPath: "secret/data/myapp/config"
        secretKey: "db_password"

Agent / Proxy

Agent 是客户端守护进程,干四件事:Auto Auth 自动拿 Token、本地缓存、模板渲染、API 代理。

hcl 复制代码
# /etc/openbao/agent.hcl
auto_auth {
  method "approle" {
    config = {
      role_id_file_path   = "/etc/openbao/role-id"
      secret_id_file_path = "/etc/openbao/secret-id"
      remove_secret_id_file_after_reading = true
    }
  }

  sink "file" {
    config = {
      path = "/tmp/openbao-token"
      mode = 0640
      # 2.6 起可以指定属主:
      # uid = 1000
      # gid = 1000
    }
  }
}

cache {
  use_auto_auth_token = true
}

listener "tcp" {
  address     = "127.0.0.1:8100"
  tls_disable = true
}

vault {
  address     = "https://openbao.mycompany.com:8200"
  tls_ca_cert = "/etc/openbao/tls/ca.crt"
}

template {
  source      = "/etc/openbao/templates/db-config.tpl"
  destination = "/app/config/database.env"
  perms       = 0640
  command     = "systemctl reload my-app"
}

模板文件长这样:

复制代码
{{ with secret "database/creds/readonly" }}
DB_USER={{ .Data.username }}
DB_PASSWORD={{ .Data.password }}
{{ end }}

Proxy 是精简版 Agent,只做 API 代理,适合当 Sidecar。2.6/2.7 的小变化:SIGHUP 会重载客户端 TLS 配置;开了 require_request_header 之后 /quit 和 /cache-clear 要求带 X-Vault-Request 头。

运维

监控指标

盯这几个就够:

指标 说明 告警参考
vault.core.handle_request 请求延迟 P99 > 500ms
vault.barrier.get / put Barrier 读写延迟 P99 > 100ms / 200ms
vault.raft.leader.lastContact Follower 到 Leader 最后联系 > 500ms
vault.raft.commitTime Raft 提交时间 P99 > 50ms
vault.expire.num_leases 活跃 Lease 数 按容量规划
vault.core.unsealed 是否已 Unseal 等于 0 立刻告警

2.7 起 vault.core.unsealed=0 走独立指标循环上报,Sealed 状态下的指标更靠谱了。

Prometheus 抓取配置:

yaml 复制代码
scrape_configs:
  - job_name: "openbao"
    metrics_path: "/v1/sys/metrics"
    params:
      format: ["prometheus"]
    scheme: "https"
    tls_config:
      ca_file: "/etc/prometheus/openbao-ca.crt"
    authorization:
      credentials_file: "/etc/prometheus/openbao-token"
    static_configs:
      - targets: ["openbao-0.internal:8200", "openbao-1.internal:8200", "openbao-2.internal:8200"]

备份

每天快照加异地归档:

bash 复制代码
#!/bin/bash
# /opt/openbao/scripts/backup.sh
BACKUP_DIR="/opt/openbao/backups"
DATE=$(date +%Y%m%d-%H%M%S)

bao operator raft snapshot save "${BACKUP_DIR}/openbao-${DATE}.snap"

aws s3 cp "${BACKUP_DIR}/openbao-${DATE}.snap" \
  "s3://my-backups/openbao/openbao-${DATE}.snap" \
  --sse aws:kms

find "${BACKUP_DIR}" -name "*.snap" -mtime +30 -delete
bash 复制代码
echo "0 3 * * * /opt/openbao/scripts/backup.sh >> /var/log/openbao/backup.log 2>&1" | crontab -

密钥轮换

bash 复制代码
bao operator rotate          # 在线轮换,不影响服务
bao read sys/key-status      # 看当前 term

2.7 修了轮换配置里 disabled 字段更新不持久化的问题。

应用集成

四种接法,按环境选:直接调 API(控制力最强,但要自己管 Token 生命周期)、Agent(虚机/裸机首选)、K8s Injector(注解驱动零侵入)、CSI Provider(标准卷接口)。

Go:

go 复制代码
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    vault "github.com/openbao/openbao/api/v2"
)

func main() {
    config := vault.DefaultConfig()
    config.Address = "https://openbao.mycompany.com:8200"

    client, err := vault.NewClient(config)
    if err != nil {
        log.Fatalf("init client: %v", err)
    }

    resp, err := client.Logical().Write("auth/approle/login", map[string]interface{}{
        "role_id":   "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
        "secret_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
    })
    if err != nil {
        log.Fatalf("login: %v", err)
    }
    client.SetToken(resp.Auth.ClientToken)

    secret, err := client.KVv2("secret").Get(context.Background(), "myapp/config")
    if err != nil {
        log.Fatalf("read secret: %v", err)
    }
    fmt.Printf("DB Host: %s\n", secret.Data["db_host"].(string))

    // 后台续租;2.7 修了 LifetimeWatcher 续租失败不遵守退避间隔的问题
    go func() {
        for {
            time.Sleep(30 * time.Minute)
            if _, err := client.Auth().Token().RenewSelf(3600); err != nil {
                log.Printf("renew failed: %v", err)
            }
        }
    }()

    select {}
}

Python(用 hvac,兼容 OpenBao API):

python 复制代码
import hvac
import os
import threading
import time


class SecretsManager:
    def __init__(self):
        self.client = hvac.Client(
            url=os.environ.get('BAO_ADDR', 'https://openbao.mycompany.com:8200'),
            verify='/etc/openbao/tls/ca.crt'
        )
        self._login()
        self._start_renewal()

    def _login(self):
        result = self.client.auth.approle.login(
            role_id=os.environ['BAO_ROLE_ID'],
            secret_id=os.environ['BAO_SECRET_ID']
        )
        self.client.token = result['auth']['client_token']
        self.lease_duration = result['auth']['lease_duration']

    def get_secret(self, path, key=None):
        response = self.client.secrets.kv.v2.read_secret_version(
            path=path, mount_point='secret'
        )
        data = response['data']['data']
        return data[key] if key else data

    def get_db_credentials(self, role='readonly'):
        response = self.client.read(f'database/creds/{role}')
        return {
            'username': response['data']['username'],
            'password': response['data']['password'],
            'lease_id': response['lease_id'],
        }

    def _start_renewal(self):
        def renew():
            while True:
                time.sleep(self.lease_duration * 0.7)
                try:
                    self.client.auth.token.renew_self()
                except Exception as e:
                    print(f"renew failed: {e}")
                    self._login()
        threading.Thread(target=renew, daemon=True).start()

上生产前对照一遍

基础安全:TLS 启用(可以顺手开 tls_auto_reload);版本不低于 2.7.2;Swap 禁用;Root Token 撤销;Unseal Key 分人分地存放;Auto Unseal 用的是注册好的 KMS 插件且 KMS 密钥有防删除保护。

存储与 HA:Raft 三节点起步、奇数;或者复用现有 PostgreSQL 集群(ha_enabled=true,2.7 起支持读水平扩展);单机用 PebbleDB;Raft 每天 snapshot,PG 后端用数据库自己的备份体系,恢复流程真的演练过。

审计监控:至少一个审计设备;日志进集中式系统;Prometheus 指标和告警都配了。

访问控制:策略最小权限;高危路径套 Control Group;应用走 AppRole 或 K8s Auth;Secret ID 限制使用次数和 TTL;Token TTL 别超过 24h;有"写后立刻读"场景的考虑 X-Vault-Index。

密钥管理:KV 用 v2;数据库凭证用动态生成;定期 bao operator rotate;动态凭证 TTL 压到 1 小时内;有合规压力的可以开始评估 ML-DSA 和 PQC TLS。

架构图

下面是 OpenBao v2.7.2 的整体架构


参考资料