Windows 上用 paramiko 做免交互 SSH 自动化:一次真实的踩坑与改造
最近要给一批机器做批量巡检,第一步就卡住了:Windows 自带的 OpenSSH 客户端没法在脚本里自动填密码。
## 一、为什么不用 ssh 命令
`ssh.exe` 从 `/dev/tty` 读取密码,不是 stdin。所以下面这种写法一定失败:
```powershell
"mypassword" | ssh root@10.0.0.1 "uptime"
```
常见绕法有三条:装 `sshpass`(Windows 上没有官方版本)、用 PuTTY 的 `plink -pw`(需要额外下载)、或者上 Python 的 paramiko。我选了第三条,因为不想引入新的可执行文件。
## 二、第一次安装就翻车
```powershell
python -m pip install --target D:\work\_libs paramiko
```
装完 import 直接报错:
```
ImportError: DLL load failed while importing _rust:
找不到指定的程序。
```
原因是新版 paramiko 依赖 cryptography 43+,而 cryptography 41 之后把 OpenSSL 绑定换成了 Rust 实现(`_rust.pyd`),这个二进制对 Windows 运行库版本有要求,老一点的 Python 3.9 环境上容易加载失败。
### 解法:退回纯 C 实现的版本
cryptography 3.4.x 还是 CFFI + OpenSSL 的老架构,没有 Rust 依赖:
```powershell
python -m pip install --target D:\work\_libs ^
"cryptography==3.4.8" ^
"paramiko==2.11.0" ^
"bcrypt==3.2.0" ^
"pynacl==1.5.0"
```
`--target` 很关键:所有包装进独立目录,不污染全局 site-packages,用完删掉目录就干净了。脚本里手动加路径:
```python
import sys
sys.path.insert(0, r"D:\work\_libs")
import paramiko
print(paramiko.__version__) # 2.11.0
```
## 三、密码不要进代码,也不要进日志
最容易犯的错是把密码写进脚本或者当命令行参数传:
```powershell
# 反例:密码会出现在进程列表、命令历史、CI 日志里
python deploy.py --password mypassword
```
改成「密码只存在于一个文件,由子进程自己读」:
```python
import sys
sys.path.insert(0, r"D:\work\_libs")
import paramiko
secret_file, host, user, cmd = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
with open(secret_file, "r", encoding="utf-8-sig") as f:
pwd = f.read().strip()
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
c.connect(host, port=22, username=user, password=pwd,
timeout=20, look_for_keys=False, allow_agent=False)
finally:
del pwd
stdin, stdout, stderr = c.exec_command(cmd, timeout=90)
print(stdout.read().decode("utf-8", "replace"))
c.close()
```
调用方只传文件路径:
```powershell
python ssh_run.py D:\work\.secrets\host-a.txt 10.0.0.1 root "ss -tlnp"
```
这样密码不出现在 `argv`、不出现在日志、也不出现在任何输出里。`utf-8-sig` 是为了吃掉记事本存文件时加的 BOM,不加会多一个不可见字符导致认证失败——这个坑很隐蔽,报错只有一句 `Authentication failed`。
## 四、连不上时先分清是「网络」还是「认证」
`Authentication failed` 这个报错信息量太少。可以先只做 SSH 握手,把服务器支持的认证方式列出来:
```python
import socket, paramiko
sock = socket.create_connection((host, 22), timeout=15)
t = paramiko.Transport(sock)
t.start_client(timeout=20)
print("banner:", t.remote_version)
try:
t.auth_none("root")
except paramiko.BadAuthenticationType as e:
print("allowed:", e.allowed_types) # 例如 ['publickey', 'password']
```
`auth_none` 一定失败,但失败时抛出的异常里带着 `allowed_types`,一眼就能看出服务器到底支不支持密码登录、还是只允许密钥。这比反复试密码快得多,也不会因为多次失败触发 fail2ban。
顺带还能拿到 `remote_version`,比如 `SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.16`,直接确认对端系统。
## 五、几个实用细节
1. **`look_for_keys=False, allow_agent=False`**:不关掉的话 paramiko 会先去翻 `~/.ssh` 和 ssh-agent,机器上恰好有别的 key 时会绕一大圈才落到密码认证,慢且容易误用错 key。
2. **`banner_timeout` / `auth_timeout`** 单独设:跨公网时默认超时经常不够,表现是连接卡住而不是报错。
3. **`AutoAddPolicy`** 只适合内网或一次性脚本;生产环境应该预置 `known_hosts`,否则等于放弃中间人防护。
4. **认证成功后再 `del pwd`**:减少密码在内存里的存活时间,虽然不能彻底防内存 dump,但至少不会被顺手打印出来。
## 六、小结
在 Windows 上做 SSH 自动化,paramiko 仍然是成本最低的方案,代价是要处理好依赖版本。真正值得记住的是两条:
- 依赖装到独立目录,别污染全局环境;
- 密码走文件、由子进程读取,永远不进命令行参数和日志。
这两条做到了,剩下的就是普通的 Python 编程。
- 点赞
- 收藏
- 关注作者
评论(0)