# 2023-21: 我的 1Password 密钥管理实践

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

> 这些实践应当同样适用于 [bitwarden](https://bitwarden.com/) 等服务。

## 密码管理

常见的 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 都会弹出如下窗口：

![](https://xuanwo.io/reports/2023-21/auth-prompt-touch-id.png)

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

### 配置 SSH Agent

使用这个功能首先需要正确配置 [1Password SSH Agent](https://developer.1password.com/docs/ssh/agent)。

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

![](https://xuanwo.io/reports/2023-21/ssh-agent.png)

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

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

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

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

或者为其指定一个 key：

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

最后设置 `SSH_AUTH_SOCK` 即可：

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

### 迁移 SSH Key

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

![](https://xuanwo.io/reports/2023-21/github-ssh-key.png)

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

## Shell 集成

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

以 Github Plugin 为例，本地的 `op/plugins.sh` 内容如下：

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

而对应的具体实现为：

```go
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 集成同样十分简单：

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

所有的 shell plugin 都开源在 [shell-plugins](https://github.com/1Password/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](https://github.com/apache/incubator-opendal) 项目就遇到了这些问题：

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

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

### 申请 1Password 开源赞助

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

### 设置 1Password Services Account

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

### 配置 Github Actions

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

```yaml
- 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-credentials](https://github.com/aws-actions/configure-aws-credentials) 和 [google-github-actions/auth](https://github.com/google-github-actions/auth) 等 actions 实现基于 GitHub OIDC 的无密钥认证
- 所有配置进入 Vault 的密钥需遵循最小化原则，能且只能访问指定资源，比如 AWS 资源需配置 IAM 子账户并通过 role 限制权限

## 总结

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