Bitwarden:组织共享、机器 Secret 与退出治理
Bitwarden 把 Password Manager 与 Secrets Manager 放在同一品牌下,但它们不是一套对象模型。人员共享依赖 organization、collection 与 group,机器任务依赖 project、secret、machine account 与 access token;混用个人会话和机器身份,会让所有权、撤权和审计同时失真。
Bitwarden:个人 vault、organization 与 collection 不能混用
Bitwarden 客户端把个人项和组织项显示在一个 Vaults 视图中,但所有权不同。My vault 中的 item 属于个人;移动到 organization 后,item 归组织所有,并至少属于一个 collection。collection 将组织项分组并赋予成员或 group 权限。官方 Collections 说明特别强调:嵌套 collection 只改变显示层级,不继承父 collection 的 item、访问或权限。把 Production 放在 Engineering 下面,不会自动把工程组权限传给生产项,也不会自动隔离它。
folder 是个人整理工具,collection 才是组织共享边界;group 用来批量关联人员与 collection。把同一凭据复制到多人个人 vault 会失去组织所有权与统一更新能力。共享动作应把 item 移入 organization 并选择 collection,而不是通过聊天发送值。移入前确认目标 collection 的人员和权限,因为归属改变后,原创建者可能不再拥有管理权。
Password Manager 和 Secrets Manager 也是不同对象面。Password Manager 的 bw CLI操作 login、secure note、organization item 与 collection;Secrets Manager 的 bws CLI围绕 project、secret、machine account 和 access token。官方已将 Secrets Manager 的 service account 称为 machine account,它代表应用或流水线,并按 project 配置 Can read 或 Can read, write。当前 Secrets Manager CLI 已提供 secret create、secret edit、secret delete 以及 project 写命令,实际读写能力由 access token 对应 machine account 的 project 权限约束;生产读取任务仍应只授予 Can read,把写入与删除交给独立发布身份,并用 bws secret --help、允许写入和只读拒绝两组实验确认安装版本与服务端权限一致。不能给 CI 一个员工的 BW_SESSION,也不能把 organization collection 当成 Secrets Manager project 的别名。
安装 Bitwarden 客户端与两套 CLI
人员设备从官方客户端入口安装桌面应用和浏览器扩展,企业分发要固定服务器区域或自托管 URL、扩展来源、锁定策略和更新通道。Password Manager CLI 可以使用官方原生包;已经受控使用 Node.js 的开发机可按 Bitwarden CLI 文档安装:
npm install -g @bitwarden/cli
Get-Command bw | Format-List Source,Version
bw --version
bw statusbw login 建立登录状态,bw unlock 才生成可解密 vault 的 session key。BW_SESSION 是解密会话材料,不应写进 profile、日志或 CI 变量库;使用结束执行 bw lock 或 bw logout。bw sync 只从服务器拉取加密 vault,客户端创建、编辑或删除则会自动推送。故障时先看 bw status 的服务器 URL、同步时间与 locked/unlocked 状态,避免在错误的自托管实例上反复重置密码。
bw login
$env:BW_SESSION = bw unlock --raw
bw sync
bw status
# 完成实验后:
bw lock
Remove-Item Env:BW_SESSION -ErrorAction SilentlyContinueSecrets Manager CLI 使用 bws,从官方 SDK releases 取得二进制并校验来源,或使用官方容器入口。它用 machine account access token 认证;token 决定能访问哪些 project 与 secret。官方 Secrets Manager CLI提醒 token 可以通过环境变量或参数传入,但命令行参数容易进入进程列表和 shell 历史,团队应使用受保护的环境注入并关闭 trace。
bws --version
bws project list
bws run --help这三个命令只有在 BWS_ACCESS_TOKEN 来自受控临时环境时才构成最小验证。bws secret list 或 bws secret get 可能把值写到标准输出,不能把它们当作探活命令;读取验证应改用受控子进程,只输出成功状态和不敏感版本标签。机器账号的 token 轮换与 Password Manager 主密码变更是两条不同链路,不能互相替代。
bws 可在本机配置目录保存加密状态文件以减少重复认证和速率限制,但状态目录仍是需要盘点、限制权限和离职清理的副本;一次性 runner 可选择不保留状态。机器账号 access token、bws 状态文件与 Password Manager 主密码是三条不同生命周期,不能互相替代。
用 Bitwarden 验证组织所有权与 collection 权限
先在实验 organization 创建 catalog-demo collection,建立只读 group 并加入测试成员。用桌面或 Web 创建一个合成 Login item,选择 organization 与该 collection,用户名填 demo-user,密码由客户端生成。同步 CLI 后,只读取元数据和密码指纹:
$env:BW_SESSION = bw unlock --raw
bw sync
$item = bw list items --search "catalog-demo-api" | ConvertFrom-Json |
Where-Object { $_.organizationId } | Select-Object -First 1
if (-not $item -or -not $item.collectionIds) { throw "organization_item_missing" }
$secret = bw get password $item.id
$hash = [Convert]::ToHexString(
[Security.Cryptography.SHA256]::HashData([Text.Encoding]::UTF8.GetBytes($secret))
).Substring(0,12)
[pscustomobject]@{
itemId = $item.id
organizationOwned = [bool]$item.organizationId
collectionCount = $item.collectionIds.Count
fingerprint = $hash
}
Remove-Variable secret成功输出应显示 organizationOwned: True、至少一个 collection 和短指纹。脚本故意不打印密码。若搜索命中多个同名项,必须改用稳定 item ID;bw get 对非唯一字符串会失败,这恰好能阻止自动化悄悄选择错误凭据。
反向实验从 collection 移除测试成员或 group,在线同步后重新解锁并查询 item。预期是 item 不再出现在组织视图,按旧 ID 读取失败。然后把设备断网再观察:官方 永久移除成员说明指出,离线设备会缓存包含组织项的只读副本,部分客户端在移除后短时间仍可能访问它。这个事实决定了离职动作必须同时轮换成员曾访问的目标凭据;“服务端列表里看不到他”不是旧值失效证据。
清理实验时,在组织端删除合成 item 或移入回收站,确认其他授权成员同步后不再使用;从 group 移除测试账号;执行 bw lock、清除 BW_SESSION,并删除任何导出。删除 collection 前先确认没有仍需保留的 item 和唯一管理者,避免把权限对象当普通文件夹清理。
Bitwarden 的离职链要区分撤销、移除和删除账号。先撤销组织访问可切断服务器端同步,保留调查窗口;确认组织所有的 item、collection 与机器账号已由留任 owner 接管后,再移除成员。成员的个人 Bitwarden 账号不会因移出 organization 而被删除,个人 vault 中的工作凭据也不会自动转为组织所有。离线设备缓存的组织项按已经披露处理,目标端必须轮换。退出平台前分别导出个人 vault 与 organization 数据:明文 CSV/JSON 是敏感副本;加密 JSON 还要区分密码保护型与绑定账号加密密钥的类型,并用独立测试账号实际导入。Password Manager 导出说明指出,只有 JSON 导出包含 passkey、SSH key、card 和 identity;带附件的 ZIP 当前只适用于个人 vault;任何格式都不包含回收站和 Send。Secrets Manager 导出还要单独执行,其 JSON 只包含 project 与 secret,不包含 machine account 和 access token,因此退出前必须另建机器身份、权限与 token 清单。随后清除导出、BW_SESSION、bw 缓存与 bws 状态目录,最后才停用自托管服务或合同账号。
Bitwarden Secrets Manager 承担机器 secret,而不是个人 vault
流水线应使用 machine account 访问 project 中的 secret。创建 catalog-demo project、合成 secret 和只读 machine account,给它仅该 project 的 Can read 权限,再签发 access token。人可以管理 project,作业只读取需要的 secret。官方 Machine Accounts说明,机器账号可按 project 授权,删除机器账号不会删除关联 secret;这让身份撤销和数据删除成为两个独立动作。
steps:
- name: run with project secrets
shell: bash
env:
BWS_ACCESS_TOKEN: ${{ secrets.BWS_ACCESS_TOKEN }}
BWS_PROJECT_ID: ${{ vars.BWS_PROJECT_ID }}
run: |
set +x
bws run --project-id "$BWS_PROJECT_ID" -- node verify-credential.mjs正向证据是作业身份只能列出授权 project,验证脚本得到合成值且日志无明文。反向实验应创建另一个 project,确认同一 token 无法读取;撤销 token 后,新作业认证失败;已经注入到旧进程内存的值不会自动消失,因此还要终止旧作业并轮换目标凭据。删除 machine account 前导出必要的事件证据,盘点所有 token 注入位置,再验证没有消费者仍依赖它。
Secrets Manager 能为机器提供受控静态 secret,但 access token 本身仍是 bootstrap 凭据。Access token 说明显示,创建 token 时默认到期时间是 Never,生产任务必须显式选择有限到期时间,并把到期告警、双 token 切换和旧 token 撤销纳入发布流程;token 值只在创建时显示,Bitwarden 不保存可供再次取回的副本。支持工作负载身份联合的环境应优先使用短期身份换取访问;若只能使用 token,还要限制 runner、环境、分支和网络。个人 Password Manager CLI 的 API key 或 session key 不应成为无人值守服务的长期身份。
用本地合成实验确认撤权不等于旧值失效
没有可用的测试租户或隔离设备时,可以先运行下面的教学模型。它不调用厂商 API,不能代替产品验收;它只演示密码管理工具必须面对的状态不变量:在线读取被阻断后,已下发的离线副本仍可能保留旧值;只有目标端轮换后,旧副本才失去业务效力。脚本只生成随机合成值,只输出 hash 指纹,并在退出前清空临时目录。
// password-vault-lifecycle-lab.mjs
import { createHash, randomBytes } from "node:crypto";
import { mkdtemp, rm, writeFile, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const dir = await mkdtemp(join(tmpdir(), "password-vault-lab-"));
const fp = (value) => createHash("sha256").update(value).digest("hex").slice(0, 12);
const randomSecret = () => `synthetic-${randomBytes(24).toString("base64url")}`;
const authority = {
itemId: "catalog-demo-api",
version: 1,
readers: ["developer", "leaver"],
value: randomSecret(),
};
const target = { acceptedFingerprint: fp(authority.value) };
const readOnline = (actor) => {
if (!authority.readers.includes(actor)) throw new Error("ACCESS_DENIED");
return authority.value;
};
const authenticate = (value) => fp(value) === target.acceptedFingerprint;
try {
const oldValue = readOnline("leaver");
await writeFile(join(dir, "offline-cache.json"), JSON.stringify({ value: oldValue }));
console.log(JSON.stringify({ phase: "positive", online: authenticate(oldValue), fp: fp(oldValue) }));
authority.readers = authority.readers.filter((name) => name !== "leaver");
try {
readOnline("leaver");
throw new Error("revocation_test_did_not_fail");
} catch (error) {
if (error.message !== "ACCESS_DENIED") throw error;
console.log(JSON.stringify({ phase: "revoked", onlineRead: "ACCESS_DENIED" }));
}
const cached = JSON.parse(await readFile(join(dir, "offline-cache.json"), "utf8")).value;
console.log(JSON.stringify({ phase: "offline-copy", oldValueStillWorks: authenticate(cached), fp: fp(cached) }));
authority.value = randomSecret();
authority.version += 1;
target.acceptedFingerprint = fp(authority.value);
console.log(JSON.stringify({
phase: "rotated",
version: authority.version,
oldValueStillWorks: authenticate(cached),
newValueWorks: authenticate(readOnline("developer")),
}));
} finally {
await rm(dir, { recursive: true, force: true });
console.log(JSON.stringify({ phase: "cleanup", removed: true }));
}node .\password-vault-lifecycle-lab.mjs
Remove-Item .\password-vault-lifecycle-lab.mjs预期顺序为:正向读取成功;撤权后在线读取返回 ACCESS_DENIED;离线旧值在轮换前仍可认证;轮换后 oldValueStillWorks 变为 false、newValueWorks 为 true;临时目录清理成功。任何产品的离职流程如果无法给出同等证据,就还没有关闭旧凭据风险。
浏览器、剪贴板与自动填充仍是独立攻击面
Bitwarden 的客户端负责解密与填充,服务端权限正确并不意味着终端没有旁路。浏览器扩展只从官方渠道安装,工作资料与个人 profile 分离,关闭浏览器自带密码保存以避免双写。自动填充前核对协议、完整域名、端口和 iframe;相似域名、HTTP 页面和不受信窗口应拒绝自动填充。复制动作会经过系统剪贴板、远程桌面、跨设备同步和录屏环境,短时清除只是降低暴露窗口,不能撤回已经被其他进程读取的内容。
自动化优先使用该产品的机器身份和进程级注入,交互登录优先使用受控扩展或客户端集成。故障排查不得打印 secret、会话 token 或完整导出内容;只输出稳定对象 ID、版本、权限结果和不可逆短指纹。需要人工共享时,必须设置接收者、最短有效期、owner 和主动删除时点,并把接收者可能已经复制内容纳入轮换判断。
导出、恢复与迁移要验证数据可读性
Bitwarden 导出要区分个人 Vault、Organization 与 Secrets Manager。明文 CSV/JSON、加密 JSON 和附件 ZIP 的字段覆盖、可移植性与解密依赖不同;Secrets Manager 的 project 与 secret 需要单独导出,machine account 和 access token 还要另建清单。回收站、Send 和部分附件不会自然出现在同一份备份里。
恢复演练必须在隔离环境中实际导入,核对条目数、自定义字段、附件、TOTP、SSH key、passkey 与历史版本,记录导出文件 hash、保管人和销毁时点。导入成功也不代表权限模型已经重建;新平台中的人员组、机器身份和最小权限需要单独验收。凡经过明文或降级格式迁移的真实凭据,都应进入轮换队列。
离职与失窃设备按两阶段回收
从 organization 撤销或移除成员只能阻断后续在线同步,离线设备可能保留组织项的只读缓存。离职动作还要检查个人 Vault 中误存的工作凭据、organization 所有权、collection 与 group 关系、Secrets Manager machine account、access token、Send 和自托管实例会话。
第一阶段立即阻断身份、会话、共享链接和机器 token,并隔离失窃设备;第二阶段按“曾经可读”的最大范围盘点目标凭据,逐项轮换或撤销。结案证据至少包括旧账号不能继续同步、旧会话不能新取值、旧值在目标系统被拒绝、新值由留任身份验证成功,以及导出和离线副本已按最坏情况处置。
长期治理与退出条件
Password Manager 的 Organization/Collection 与 Secrets Manager 的 Project/Machine Account 分别建立 owner 和复核表。自托管团队还要负责数据库、对象存储、邮件、TLS、备份、升级与恢复,不能把‘数据在自己服务器’当成已经具备治理能力。
每个授权容器和机器身份都要有业务 owner、技术 owner、复核周期和退出路径。持续指标应观察无 owner 对象、超期共享、长期 token、离职暴露项、旧值拒绝证据缺口和恢复失败,而不是只统计保存了多少密码。停止使用前先冻结写入、完成受控导出与恢复验证、重建权限和自动化,再撤销旧 token、删除客户端配置和不再需要的副本;无法证明旧值失效,就不能把迁移标记为完成。
