在日常工作中,我经常需要登录堡垒机。传统的登录方式需要手动输入密码和一次性密码(OTP),操作繁琐且效率低下。即使改为 SSH 密钥登录,也还是需要输入一次性密码,只能算是半自动化登录。每天登录个几次下来,就有点烦人了。
最近,我在许久没打开过的涛叔博客上,看到了他最新发布的文章:《自动化登录堡垒机》。在文章中,他通过编写一个脚本模拟输入密码来实现自动化登录堡垒机。
这让我意识到,是时候将我的半自动化登录流程升级为全自动化了。
方案一:使用 1Password CLI
首先,需要安装 1Password CLI:
# macOS
brew install 1password-cli
# 其它系统
# https://developer.1password.com/docs/cli/get-started/#step-1-install-1password-cli
接着打开 1Password 的设置,启用「与 1Password CLI 集成」:
完成配置后,就可以在终端访问 1Password 了,下面是一些常用的命令:
# 获取保险库列表
op vault list
# 获取项目列表
op item list
# 获取项目信息
op item get {id/name}
# 获取项目信息(包含密码)
op item get {id/name} --reveal
# 获取 JSON 格式的项目信息(包含密码)
op item get {id/name} --reveal --format json
# 获取项目的一次性密码
op item get {id/name} --otp
除了 expect
和 op
之外,还需要安装 jq 工具,用于解析 JSON,用来从 1Password 的项目信息中提取出密码和一次性密码。
# macOS
brew install jq
# Debian/Ubuntu
sudo apt-get install jq
# RedHat/CentOS
yum install jq
对原始脚本的改动很简单,只需要将脚本中的「自动读取动态口令」和「硬编码的密码」改为使用上述命令获取即可。
#!/usr/bin/expect
# 自动调整窗口大小
trap {
set XZ [stty rows ]
set YZ [stty columns]
stty rows $XZ columns $YZ < $spawn_out(slave,name)
} WINCH
set OP_ITEM_ID "Bastion-Host-Login" # 使用项目名称
# 使用 1Password CLI 获取堡垒机的项目信息
if {[catch {exec op item get ${OP_ITEM_ID} --reveal --format json} data]} {
puts "错误: 无法获取1Password数据,请确保已登录op CLI"
exit 1
}
# 从 JSON 中提取密码
if {[catch {exec echo $data | jq -r {.fields[] | select(.id == "password") | .value}} password]} {
puts "错误: 无法解析密码"
exit 1
}
# 从 JSON 中提取一次性密码
if {[catch {exec echo $data | jq -r {.fields[] | select(.type == "OTP") | .totp}} totp]} {
puts "错误: 无法解析TOTP"
exit 1
}
# 发起 ssh 会话
spawn ssh foo@example.zz.ac
# 自动输入登录密码
expect "Password:"
send "$password\r"
# 自动输入动态口令
expect "Verification code:"
send "$totp\r"
# 将终端控制权交还给 ssh 会话,完成登录
interact
在上面的脚本中,使用了 catch
命令用来捕获异常,类似于编程语言中的 try-catch
。如果命令执行失败,catch
会返回非零值(通常是 1),命令执行成功,catch
会返回 0,并将命令执行结果保存到 result 中。
在使用脚本前,你需要将 OP_ITEM_ID
的值替换成项目的实际 ID 或者名称。可以使用以下命令获取:
# 列出所有项目,找到堡垒机相关的项目
op item list
# 或者搜索特定名称
op item list | grep "堡垒机"
例如:
set OP_ITEM_ID "Bastion-Host-Login" # 使用项目名称
# 或者
set OP_ITEM_ID "abc123def456" # 使用项目ID
1Password CLI 的性能问题
原本以为到这里就结束了,但是在实际使用过程中我发现了一个问题:这个脚本非常慢!慢到什么程度呢?在不需要认证的情况下,从执行命令到登录成功,需要 3 秒左右,最慢的时候差不多需要 10 秒。
自动化的目的是为了提升效率,现在操作虽然自动化了,但时间效率没有提升多少,跟手动登录差不多,这显然无法满足需求。
研究了一会儿,发现问题出在 op
命令获取密码太慢了,不仅获取密码慢,获取一次性密码也慢。我尝试过指定保险库、使用 --fields
选项减少获取的字段,没什么效果。
Google 搜索发现很早之前就有这个问题,但都没有得到解决。
- 1password cli v 2.6.0 is still slow.
- CLI is so/too slow
- CLI commands are very slow
- Why is the CLI so slow? Any tips?
官方回复说在 macOS 上会默认开启缓存来提升执行速度,在命令中加上 --debug
选项,确实可以看到使用了缓存,并且也命中了缓存。但是,为什么还这么慢?
op item get xxxx --reveal --debug
23:19PM | DEBUG | Session delegation enabled
23:19PM | DEBUG | NM request: NmRequestAccounts
23:19PM | DEBUG | NM response: Success
23:19PM | DEBUG | NM request: NmRequestAccounts
23:19PM | DEBUG | NM response: Success
23:19PM | DEBUG | InitDefaultCache: successfully initialized cache
23:19PM | DEBUG | EncryptedKeysets: Cache hit on keyset
23:19PM | DEBUG | AllVaults: cache hit on vault xxxxxxxxxxxxxxxxx
23:19PM | DEBUG | AllVaults: cache hit on vault xxxxxxxxxxxxxxxxx
23:19PM | DEBUG | AllVaults: cache hit on vault xxxxxxxxxxxxxxxxx
23:19PM | DEBUG | VaultItems: cache hit on vault items of vault xxxxxxxxxxxxxxxxx
23:19PM | DEBUG | VaultItems: cache hit on vault items of vault xxxxxxxxxxxxxxxxx
23:19PM | DEBUG | VaultItems: cache hit on vault items of vault xxxxxxxxxxxxxxxxx
23:19PM | DEBUG | VaultItems: cache hit on vault items of vault xxxxxxxxxxxxxxxxx
23:19PM | DEBUG | Item: VaultItems cache hit for vault xxxxxxxxxxxxxxxxx - validating staleness using item version
23:19PM | DEBUG | Item: cache hit on item xxxxxxxxxxxxxxxxx of vault xxxxxxxxxxxxxxxxx
折腾无果后,既然 1Password CLI 的性能问题无法解决,不如换个思路:用 SSH 密钥解决认证问题,用独立的 2FA 工具处理 OTP。
方案二:使用 SSH 密钥 + 2FA
1Password 还提供了另一项功能:1Password SSH Agent。你可以将本地的 SSH 密钥保存到 1Password 上。每次需要使用 SSH 密钥时,都会弹出一个 1Password 的授权框,上面显示了哪个客户端正在请求使用哪个 SSH 密钥,你可以使用指纹或者密码同意这次授权。
这样带来的好处是:
- 本地不用再存储任何私钥文件,避免了恶意扫描导致私钥被泄露的风险
- 基于 1Password 的同步功能,可以在任何登录了 1Password 账号的设备上使用这些 SSH 密钥
为了使用这项功能,你需要在 1Password 的设置中启用「使用 SSH Agent」。
配置 1Password SSH Agent
首先,在 1Password 上创建一个 SSH 密钥项目,你可以选择将堡垒机的密钥导入进来,也可以生成一个新的私钥。
创建完成后,将公钥保存到 ~/.ssh/bastion.pub
,避免 SSH 密钥过多,产生 Too many authentication failures 问题。
然后在 ~/.ssh/config
文件中添加以下内容:
Host bastion
HostName example.zz.ac # 替换为堡垒机的地址
Port 22 # 替换为堡垒机的端口
IdentitiesOnly yes
IdentityFile ~/.ssh/bastion.pub
IdentityAgent "~/.1password/agent.sock"
这时候在终端中执行 ssh bastion
命令,你就可以免密登录到堡垒机了,但是还需要输入一次性密码,接下来我们解决这个问题。
配置 2FA 工具
执行以下命令安装 2fa 并添加一次性密码。
# 安装
go install rsc.io/2fa@latest
# 添加一次性密码
2fa -add bastion
2fa key for bastion:
# 获取一次性密码
2fa bastion
211762
我对脚本做了一些优化,增加了超时处理、登录后执行命令等:
#!/usr/bin/expect
# 自动调整窗口大小
trap {
set XZ [stty rows ]
set YZ [stty columns]
stty rows $XZ columns $YZ < $spawn_out(slave,name)
} WINCH
# 自动读取动态口令
spawn 2fa bastion
expect -re "(.*)\n"
set totp $expect_out(1,string)
# 发起 ssh 会话
spawn ssh example.zz.ac
# 等待 OTP 提示并自动输入
expect {
"Verification code:" {
send "$totp\r"
}
timeout {
puts "超时: 未收到 OTP 提示"
exit 1
}
}
# 等待登录成功(通常是 shell 提示符)
expect {
"*$" { }
"*#" { }
"*>" { }
timeout {
puts "登录超时"
exit 1
}
}
# 你可以在登录成功后执行一些命令
# send "ls -l\r"
# 将终端控制权交还给 ssh 会话,完成登录
interact
将上面脚本保存到文件中,就可以愉快的登录堡垒机了。实测下来,响应速度基本在 1 秒左右,相比之前的 3-10 秒有了非常大的提升。更重要的是,整个流程的可靠性也更好了。
总结
虽然最终没能用上 1Password CLI 有点遗憾(主要是太慢了),但好在找到了替代方案,日常使用体验还是很不错的。期待 1Password 官方后续能优化一下 CLI 的性能吧。