Dev Container 中使用 1Password SSH Agent

本文由 Codex 根据一次真实的 Dev Container 排障过程整理生成。

最近在本地用 Dev Container 跑项目时,遇到一个看起来很绕的问题:宿主机已经启用了 1Password SSH Agent,容器里也能看到 SSH_AUTH_SOCK,但 ssh-add -l 仍然失败。

这篇记录一下问题现象、背后的原理,以及一套比较稳的修复方法。

问题现象

最开始 Dev Container 启动时出现过这样的错误:

1
2
3
Container server: Remote to local stream terminated with error: {
message: 'connect ENOENT ~/Library/Group\\ Containers/2BUA8C4S2C.com.1password/t/agent.sock'
}

修了一轮之后,容器里已经能看到 VS Code / Dev Containers 转发出来的 socket:

1
2
root@container:/data/autotest# echo "$SSH_AUTH_SOCK"
/tmp/vscode-ssh-auth-395058ed-3daa-4a0f-b9b8-4c196531f750.sock

但是继续执行:

1
ssh-add -l

却得到:

1
error fetching identities: communication with agent failed

这说明问题已经不是“容器里没有 socket”,而是“容器内的转发 socket 连回宿主机 agent 时失败”。

原理:Dev Container 不是直接使用 macOS 的 agent.sock

在 macOS 上,1Password SSH Agent 的真实 socket 一般在:

1
~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock

但是 Linux 容器不能直接理解这个 macOS 路径。尤其路径里还有空格,很多工具链里如果写成 ~/Library/Group\ Containers/...~ 并不会被展开成 /Users/xxx

Dev Containers 的正常做法不是把这个 macOS socket 原样挂进容器,而是:

  1. VS Code / Dev Containers 在宿主机读取 SSH_AUTH_SOCK
  2. 它在容器里创建一个临时 socket,例如 /tmp/vscode-ssh-auth-xxxx.sock
  3. 容器里的 SSH 客户端连接这个临时 socket。
  4. Dev Containers 再把请求转发回宿主机的真实 SSH Agent。
  5. 1Password 弹窗授权或返回 key 列表。

所以容器里看到的路径应该是:

1
/tmp/vscode-ssh-auth-xxxx.sock

而不应该是:

1
/Users/xxx/Library/Group Containers/...

也不应该是:

1
~/Library/Group Containers/...

如果容器里已经出现 /tmp/vscode-ssh-auth-xxxx.sock,说明 Dev Containers 的转发层已经建起来了。此时 communication with agent failed 往往表示 VS Code / Dev Containers 进程拿到的宿主机 SSH_AUTH_SOCK 是错的、旧的,或者对应的 1Password agent 当前不可用。

修复方法

1. 在宿主机创建一个稳定的短路径

1Password 的原始路径包含空格,不适合到处配置。建议在宿主机建立一个稳定软链接:

1
2
mkdir -p "$HOME/.1password"
ln -sf "$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" "$HOME/.1password/agent.sock"

这样之后统一使用:

1
/Users/<username>/.1password/agent.sock

路径更短,也没有空格。

2. 验证 1Password Agent 本身可用

在 macOS 宿主机终端执行:

1
SSH_AUTH_SOCK="$HOME/.1password/agent.sock" ssh-add -l

如果这里失败,说明还没到 Dev Container 层,应该先检查 1Password:

  • 打开 1Password。
  • 进入 Settings -> Developer
  • 确认 Use the SSH Agent 已开启。
  • 确认对应 SSH key 已经加入 1Password。

宿主机这一步必须先成功。

3. 配置 SSH_AUTH_SOCK

如果平时从终端启动 VS Code / Codex,最自然的做法是在 ~/.zshrc 里配置:

1
2
echo 'export SSH_AUTH_SOCK="$HOME/.1password/agent.sock"' >> "$HOME/.zshrc"
source "$HOME/.zshrc"

然后验证:

1
2
echo "$SSH_AUTH_SOCK"
ssh-add -l

如果你是从 Dock、Finder、Spotlight 这类 GUI 入口启动 VS Code / Codex,需要额外注意:GUI App 不一定继承当前终端里的环境变量。即使你在 shell 里配置了 export SSH_AUTH_SOCK=...,GUI 进程也可能拿不到。

这时可以把变量写入 macOS 的 launchd 环境:

1
2
3
launchctl unsetenv SSH_AUTH_SOCK
launchctl setenv SSH_AUTH_SOCK "$HOME/.1password/agent.sock"
launchctl getenv SSH_AUTH_SOCK

最后一行应该输出类似:

1
/Users/baymax/.1password/agent.sock

注意,输出里不能有 ~

如果你始终从终端启动,比如:

1
code .

或者从终端启动 Codex,那么只配置 ~/.zshrc 通常就够了。

4. 重启 VS Code / Codex

这一步很关键。

如果 VS Code / Codex 已经启动,它已经读到了旧环境变量。即使你后来 launchctl setenv,当前进程也不会自动更新。

所以需要:

  1. 完整退出 VS Code / Codex。
  2. 重新打开项目。
  3. Rebuild / Reopen Dev Container。

5. 容器内验证

进入容器后执行:

1
2
3
echo "$SSH_AUTH_SOCK"
ls -l "$SSH_AUTH_SOCK"
ssh-add -l

正常情况下,第一条命令应该输出类似:

1
/tmp/vscode-ssh-auth-xxxx.sock

然后:

1
ssh-add -l

应该能列出 1Password 中的 SSH key,或者触发 1Password 授权。

最后可以测试 GitHub:

1
ssh -T git@github.com

如果配置正确,会看到 GitHub 的 SSH 认证提示。

不建议在 devcontainer.json 里直接挂载 1Password socket

一种看似直接的做法是把宿主机的 1Password socket mount 到容器里:

1
2
3
4
5
6
7
8
{
"mounts": [
"source=${localEnv:SSH_AUTH_SOCK},target=/ssh-agent,type=bind"
],
"remoteEnv": {
"SSH_AUTH_SOCK": "/ssh-agent"
}
}

这在某些 Linux 宿主机上可行,但在 macOS + Docker Desktop / Colima 这类环境里并不稳。

原因是容器实际运行在 Linux VM 里,不是直接运行在 macOS 内核上。macOS 的 Unix socket 不一定能像普通文件一样被正确转发到 VM 再进入容器。路径里还有空格和 ~ 展开问题,排障成本会更高。

更稳的方案是让 Dev Containers 自己做 SSH agent forwarding。也就是:

  • 宿主机 SSH_AUTH_SOCK 指向真实可用的 1Password agent。
  • GUI App 从 launchd 环境拿到正确值。
  • 容器内使用 Dev Containers 自动生成的 /tmp/vscode-ssh-auth-xxxx.sock

检查 ~/.ssh/config

还有一个容易忽略的坑:~/.ssh/config 里的 IdentityAgent

如果你写了:

1
2
Host *
IdentityAgent "~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock"

容器内 SSH 客户端会尝试直接访问这个 macOS 路径。这个路径在容器里不存在,也不会自动展开。

推荐改成:

1
2
Host *
IdentityAgent SSH_AUTH_SOCK

或者干脆去掉 IdentityAgent,让 SSH 使用环境变量里的 SSH_AUTH_SOCK

如果只想对 GitHub 使用 1Password agent,可以写得更收敛:

1
2
3
4
Host github.com
HostName github.com
User git
IdentityAgent SSH_AUTH_SOCK

最终检查清单

宿主机:

1
2
3
test -S "$HOME/.1password/agent.sock"
SSH_AUTH_SOCK="$HOME/.1password/agent.sock" ssh-add -l
launchctl getenv SSH_AUTH_SOCK

容器内:

1
2
3
4
echo "$SSH_AUTH_SOCK"
test -S "$SSH_AUTH_SOCK"
ssh-add -l
ssh -T git@github.com

预期结果:

  • 宿主机 SSH_AUTH_SOCK 是绝对路径。
  • 容器内 SSH_AUTH_SOCK/tmp/vscode-ssh-auth-xxxx.sock
  • ssh-add -l 能列出 1Password 管理的 key。
  • ssh -T git@github.com 能触发或通过 SSH 认证。

小结

这个问题的关键判断点是:

  • ENOENT ~/Library/.../agent.sock:宿主机 agent 路径没有被正确展开。
  • 容器里没有 SSH_AUTH_SOCK:Dev Containers 没有建立 SSH agent forwarding。
  • 容器里有 /tmp/vscode-ssh-auth-xxxx.sock,但 communication with agent failed:转发层存在,但 VS Code / Codex 背后的宿主机 agent 不可用或路径仍是旧值。

最终不要把 macOS 的 1Password socket 当成普通文件直接塞进容器。让宿主机提供一个稳定、绝对、无空格的 SSH_AUTH_SOCK,再让 Dev Containers 负责转发,才是更省心的方式。✨