前言

因为新买的笔记本人脸识别很准很方便,就想折腾一下看能不能用Windowshello来免密登录ssh,没想到居然可以!记录一下这个配置小成就。

我的核心诉求其实很明确,就三点:

  1. 免密:用 Windows Hello(指纹/人脸)直接 SSH 登录路由。
  2. 所有服务器都用一套密钥:方便管理。
  3. 安全:私钥丢了或者被盗了别人也用不了。

alt text 本以为是个简单的 ssh-agentssh-keygen 命令就能解决的事,没想到硬生生踩到了底层的 TPM 硬件兼容性大坑。特此记录,供遇到类似问题的朋友参考。

❌ 踩坑记录:那些看似可行实则行不通的方案

坑一:使用原生 ssh-agent 服务(权限不够)

一开始想着复用现有的 id_rsa,交给 Windows 的 ssh-agent 服务托管。结果运行 Get-Service ssh-agent | Set-Service -StartupType Automatic 直接报错 “拒绝访问”原因:普通用户无权修改系统全局服务的启动类型,即使勉强用伪装进程拉起,体验也不够优雅。

坑二:尝试 ed25519-sk 与“Passkey 冲突”

既然服务走不通,那就走 FIDO2 硬件安全密钥。执行了 ssh-keygen -t ed25519-sk。 结果系统提示 Overwrite (y/n)?,同时我非常担心这会向 TPM 的“驻留密钥(Resident Key)”槽位写入数据,把我辛辛苦苦攒的网站 Passkey 给顶掉。吓得我赶紧 Ctrl+C 止损。

坑三:Windows Hello 消失,让我用物理安全密钥

alt text

后来弄清楚了,只要不加 -O resident 参数,默认就是“非驻留”模式,不占用 TPM 空间。于是放心大胆地再次执行,结果弹出的 Windows 安全中心窗口里,根本没有“Windows Hello(此设备)”的选项,只能选手机或 USB 安全密钥!

破案:这其实是底层硬件的锅。目前绝大多数 PC 主板的 TPM 2.0 芯片,硬件级别只原生支持 ECDSA (NIST P-256) 曲线,不支持 Ed25519 曲线。所以当 SSH 客户端向 TPM 请求 Ed25519 签名时,TPM 直接表示“干不了”,Windows 就把本地设备的选项给隐藏了。


搞懂了底层逻辑,解决起来就是一行命令的事。既然主板 TPM “挑食”,那我们把加密算法换成它认识的 ecdsa-sk 即可。

1. 生成硬件保护的普通凭证

在 PowerShell(普通权限即可)中执行:

ssh-keygen -t ecdsa-sk -f $env:USERPROFILE\.ssh\happy_hello
  • 敲回车后:熟悉的 Windows Hello 窗口终于弹出来了!直接刷脸或按指纹。
  • 提示输入密码 (passphrase) 时直接回车留空。因为此时你的生物特征和主板 TPM 就是最高级别的物理密码。
  • 安全性原理解析:因为没有加 -O resident,这就只是一个 “非驻留密钥”。文件里存的只是一个被 TPM 锁死的“句柄”,不往 TPM 里面写任何持久化数据,百分之百不会影响你的 Passkey。即使这个私钥文件被黑客偷走,没有你主板上的 TPM 芯片和你的脸,它就是一堆废纸。

alt text

2. 将公钥发送到路由器

将生成的 happy_hello.pub 内容追加到路由器的授权列表中: 交给后面的Python脚本来搞

3. 配置快捷登录

为了不用每次都敲那么长的路径,编辑一下 ~/.ssh/config 文件:

Host router
    HostName 192.168.8.1
    User root
    IdentityFile ~/.ssh/happy_hello

大功告成! 以后只要在终端输入 ssh router,系统就会丝滑地弹出指纹/人脸验证框,看一眼屏幕直接进路由。

插曲:一次经典的“把自己锁在门外”

在折腾配置的时候,我犯了一个低级但经典的运维错误:openwrt安装 zsh 默认在/usr/bin/下,但我顺手把 /etc/passwd 里 root 的默认 Shell 改成了 /bin/zsh。

结果就是:哪怕免密配得再完美,SSH 连接也会在建立会话的最后一刻被系统一脚踢开(直接报错断开)。最后不得不走 LuCI 网页后台的启动脚本,写了个 sed -i ’s|/bin/zsh|/bin/ash|g’ /etc/passwd 把命救了回来。 经验教训:改 passwd 之前,千万记得先 which zsh 确认一下路径!

🚀 Python 自动化公钥注入脚本

为了以后给 Homelab 里的其他节点快速配置 Windows Hello 免密,我顺手写了个 Python 脚本。 这个脚本巧妙地利用了 subprocess 和 stdin 管道流传数据,避开了繁琐的转义字符,同时内置了 OpenSSH 强迫症般的 700/600 权限配置。

push_key.py 源码如下:

Python
#!/usr/bin/env python3
import argparse
import subprocess
import sys
from pathlib import Path

COLORS = {"GREEN": "\033[92m", "RED": "\033[91m", "CYAN": "\033[96m", "YELLOW": "\033[93m", "RESET": "\033[0m"}

def print_c(msg, color="RESET", end="\n"):
    print(f"{COLORS.get(color, '')}{msg}{COLORS['RESET']}", end=end, flush=True)

def push_key(target_host, user, home_dir, pub_key_content):
    print_c(f"  -> 正在处理用户: {user}... ", end="")
    
    # 严格的目录与文件权限控制
    remote_script = (
        f"mkdir -p {home_dir}/.ssh && "
        f"chmod 700 {home_dir}/.ssh && "
        f"cat >> {home_dir}/.ssh/authorized_keys && "
        f"chmod 600 {home_dir}/.ssh/authorized_keys"
    )
    
    ssh_cmd = ["ssh", f"{user}@{target_host}", remote_script]
    
    try:
        # 利用标准输入传值,安全且无痛
        result = subprocess.run(ssh_cmd, input=pub_key_content, text=True, capture_output=True)
        if result.returncode == 0:
            print_c("[成功 ✅]", color="GREEN")
        else:
            print_c("[失败 ❌]", color="RED")
            if result.stderr:
                print_c(f"     错误信息: {result.stderr.strip()}", color="YELLOW")
    except Exception as e:
        print_c(f"[异常 ❌] {str(e)}", color="RED")

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("host", help="目标主机的 IP 或 主机名")
    args = parser.parse_args()
    
    # 指向刚刚生成的 ecdsa-sk 公钥
    pub_key_path = Path.home() / ".ssh" / "happy_hello.pub"
    if not pub_key_path.exists():
        print_c(f"❌ 找不到公钥: {pub_key_path}", color="RED")
        sys.exit(1)
        
    pub_key_content = pub_key_path.read_text(encoding='utf-8').strip() + "\n"
    print_c(f"🚀 开始向主机 [{args.host}] 注入 FIDO2 公钥...", color="CYAN")
    
    push_key(args.host, user="root", home_dir="/root", pub_key_content=pub_key_content)
    push_key(args.host, user="happy", home_dir="/home/happy", pub_key_content=pub_key_content)
    print_c(f"🎉 注入流程结束。", color="YELLOW")

if __name__ == "__main__":
    main()

使用方法:python push_key.py 192.168.8.1。 以后再加新机器,跑一下脚本,就能享受 ssh gl 直接弹窗刷脸秒进系统的丝滑体验了。 注意⚠️,脚本在的用户名固定微root 和happy,记得替换一下


🛡️ 灾备预案(Disaster Recovery)

安全做到了极致,也要考虑极端情况。

如果哪天我的 Windows 账户重置了,或者本地 PIN 码被清除,会导致 TPM 里的硬件主密钥被重置,此时硬盘上的 happy_hello 将彻底失效。 应对方案:发生灾难时,直接用密码连上路由,删掉旧的 ecdsa-sk 公钥,重新执行一遍上述流程颁发新钥匙即可。

总结: 爽!一个私钥搞定所有服务器的登录,还贼安全,这波折腾,值了!