这期周报分享我的 1Password 密钥管理实践,主要内容包括常见的密码管理,SSH/Shell 集成和 CI/CD 应用。

这些实践应当同样适用于 bitwarden 等服务。

密码管理

常见的 SaaS 服务密码管理直接使用 1Password 跨平台客户端创建即可,我个人会遵循如下原则:

每个网站都使用随机生成的唯一密码

1Password 默认使用 Smart Password 来根据网站的要求来生成,不满意的话可以自行微调,比如生成 32 位带大小写字母,数字和符号的密码。

两步验证应开尽开

两步验证是保护账号安全十分有效的手段,如果网站本身支持,应当尽可能开启。

我个人更喜欢 TOTP (Time-based one-time password) 而不是基于短信的两步验证方案:

  • TOTP 是纯离线计算,不需要等待接收短信,速度更快
  • TOTP 能够更好的与登陆流程集成,比如 1Password 会在登陆成功后自动复制 code, 部分支持的网站中还会直接自动填充,体验更顺畅
  • TOTP 不依赖手机,在手机不在身边或者遗失时也能正常工作

1Password 的浏览器插件集成了 Scan QR Code 功能,在开启两步验证时直接扫码即可。

密保问题和答案也是密码

现在还需要设置密保问题和答案的服务已经不多了,但是如果需要设置的话,请将他们视作密码的一部分:不要使用真实的回答,将问题原文记录下来,并使用密码生成器生成随机字符串作为答案。

SSH 集成

1Password 在上一轮融资之后大刀阔斧地开发了一系列开发者工具,SSH 集成就是其中之一。

对我来说,它能解决如下问题:

  • 本地无需再存储 SSH Secret Key,避免了密钥泄漏,也避免了被恶意软件读取的可能
  • 在机器出现故障或者需要迁移时,不需要考虑密钥同步,避免了密钥丢失
  • 所有对 SSH Key 的访问都需要经过 1Password 显式的弹窗请求,从而避免恶意软件在后台静默访问

它的思路非常简单:开发一个 SSH Agent,对外暴露为 SSH_AUTH_SOCK,这样所有 SSH Key 请求都由本地 1Password 客户端来处理。

配置完成后,所有应用首次访问指定的 SSH Key 都会弹出如下窗口:

该窗口会展示应用 X 尝试访问 SSH Key Y,只有在认证通过后才能正常获取到 key 的内容。根据各自系统的配置不同,支持 Apple Touch ID,Windows Hello 等生物验证,在 Linux 平台上还提供了完整的 polkitPAM 支持,会调用系统内置的用户认证机制提供原生体验。

配置 SSH Agent

使用这个功能首先需要正确配置 1Password SSH Agent

在 1Password 客户端的配置中勾选并开启 SSH Agent:

在 ssh config 增加如下配置以指定 IdentityAgent

Host *
  IdentityAgent ~/.1password/agent.sock

根据实际的情况,可以对不同的 Host 增加一些额外配置,比如指定 Host 不使用 1Password:

Host ec2-server
  HostName 1.2.3.4
  User ec2-user
  IdentityFile ~/.ssh/ssh-key-not-on-1password.pem
  IdentityAgent none

或者为其指定一个 key:

Host github.com
    User git
    IdentitiesOnly yes
    IdentityFile ~/.ssh/github.pub

最后设置 SSH_AUTH_SOCK 即可:

export SSH_AUTH_SOCK=~/.1password/agent.sock

迁移 SSH Key

完成 SSH 配置之后,我们就可以开始迁移自己的 SSH Key 了。目前 1Password 支持 Ed25519RSA (2048-bit, 3072-bit, and 4096-bit) 两种 key,点击 New Item,选择 SSH Key,然后将 Private Key 添加进去就行。Private Key 添加完毕后 1Password 会自动生成一系列信息以供区分(体验满分!):

配置完毕后可以使用 git fetch 等命令来验证~

Shell 集成

除了 SSH 集成之外,1Password 还基于他们的 CLI 开发了 shell-plugins。它解决的问题与 SSH 集成是类似的:很多开发者本地电脑上都以静态方式存储着 AWS/GCP/Github 等服务的密钥,万一泄漏的话可能就会导致测试环境甚至生产环境受到影响。思路同样非常简单,1Password 负责存储实际的密钥,而 1Password Plugin 是一层简单的 wrapper,将 GITHUB_TOKEN 等环境变量替换为实际的密钥,然后再调用对应的命令。

以 Github Plugin 为例,本地的 op/plugins.sh 内容如下:

export OP_PLUGIN_ALIASES_SOURCED=1
alias gh="op plugin run -- gh"

而对应的具体实现为:

func GitHubCLI() schema.Executable {
	return schema.Executable{
		Name:    "GitHub CLI",
		Runs:    []string{"gh"},
		DocsURL: sdk.URL("https://cli.github.com"),
		NeedsAuth: needsauth.IfAll(
			needsauth.NotForHelpOrVersion(),
			needsauth.NotWithoutArgs(),
		),
		Uses: []schema.CredentialUsage{
			{
				Name: credname.PersonalAccessToken,
			},
		},
	}
}

配置 Shell 集成同样十分简单:

# Signin op to make sure op itself works
op signin
# Setup plugin gh
op plugin init gh

所有的 shell plugin 都开源在 shell-plugins,使用 Golang 开发,如果有想支持的命令行工具欢迎提交 PR!

CI/CD 应用

最后想分享的是 1Password 在 CI/CD 中的应用。Github Actions 提供了内置的 Secrets 功能,但是有如下问题:

  • Secrets 要求 Admin 权限,但是有时候项目的维护者可能只有 Committer 权限(比如 ASF 项目增加修改 Secrets 都需要向 Infra 团队提交工单)
  • Secrets 只能 per repo 或者 per org 配置,在跨多个 repo 写作时配置复杂且麻烦
  • Secrets 只能在 Github 平台中使用,无法支撑本地开发和调试等场景

OpenDAL 项目就遇到了这些问题:

  • OpenDAL 中大量服务需要配置 Secrets,其中部分 Secrets 还需要进行周期轮换,通过 ASF Infra 团队来修改不仅等待时间长,而且大幅度增加了对方的工作量
  • OpenDAL 的 Committer 调试服务时同样依赖 Secrets,但是很难保证 Secrets 在传递过程中的安全和同步

为此,OpenDAL 团队采用了基于 1Password 的密钥管理方案。

申请 1Password 开源赞助

感谢 1Password 对开源项目的大力支持,只需要向 1password-teams-open-source 提交 PR 即可获取免费的 1Password Teams membership。OpenDAL 提交的 PR 参见:Add Apache OpenDAL (incubating)。OpenDAL 创建了一个独立的 Services Vault 并将权限赋予给所有的 PPMC 和 Committers,这样确保了团队内部对所有 Secrets 访问的一致性,消除了对 @Xuanwo 的单点依赖。

设置 1Password Services Account

在最近的更新中,1Password 增加了 Service Account 功能,支持授权该账户访问特定的 Vault。我们向 Infra team 提交了开通 1Password/load-secrets-action 并设置 OP_SERVICE_ACCOUNT_TOKEN 的申请。到这里,我们的 Github 环境已经可以正常从 CI 中获取 1Password 存储的密钥了。

配置 Github Actions

接下来我们只需要使用 load-secrets-action 来加载密钥即可,以 OpenDAL 配置 COS 服务为例:

- name: Load secret
  id: op-load-secret
  uses: 1password/load-secrets-action@v1
  with:
    export-env: true
  env:
    OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
    OPENDAL_COS_TEST: op://services/cos/test
    OPENDAL_COS_BUCKET: op://services/cos/bucket
    OPENDAL_COS_ENDPOINT: op://services/cos/endpoint
    OPENDAL_COS_SECRET_ID: op://services/cos/secret_id
    OPENDAL_COS_SECRET_KEY: op://services/cos/secret_key

- name: Test
  shell: bash
  working-directory: core
  run: cargo test cos -- --show-output
  env:
    RUST_BACKTRACE: full
    RUST_LOG: debug
  • export-env 表示将这些密钥导出为环境变量
  • OP_SERVICE_ACCOUNT_TOKEN 是我们之前配置好的 service account token
  • op://services/cos/test 表示使用 services vault 的 cos 条目中的 test 字段值来替换

需要注意的是,该方案同样依赖静态的 OP_SERVICE_ACCOUNT_TOKEN 密钥,如果泄漏可能导致整个 vault 中的密钥泄漏,所以:

  • 如果场景不是像 OpenDAL 这样的复杂,请使用 configure-aws-credentialsgoogle-github-actions/auth 等 actions 实现基于 GitHub OIDC 的无密钥认证
  • 所有配置进入 Vault 的密钥需遵循最小化原则,能且只能访问指定资源,比如 AWS 资源需配置 IAM 子账户并通过 role 限制权限

总结

本文分享了我在日常工作和生活中使用 1Password 的实践经验,希望能对大家设计自己的密钥管理方案提供一些借鉴!