做运维这些年,SSH 基本是每天打交道最多的工具。登录服务器、查日志、改配置、发版本,哪一个环节都离不开它。起初用密码登录觉得没什么,直到服务器一台台增多,密码复杂度要求越来越高,每次上线都要在键盘上敲一长串口令,还要担心日志留痕、被人偷看,我终于把 Linux 下的 SSH 免密登录彻底用了起来。所谓 SSH 免密登录,简单说就是用密钥对替代密码完成身份认证:客户端持有私钥,服务器端存放公钥,登录时私钥自动完成签名校验,不需要再人工输入密码。它的适用面很广,无论是个人管理几台云主机,还是在公司维护几十上百台集群,只要涉及 Linux 远程操作,这套配置都绕不开。今天就把完整的配置思路、实操步骤和排查经验整理出来,给刚接触 Linux 的新手一份能直接照做的参考,也给自己留一份可以查漏补缺的笔记。
1. SSH免密登录的整体设计与思路拆解
1.1 从密码到密钥:免密登录到底在解决什么问题
先说为什么要折腾这套东西。很多人第一反应是"免密"等于不安全,实际上恰恰相反。基于密码的 SSH 登录,口令是经过网络传输去校验的,虽然 SSH 协议本身做了加密,但真正薄弱的环节在人:为了好记,很多人把密码设成统一口令,一旦一个网站库泄露、一台机器被拖库,其他机器的密码也跟着暴露;更不用提暴力破解工具对弱口令的穷举,我见过公网机器一天内被扫描攻击上千次的。改用密钥认证后,私钥只存在于你自己的客户端机器上,服务器的公钥泄露了也无法反向推出私钥,安全性是质的提升。
免密登录解决的第二件事是效率。手动敲密码登录单台机器还好,如果要批量更新配置、批量分发文件、批量执行巡检脚本,每台机器都要输一次密码,中间还夹杂着输入错误需要重试,人很容易崩溃。我最早管四十多台机器的时候,光是每轮上线执行指令就要花掉半个多小时敲密码,后来配好免密,一键脚本跑完所有机器,时间压缩到两三分钟。这就是免密登录的核心价值:把"人肉输密码"变成了"机器自动验签",安全基线更高,操作效率也完全不可同日而语。
还有一个容易被忽视的场景:定时任务和自动化平台。crontab 里写的同步脚本、Jenkins 或 Ansible 这类自动化工具要连接目标机器执行命令,都不可能有人守在旁边输密码,它们必须依赖免密登录或者类似的工作机制。所以做运维基础建设,SSH 免密是第一批要铺好的路。
1.2 非对称加密的直观类比:锁和钥匙的关系
想要把免密登录配置得明明白白,得先理解它的底层逻辑,也就是非对称加密。这个概念听起来唬人,但打一个比方就好懂了:想象你手里有一把特殊的锁和几把配套的钥匙,锁可以随便发给任何人,钥匙只留在自己手里。别人拿到锁后,把锁挂到门上,你用自己手里的钥匙就能打开这把锁,而别人手里只有锁、没有钥匙,永远开不了门。
这里的"锁"就是公钥,可以放到任何一台你想登录的服务器上;"钥匙"就是私钥,只能保存在你自己的客户端。当你发起 SSH 连接时,服务器会抛出一个验证挑战,你的客户端用私钥对这个挑战进行签名,服务器再用存放的公钥去验签,验证通过就放行。整个过程里,私钥不会在网络上传送,服务器端也没有任何能反推出私钥的敏感信息,所以哪怕公钥在网络上被人截获,也不影响安全性。
理解了这层关系,再去看配置过程就非常清晰:客户端的 ~/.ssh/ 目录里存放公钥(.pub 后缀)和私钥,服务器的 ~/.ssh/authorized_keys 文件里存放的都是各客户端的公钥。配置免密登录的本质,就是把你的公钥"投递"到目标服务器上指定用户的 authorized_keys 里,并保证相关权限正确。权限问题我在后面会展开讲,这是免密配置里坑最多的环节。
1.3 密钥算法的选择:是 ed25519 还是 RSA
很多教程一开始就让你执行 ssh-keygen,但没告诉你算法参数怎么选。早年主流是 RSA,生成命令通常是 ssh-keygen -t rsa -b 4096,位数决定了破解难度,2048 位目前仍可用,但为了稳妥,新生成的密钥建议直接上 4096。RSA 兼容性极好,几乎所有 SSH 实现都支持,缺点是密钥比较长,校验时计算量稍大,不过在当今的硬件条件下感知不强。
如果你的 Linux 发行版和 SSH 客户端版本都较新,我更推荐用 ed25519 算法,生成命令是 ssh-keygen -t ed25519。它在同等安全强度下密钥长度更短(公钥大概几十个字节),签名速度快,私钥本身也足够安全,OpenSSH 6.5 以上的版本都支持。唯一要注意的是,一些非常老旧的嵌入式设备或低版本网络设备,可能不支持 ed25519,这时候老老实实用 RSA 4096 最省心。
我的习惯是:个人电脑和主流服务器之间用 ed25519,遇到老设备或银行、政企客户那边的特定环境,就针对性生成 RSA 4096 密钥。这种"按环境分类处理"的思路,也是做运维的基本素养——不是越新越好,而是兼容和安全的折中最优。
2. 核心细节解析与实操要点
2.1 密钥生成:参数不同,结果天差地别
密钥对生成看似一条命令搞定,里面的细节却直接影响后面排查问题的难度。
ssh-keygen -t ed25519 -C "your_email_or_note" -f ~/.ssh/id_ed25519-t 指定算法,-C 是注释信息,通常写自己的邮箱或机器标识,方便在多密钥环境下分辨哪把是哪个用途的。-f 指定生成路径和文件名。如果直接执行 ssh-keygen 不带任何参数,它会用默认算法和默认文件名一路回车,最终生成 ~/.ssh/id_rsa 和 ~/.ssh/id_rsa.pub,不是不行,只是后续维护时不够清晰。
生成过程中会提示 Enter passphrase,这是给私钥再加一道本地口令保护。如果这里直接回车,私钥文件本身没做二次加密,任何人拿到你的私钥文件就能直接仿冒你登录服务器。建议个人工作机设置一个 passphrase,配合本机的钥匙串或 ssh-agent 实现"输一次、以后都不用输"的效果,既不牺牲体验,又多一层保险。我自己的做法是:服务器上的自动化专用密钥不设 passphrase,因为要让 crontab 和 Jenkins 无人值守调用;而用于人工日常登录个人电脑的密钥一定设 passphrase,并加入 ssh-agent 托管。
密钥文件创建好之后可以检查一下:
ls -l ~/.ssh/ chmod 700 ~/.ssh chmod 600 ~/.ssh/id_ed25519 chmod 644 ~/.ssh/id_ed25519.pub私钥权限必须收紧,公钥可以放开读,但目录至少要 700,这是很多新手容易忽略的第一道坎。
2.2 权限问题:700、600 一个都不能错
SSH 免密登录配置里,最高频的故障原因就是权限错误。OpenSSH 的安全机制里有 StrictModes 这个默认开启的选项,它会严格检查目标用户的家目录、~/.ssh 目录以及相关文件的属主和权限,一旦发现权限过宽,就直接拒绝用密钥登录,宁可给你报错,也不给攻击者留利用宽松权限做手脚的机会。
具体来说,以下权限要求是硬性的:
| 检查项 | 客户端要求 | 服务器端要求 |
|---|---|---|
| 家目录 | 不要求必须 700,但属主必须是当前用户 | 不能被其他用户写(典型是 755/700) |
| ~/.ssh 目录 | 700 | 700 |
| 私钥文件 | 600 | 不需要存在 |
| authorized_keys | 不需要 | 600 |
| 公钥文件 | 644 即可 | 600 或 644 均可 |
这个表格的关键信息是:服务器端的 ~/.ssh 目录和 authorized_keys 文件的属主必须和登录用户一致,不能 root 创建的目录让普通用户用。我遇到过最典型的坑是:用 sudo 把公钥写进别人的家目录,导致 authorized_keys 属主变成了 root,结果普通用户怎么都登不上去,报错 Permission denied (publickey)。解决办法是直接切换到目标用户身份去操作,或者用 sudo -u 指定属主,最后再用 chown 修正。
修正权限后,通常问题就能解决:
chown -R 用户名:用户组 ~/.ssh chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys2.3 服务端配置:sshd_config 里的关键开关
免密登录不是仅靠客户端就能完成的,服务端的 sshd 配置也得配合。配置文件在 /etc/ssh/sshd_config,修改后需要重启服务,不同系统的命令略有差别。Ubuntu 和 Debian 系是 sudo systemctl restart ssh,CentOS/RHEL 系是 sudo systemctl restart sshd。
和免密直接相关的参数有四个:
- PubkeyAuthentication:必须设置为 yes,允许公钥认证。
- StrictModes:默认就是 yes,强烈建议保持开启,它能帮我们挡住权限错配的问题,关闭后反而是放松了安全基线。
- PasswordAuthentication:可以设为 no,强制所有登录走密钥认证。这是"彻底免密"的最后一步,但要注意,改成 no 之前务必确认你的密钥已经配置成功并能登录,否则一旦断连就进不去了。
- AuthorizedKeysFile:默认是 .ssh/authorized_keys,一般不需要动,如果自定义了路径或者配了多个路径才需要修改。
还有一个容易踩的细节是,某些发行版的 sshd_config 里其实只有默认配置,而在 /etc/ssh/sshd_config.d/ 目录下有其他配置文件覆盖设置。比如 Ubuntu 22.04 之后的版本,默认就带一个 50-cloud-init.conf 之类的东西,里面的配置可能和主文件冲突。排查问题前,最好先用 sshd -T 查看实际生效的配置,而不是只看 sshd_config 字面内容。
sudo sshd -T | grep -E 'pubkeyauthentication|passwordauthentication|strictmodes'它能打印出服务端真实生效的安全策略,很多"改了半天不生效"的玄学问题,最后都出在覆盖性配置上。
3. 实操过程与核心环节实现
3.1 从零开始的单机配置完整流程
把前面讲过的原理串起来,一次完整的免密配置只需要四步。
第一步,在客户端生成密钥对。
ssh-keygen -t ed25519 -C "work-station" -f ~/.ssh/id_ed25519一路按提示操作,passphrase 根据实际情况决定要不要设,最后会生成两个文件:id_ed25519(私钥)和 id_ed25519.pub(公钥)。
第二步,把公钥部署到服务器。
如果你手里有目标服务器的密码,最简单的方式是用 ssh-copy-id 工具。
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server_ip这个命令会要求你输入一次目标服务器的用户密码,输入后它会自动把公钥追加到目标用户家目录的 ~/.ssh/authorized_keys 里,并且自动设置好相关权限。一步到位,非常省事。如果你的系统没有 ssh-copy-id(部分精简版系统或旧系统没有),可以手动执行:
cat ~/.ssh/id_ed25519.pub | ssh user@server_ip "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"这两条命令的效果是一样的,区别只是后者把权限设置放在了"管道"里一并执行。我用的是后者,因为它在自动化脚本里更可控,不怕某个环节漏掉。
第三步,验证免密登录。
ssh user@server_ip正常情况下不会再提示输入密码,直接进入目标机器的 shell。第一次连接时如果遇到 host key 确认提示,输入 yes 即可。
第四步,考虑是否彻底禁用密码登录。
确认密钥认证稳定后,再去修改 sshd_config:
sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config sudo systemctl restart sshd这里提供的是 sed 快速改法,实际生产环境我建议用编辑器仔细改,改完一定要用 sshd -t 校验配置语法。养成这个习惯,能避免手滑写错参数导致 sshd 无法启动的尴尬。
3.2 大规模服务器的批量免密配置思路
单台机器配免密人人都会,真正考验人的是几十台上百台怎么快速铺开。我经历过最原始的做法:一台台 ssh-copy-id,后来人麻了,就开始写脚本。核心思路是先准备一个已经支持免密登录的"跳板机"或者控制机,从这台控制机把公钥推送到所有目标机器。
一个简单的循环脚本思路如下:
#!/bin/bash # 批量分发公钥脚本 for host in $(cat hostlist.txt); do echo "正在处理 $host" sshpass -p '统一初始密码' ssh-copy-id -i ~/.ssh/id_ed25519.pub -o StrictHostKeyChecking=no root@$host done这个脚本用到了 sshpass 工具来指定初始密码,适合服务器密码统一的初始化阶段。要注意的是 -o StrictHostKeyChecking=no 这个参数,它会在首次连接时跳过 host key 确认,自动化场景里是必要的,但也意味着存在中间人攻击风险,所以只建议在内网初始化环境用,公网环境不要这么干。
批量场景下还有几个进阶技巧。第一,把相同的配置抽成 Ansible 剧本或自研脚本,不要在每台机器上重复造轮子。第二,批量免密完成后,立即把临时密码作废或者轮换,并且尽快把 PasswordAuthentication 统一设为 no,避免留下口令登录的窗口。第三,hostlist.txt 里的主机名或者 IP 要提前整理准确,最好配上 DNS 泛解析或者 /etc/hosts 条目,不然脚本跑一半因为域名解析失败卡住,排查起来反而费时。
3.3 用 config 文件管理多台服务器
机器多了之后,纯记忆 IP 和用户名会让人混乱。推荐把连接信息整理到客户端的 ~/.ssh/config 文件里,语法非常简单。
Host prod-web-01 HostName 192.168.10.21 User deploy Port 22 IdentityFile ~/.ssh/id_ed25519 ServerAliveInterval 30Host 后面的别名是在命令行里直接使用的名字,配置好后,执行 ssh prod-web-01 就能自动匹配 HostName、User、端口和密钥文件,免密体验直接升一级。HostName 支持 IP,也支持域名;Port 用在非标准端口的机器上特别方便,不用每次敲 -p 参数。
config 文件里还可以用通配符做分组管理,比如给同一批测试机加统一配置:
Host *.test.internal User testadmin IdentityFile ~/.ssh/id_test StrictHostKeyChecking no我自己维护 config 文件的习惯是:每个条目都写清楚用途注释,比如 "# 生产环境-前端-A区",这样半年后再看也能一眼知道这台机器是干嘛的。配合 IdentityFile 明确指定密钥文件,还能规避多密钥环境下"明明公钥放上去了,却因为默认读取了另一个密钥导致登录失败"的问题。这条经验非常值钱,很多同事配了好几天发现连不上,最后就是栽在密钥文件选择错误上。
4. 常见问题与排查技巧实录
4.1 authorized_keys 的"幽灵权限"问题
前面提过权限,但这类问题实在太常见,值得专门拿出来讲。现象是:明显确认公钥内容已经正确写入 authorized_keys,服务端配置也没问题,但每次连接还是要求密码,打开详细日志才看到 Authentication refused: bad ownership or modes。
这种问题绝大多数出在"属主不一致"或者"文件权限过宽"。例如你用 root 在 /home/alice/.ssh/authorized_keys 里手动追加了内容,文件属主就是 root,而登录用户是 alice,StrictModes 直接拒绝。修法很简单,一条 chown 解决:
chown alice:alice /home/alice/.ssh -R chmod 700 /home/alice/.ssh chmod 600 /home/alice/.ssh/authorized_keys还有一点容易被忽略:目标用户的家目录本身权限不能是 777,也不能被其他用户写入。比如家目录是 /data 且挂载在公共目录下,权限过宽会导致同样的拒绝结果。我在共享存储环境下踩过这个坑,最后把家目录调整为 700 才解决。这类问题的通用排查命令是:
ls -ld /home/username ls -ld /home/username/.ssh ls -l /home/username/.ssh/authorized_keys三行命令看下来,问题基本无处遁形。
4.2 多密钥环境下用错了 IdentityFile
另一类典型现象是:公钥明明放上了,服务端也显示验证到了公钥,但连接仍然失败,因为客户端默认尝试的私钥不是配好的那一把。出现这个问题的场景通常是本机的 ~/.ssh/ 目录里同时存在 id_rsa、id_ed25519、部署专用密钥等多把密钥,而 ssh 默认按文件名顺序去尝试,第一把尝试的密钥不对,服务器返回拒绝,客户端也不会自动去尝试下一把(除非配置了多个 IdentityFile)。
解决办法有两个。一是在 ~/.ssh/config 里给每个 Host 加上明确的 IdentityFile,指向正确的私钥文件;二是用 ssh-agent 管理密钥,把需要用到的私钥 add 进去。
eval "$(ssh-agent -s)" ssh-add ~/.ssh/id_ed25519加了之后,ssh 会优先让 agent 提供它持有的密钥,避免"选错"的问题。注意,如果你给私钥设置了 passphrase,ssh-add 时会要求输入一次,输入后 agent 会记住解锁状态,后续连接不再询问。这就是我前面说"既设 passphrase 又不影响体验"的具体实现方式。
4.3 日志定位:-v 参数和 system log 是最后的底牌
排查 SSH 问题,最高效的武器是客户端加 -v 参数。执行 ssh -v user@host,客户端会打印整个连接过程的详细调试信息,包括尝试了哪些密钥、服务器接受了哪些认证方法、在哪一步被拒绝。如果 -v 还不够详细,可以再加一层,变成 -vvv,输出更啰嗦但信息更全。
ssh -vvv user@host重点观察有没有类似 "Offering public key: /home/user/.ssh/id_ed25519" 的行,后面如果接着 "Authentications that can continue: publickey,password",说明服务器拒绝了这把公钥,要么是公钥没写对,要么是权限问题。如果看到 "Received disconnect from ..." 则表示连接被服务端直接断开,这时要去服务端看日志。
服务端日志在多数 Linux 系统上通过 journalctl 查看:
sudo journalctl -u ssh -f然后另开一个终端尝试连接,实时观察服务端收到的认证请求和拒绝原因。很多问题在客户端看是一团迷雾,服务端一行日志就能点破要害,比如我前面提到的 "bad ownership" 和 "Permission denied",都会在日志里写明具体原因。
4.4 高频踩坑清单:SELinux、企业环境与老系统兼容
有些坑不是天天遇到,但遇到一次就能让人折腾一晚上。第一个是 SELinux。在 CentOS/RHEL 系上,如果 SELinux 开启且安全上下文错误,即使所有权限都正确,SSH 也会拒绝读取 authorized_keys。排查方法是临时放行或用 restorecon 修复上下文:
restorecon -Rv ~/.ssh或者检查 .ssh 目录的上下文标签是否和 home 目录规则一致:
ls -Zd ~/.ssh第二个是企业环境对密钥算法和协议的限制。有的加固策略要求只允许 RSA,有的禁止用 ed25519,还有一些老版本 OpenSSH 不识别较新的密钥格式,导致密钥上传到服务器后无法解析。遇到这种情况,服务端日志会提示 "no mutual signature algorithm" 或 "key type not supported"。处理方式是客户端生成密钥时针对性选型,或者升级服务端 OpenSSH 版本。
第三个是主机名和 DNS 问题。连接超时或反复提示输入密码,有时候不是密钥问题,而是目标机器的 UseDNS 配置导致反查 DNS 超时。可以在服务端设置 UseDNS no,这个参数在华丽的 ex 环境下影响不大,但在网络环境复杂的办公网里很管用。
5. 实操经验速查与安全补充
5.1 推荐直接用的一条龙脚本
与其每次手工打一堆命令,不如把整个配置过程沉淀成一个脚本,在全新机器上折腾一次,后面就完全复用。下面是我个人常用的一份一次性初始化脚本,适合在同一用户下快速完成客户端密钥生成和服务器端公钥部署:
#!/bin/bash # 一键完成 SSH 免密登录配置(客户端执行) KEY_NAME="$HOME/.ssh/id_ed25519" TARGET_USER="$1" TARGET_HOST="$2" if [ ! -f "$KEY_NAME" ]; then ssh-keygen -t ed25519 -C "$(hostname)-$(date +%Y%m%d)" -f "$KEY_NAME" -N "" fi ssh-copy-id -i "${KEY_NAME}.pub" "${TARGET_USER}@${TARGET_HOST}"脚本接受两个参数:目标用户名和目标主机地址,老机器直接跑,新机器会自动先生成密钥再部署。用起来就是 bash setup_ssh.sh deploy 192.168.10.21。
脚本里的 -N "" 表示生成的私钥不设置 passphrase,适合自动化场景;个人机器要加 passphrase 的话,把 -N "" 去掉,让命令交互式提示输入即可。自动化密钥和人工密钥混用时要分开管理,这是我反复强调的一点:供 crontab 和 CI 用的密钥不放 passphrase,供人工维护用的密钥一定要放 passphrase,一搞混,安全基线就形同虚设。
5.2 免密登录的边界:什么时候不该用
免密登录虽好,但也不是所有场景都适合无脑铺开。先说第一种情况:高安全级别的生产环境,尤其是金融、政务相关的核心资产,建议保留多因子认证,比如密钥 + 动态令牌,或者至少把来源 IP 限制到白名单里。第二种情况:临时开通给外部合作方的账号,尽量不要走免密,用临时密码 + 到期失效的模式更可控。第三种情况:服务器和客户端之间有跳板机、堡垒机链路,密钥需要跨多级系统分发时,要先制定好信任链,不能一登到底,否则跳板机被攻破,后面所有机器全沦陷。
免密的本质是把"人记忆密码"的负担转移给了"私钥文件",这个转移的前提是私钥文件本身保管好。所以但凡我配置免密,一定会同步做两件事:一是把公钥统一收集到配置管理仓库里做备份,二是私钥绝对不能进任何形式的代码仓库和分享文档。搭好这个习惯,免密才是真正的便利,而不是把风险从密码换成了私钥。
5.3 巡检免密配置是否"健康"的小技巧
配置完成后,很多人会以为一劳永逸了,但运维环境变动频繁,新老密钥交替、人员离职、机器重置,都会让免密体系慢慢"烂掉"。我习惯在巡检脚本里加一项免密连接自检,巡检时逐台机器测试无密码登录是否正常,并检查是否有非预期用户出现在 authorized_keys 里。
for host in $(cat hostlist.txt); do timeout 10 ssh "$host" "echo ok" >/dev/null 2>&1 && echo "$host 正常" || echo "$host 异常" done这个循环是我巡检的基础模板,timeout 10 防止某台机器卡住拖慢整轮检查。这个脚本跑出来的异常主机,就是需要重点排查的对象,可能密钥被轮换、服务端权限被改、机器被还原,趁早发现总比某天自动化任务突然全挂要好。
最后再分享一个我个人特别受用的习惯:给所有需要批量操作的主机统一使用同一个部署专用公钥,但每台机器上保留各自的带 passphrase 管理密钥。这样一来,日常人工登录走的是个人密钥,可以精确审计到人;自动化批量操作走的是部署密钥,方便脚本统一调用。两套体系互不干扰,排查问题时又能快速区分是人的操作还是脚本的操作,这是我踩过不少坑之后沉淀下来的做法,希望对你有用。