说实话,这个主题我太有发言权了。早几年我管着一批服务器,登录全靠密码,每天看登录日志那叫一个心慌——各种IP轮番来试密码,暴力破解的记录一拉一大片。后来有个凌晨,生产环境那台机器真的被撞库撞进去了,好在只是落了马,没造成数据损失,但从那天起我下定决心,把所有服务器的登录方式全部切到公钥与私钥配置,也就是SSH密钥认证。这篇文章我就把自己这些年配置服务器密钥认证的完整实操、踩过的坑、以及服务器端加固心得都写出来,给正要转型、或者已经被密码爆破折磨过的朋友做一份参考。
这套内容能解决的问题很直接:让你彻底告别密码登录,服务器不再裸奔在暴力破解的枪口下,同时管理一批服务器时不用再记十几个密码。适合谁看?不管是刚上手第一台云主机的学生,还是维护几十台生产环境的运维,下面这套从生成密钥、部署公钥、私钥保管,到服务器端关闭密码登录的完整链路,都可以直接照做。
1. 为什么我放弃了密码登录:密钥认证的核心价值
1.1 密码登录的三个无法回避的痛点
先别急着上命令,我想把"为什么"讲透。密码认证看起来简单,但它在真实生产环境里问题太多。
第一是爆破成本太低。只要服务器暴露在公网,就有扫描器在跑,手上一本常见密码字典,对着22端口挨个试。弱密码、默认密码、复用密码,在中奖率上差别不大,试一万次总有一次能蒙对。我在日志里见过最疯狂的一次,五分钟内同一个人尝试了两百多次登录,那还是没被工具加并发的情况。第二是密码的分布轨迹没法控制。一个人管五台服务器,很可能五台都用一个密码,脚本拿到一台的密码,等于拿到了所有机器。第三是审计困难。多个人共用一个账号密码的时候,出了安全事件你根本不知道是谁干的,权限边界完全模糊。
而换成密钥认证之后,这三个问题被一次解决:爆破不再有意义,因为私钥不在服务器上,猜一万年也猜不出来;密钥可以每台机器单独生成、单独吊销;更重要的是,人与人之间可以做到每人一把私钥,公钥上的注释信息直接标明归属。
1.2 非对称加密是"一次生成、长期使用"的钥匙
公钥与私钥的底层是非对称加密。你可以把它理解成一把特制的锁和钥匙:公钥是那把挂锁,谁都可以拿过来把箱子锁上,但锁上之后只有你手里的私钥能打开。更神奇的是,这个关系是单向的——知道那把锁长什么样,造不出钥匙。
SSH登录实际用到的机制其实更类似于"签名验证"。流程是这样的:你把公钥留在服务器的 authorized_keys 文件里,自己留着私钥。第一次用密钥连上去的时候,服务器会发来一个随机挑战,你的客户端拿私钥对挑战进行签名,然后把签名结果发回去;服务器端拿到签名后,用你留下的公钥去验证。验证通过,说明持有私钥的人确实是你,于是放行登录。
整个过程中,私钥永远不离开你的本地机器,网络传输的只是签名结果,不可能被中间人截获后反推出私钥。这也是为什么密钥认证在安全性上比密码上了一个大台阶——密码在网络上是有机会被截听的,而私钥从设计上就没有被"传出去"这个环节。
1.3 我见过太多人栽在"知道原理却配错顺序"
说个真实情况:很多人知道要生成密钥,但第一步就搞反了。我见过好几位同事,在服务器上执行 ssh-keygen,把公钥留在服务器上,私钥还在服务器上,然后高高兴兴地从本地连——这等于把钥匙挂在锁边上,完全失去意义。正确的做法是:密钥对必须在客户端(你日常使用的电脑/笔记本)生成,私钥永远留在客户端,只有公钥内容拷贝到服务器。
记住这个铁律之后,后面所有操作都是有章法的。
2. 本地生成密钥对:选型与参数详解
2.1 选哪一类密钥:ed25519、RSA 还是 ECDSA
打开终端,首先得决定生成哪种类型的密钥。我直接把结论放在前面:新环境无脑选ed25519,有老系统兼容性顾虑选RSA 4096,其他类型没必要纠结。
| 密钥类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ed25519 | 速度快、密钥短、安全性强、现代系统默认支持 | 老版本OpenSSH(7.2以下)不支持 | 新服务器、新工具链、个人主力密钥 |
| RSA 4096 | 兼容性最好,几乎所有SSH实现都认 | 密钥长、慢、生成时间长 | 需要兼容老设备/老系统的场景 |
| ECDSA | 密钥短 | 依赖随机数质量,历史上出过问题 | 不太推荐 |
| DSA | 已废弃 | 安全性弱 | 绝对不要用 |
我个人现在的习惯是:个人电脑的默认密钥一律 ed25519;给某个老客户内部的 Solaris 机器配密钥时,才单独生成一把 RSA 4096。你如果拿不准手里那台设备支不支持,可以先看它的 OpenSSH 版本跑不跑得动ssh -V。
2.2 ssh-keygen 实操:从默认到精细控制
生成密钥的命令我拆开讲。最简形态:
ssh-keygen -t ed25519一路回车的话,它会把密钥写到~/.ssh/id_ed25519和~/.ssh/id_ed25519.pub,并问你两次 passphrase(口令)。很多新手在这里图省事直接跳过,但我的建议是一定要设 passphrase。
passphrase 的作用不是登录服务器时用的,而是保护你本地的私钥文件。别人就算从你电脑上拷走了私钥,没有口令也解不开。打个比方:私钥是钱包,passphrase 是钱包的拉链密码,服务器端公钥是钱庄的印章。拉链密码设置后,你每次用私钥需要先解开拉链——放心,我用 ssh-agent 一劳永逸地解决这个问题,后面会专门讲。
如果你对文件名、注释有要求,可以用完整参数:
ssh-keygen -t ed25519 -a 64 -f ~/.ssh/deploy_key -C "deploy bot from office mac"-t指定类型-a指定 KDF 迭代次数,数值越高暴力破解越难,一般不追求极致的话默认也行-f指定保存路径和文件名,便于区分用途-C指定注释,建议写成"哪台机器、什么人、干什么用",这个注释会显示在服务器的 authorized_keys 里,之后管理密钥时一眼就能看出归属
生成 RSA 就改成:
ssh-keygen -t rsa -b 4096 -f ~/.ssh/legacy_rsa -C "rsa key for legacy hosts"2.3 生成之后的检查清单
密钥生成后不要急着走,做三件事:
- 查看公钥内容:
cat ~/.ssh/xxx.pub,开头是类型(ssh-ed25519 或 ssh-rsa),中间是一长串字符串,最后是注释。少了任何一段都说明文件有问题。 - 查看公钥指纹:
ssh-keygen -lf ~/.ssh/xxx.pub,会输出一串指纹。这个指纹是公钥的散列结果,平时在服务器上、各种托管平台的密钥管理页面上,用来核对"我手上这把公钥是不是同一把"。 - 对比私钥和公钥是否配对:
ssh-keygen -y -f ~/.ssh/xxx可以只从私钥生成对应的公钥内容,和xxx.pub对比一致才说明这把私钥和公钥是原配。
另外提一句命名习惯。我之前见过有人把所有人的公钥都往id_rsa里塞,最后根本分不清谁是谁。我的习惯是一台用途/一台服务器一把独立密钥,文件名带上目标,比如~/.ssh/github_main、~/.ssh/web_prod_01。虽然密钥多了点,但配合后面的 config 文件,管理成本很低,安全收益却很大——吊销一把密钥不会影响别的机器。
3. 公钥上服务器的三条路线:从最稳到最快
3.1 路线一:ssh-copy-id 一键部署
本地生成好密钥后,第一步是让服务器认你这个公钥。最省事的方式是ssh-copy-id这个命令,它会把本地的指定公钥追加到服务器的 authorized_keys 文件里,期间只需要你输一次当前账户的登录密码。
ssh-copy-id -i ~/.ssh/prod_server.pub deploy@192.168.1.10它会要求你输入deploy@192.168.1.10的密码。输入正确后,命令会自动完成创建~/.ssh、追加公钥、设置权限这些琐事。注意-i参数指向的是公钥文件(.pub),不是私钥。这个命令的原理等同于把下面的手动操作全部包好,但胜在不漏细节、不踩权限坑,是我给新手推荐的第一选择。
3.2 路线二:手动追加,完全可控
有些场景没法用ssh-copy-id,比如目标服务器禁止密码登录、或者你手上只有一个管理面板的网页终端。这时候就手动操作。
先用本地命令把公钥内容复制到剪贴板:
cat ~/.ssh/web_prod_02.pub然后登录服务器(或者在外壳里打开终端粘贴),执行:
mkdir -p ~/.ssh chmod 700 ~/.ssh echo "ssh-ed25519 AAAA...... comment" >> ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys这里的每一步都有讲究:mkdir -p是为了在没有.ssh目录时也能直接创建且不报错;chmod 700确保.ssh目录其他用户不可读;追加而不是覆盖,是为了不抹掉已有的其他公钥;最后的chmod 600是保证 authorized_keys 文件只能被属主读写。
有两点我反复在团队里强调:第一,不要用>覆盖写 authorized_keys,除非你确信里面是空的,否则会把其他人/其他用途的公钥清掉;第二,粘贴公钥内容时不要夹带任何多余字符,尤其是 Windows 下复制公钥,很容易带进 CRLF 换行符,导致整行公钥失效,登录时报错还看不懂原因。
3.3 路线三:批量分发,自动化场景的正确姿势
手头有十台以上服务器的时候,一台台ssh-copy-id显然不现实。我通常借助配置管理工具做批量分发,先写一个 Ansible 任务,读取本地某个公钥文件的内容,把它写入每台目标机器的/home/某个用户/.ssh/authorized_keys。这个任务的核心结构大概长这样:
- name: 部署公钥到远程用户 authorized_key: user: deploy state: present key: "{{ lookup('file', '/local/path/to/key.pub') }}"如果没有现成的配置管理环境,也可以用脚本循环,但要自己处理好权限和幂等性(也就是重复执行不会加重复行)。不管用哪种方式,批量执行完之后的验证步骤一定不能省,下一节会讲怎么验证。
3.4 权限红线:为什么 .ssh 和 authorized_keys 不能写错
我在不少踩坑案例里看到,公钥明明贴到了正确文件,SSH 却依然要求密码而且日志里报Permission denied (publickey)。往前翻日志,大概率能看到类似Authentication refused: bad ownership or modes的提示。这是 SSH 服务端的StrictModes机制在把关:如果关键目录或文件权限过宽,SSH 会拒绝使用这个公钥登录。
我总结过一组必须守死的权限底线:
| 路径 | 建议权限 | 原因 |
|---|---|---|
用户主目录~ | 755 或 700,但不能是 777 | 被其他用户可写意味着可篡改 |
~/.ssh目录 | 700 | 只有属主能进入 |
~/.ssh/authorized_keys | 600 或 644 | 属主可写,其他用户可读但不写 |
| 私钥文件(本地) | 600 或 400 | 见下一章 |
很多新人第一次配置密钥后登录失败,不是公钥有问题,而是主目录是 777、.ssh目录是 777,SSH 直接拒绝信任这个环境的完整性。修改权限后再试,问题立刻消失。
3.5 验证登录的过程要点
公钥部署好之后,务必用一次完整登录验证,别急着关掉密码登录。验证命令:
ssh -i ~/.ssh/prod_server deploy@192.168.1.10如果带 passphrase,会让你输入私钥口令;如果没有,会直接进入服务器。登录成功之后,再看一眼确凿的证据——用ssh -v连接时输出里会出现Authenticated to 192.168.1.10,以及Offering public key这样的字样。这说明实际用的是你的私钥签名,而不是密码。
4. 私钥的本地守护:权限、Agent 与多密钥管理
4.1 私钥文件权限:一条不可妥协的底线
私钥文件如果被其他用户读取,那就等于把钥匙拱手送人。OpenSSH 客户端自己也有安全检查,某一次我在一台临时配置的机器上发现私钥权限是 644,SSH 直接拒绝加载,警告UNPROTECTED PRIVATE KEY FILE——这是好事,它在强制你重视。
正确设置:
chmod 600 ~/.ssh/prod_server如果私钥需要更严格的隔离环境,可以考虑 400。日常个人使用 600 够了。团队共享的跳板机部署私钥时,建议每个账号独立私钥,不要往一个共享账号里塞多把私钥。
4.2 ssh-agent 解决的是"每次输口令"的痛
设置了 passphrase 之后,每次 SSH 连接都要输入一次口令,虽然安全,但真的烦。ssh-agent 就是专门解决这个问题的。它像你的私人钥匙保管员:你把私钥"注册"进 agent 并输入一次 passphrase,之后整个会话期,SSH 客户端需要签名时直接找 agent 要,不再反复问口令。
eval "$(ssh-agent -s)" ssh-add ~/.ssh/prod_server执行ssh-add时会要求输入那个 passphrase,输入一次后,这一串终端会话里就再也不用输入了。如果你的系统是 macOS,钥匙串还会自动管理这些私钥;Linux 桌面环境则看具体情况,但手动拉起 agent 是最通用的办法。
对于多把私钥的场景,可以在~/.ssh/config里为每个主机指认私钥,这样 agent 知道你该用哪把钥匙;也可以在连接时直接指定-i。我强烈建议第一种,因为 config 文件还能把端口、用户名、连接别名一次性配置好。
4.3 ~/.ssh/config:多服务器的"通讯录"
当手上密钥多了以后,拼命令变成一件很傻的事。我通常在~/.ssh/config里给每台服务器都写一段:
Host prod HostName 10.20.30.40 User deploy Port 22 IdentityFile ~/.ssh/prod_server ServerAliveInterval 60 Host dev HostName 192.168.1.22 User dev Port 2222 IdentityFile ~/.ssh/dev_key设置之后,连生产环境只需要ssh prod,它自动按配置走。ServerAliveInterval这个参数我也建议加上,作用是每60秒发一个保活包,避免长时间挂着的终端因为网络空闲被踢断。这对基于密钥登录的多服务器管理体验提升非常明显。
4.4 私钥丢失或泄露的处理预案
密钥这个东西,丢失比密码丢失更可怕。一旦私钥文件丢失或者疑似被拷走,不要心存侥幸,立刻执行三件事:
- 登录目标服务器,从 authorized_keys 里删除对应的那一行公钥。
- 本地重新生成一把新密钥对,按标准流程重新部署到所有相关服务器。
- 翻看服务器日志,确认泄露期间有没有异常登录记录。
我见过有人私钥在公共网盘上同步过,却觉得"我有 passphrase 应该没事"。我的态度是:私钥的任何一次意外暴露都按最坏情况处理,立刻吊销换新。passphrase 只是拖延时间,防不住有耐心的攻击者。
5. 服务器端强化:从能登录到抗爆破
5.1 sshd_config 的关键项解读
公钥能登录之后,下一步就是让服务器拒绝密码登录。这个操作的本质是修改 SSH 服务端配置文件,通常位于/etc/ssh/sshd_config,核心改动如下:
PubkeyAuthentication yes PasswordAuthentication no PermitRootLogin prohibit-password AuthorizedKeysFile .ssh/authorized_keys解释一下这几项:
PubkeyAuthentication yes:明确允许公钥认证,这是前提。PasswordAuthentication no:关闭密码认证,这是降低爆破风险的关键。一旦关闭,暴力破解者连试密码的机会都没有。PermitRootLogin prohibit-password:允许 root 用密钥登录,但禁止密码登录。如果你的安全规范更严格,可以改成PermitRootLogin no,完全禁止 root 直接登录,日常用普通用户登录再 sudo。AuthorizedKeysFile .ssh/authorized_keys:指定公钥文件的位置。除非有个性化需求,否则保持默认即可。
老版本 OpenSSH 还可能涉及ChallengeResponseAuthentication,建议一并关掉:
ChallengeResponseAuthentication no5.2 先备份、再修改、最后验证的三步走
修改sshd_config最忌讳的就是手滑而且没有备份。我的固定流程是:
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak然后编辑文件,保存之后先做语法检查:
sshd -tsshd -t会告诉你配置是否有语法错误。这一步之后重启服务:
systemctl reload sshd这里用reload而不是restart:reload 不中断已有连接,风险更低。但要注意,reload 之后新的连接才生效。
接下来是最重要的一步:保持当前连接不要断开,另外开一个新的终端从外部用密钥登录测试。测试成功后,再回来关掉旧连接。这样做的原因是,万一你配置写错导致新连接全部被拒,至少还有一条在线的逃生通道,不会把自己锁在门外。我吃过一次亏,在关闭密码登录的 reload 之后,发现公钥验证也失败,好在保留了一条另外一个端口的备用连接,否则那台机器就只能去机房/控制台救人了。
5.3 让爆破工具失效的辅助手段
关闭密码登录之后,理论上爆破已经无效了,但端口暴露仍在,扫描器还在不断打22端口。我建议再加两层防护。
第一层是 Fail2ban。它监控/var/log/auth.log(Debian/Ubuntu)或/var/log/secure(CentOS/RedHat),短时间内出现多次验证失败就拉黑对方IP。配置好之后,爆破者往往试几次就被隔离,服务器安全日志立刻安静很多。我见过有机器接入前每天上万次失败尝试,接入后日志里的暴力痕迹几乎清零。
第二层是防火墙或云安全组限制来源IP。如果你们公司有固定出口IP,把22端口只对那个IP开放是最干净的方案。就算没有固定出口,至少把22端口对内地、对办公网段放开,其他网段全部拒绝。这层属于"能不开就不开"的防守思路。
5.4 修改安全项后的自我检查
修改完配置别急着收工。我给自己定过一套事后检查:
- 用新终端重新 SSH 连接一次,确认密钥认证工作正常。
- 观察服务日志,确认重启后 SSH 服务正常。
- 尝试一次错误登录,看看是否提示密码认证被禁用。
- 从另一台不持有私钥的机器上执行
ssh -v,确认登录无法完成。
如果一切正常,服务器才算真正把安全基线拉高了一截。
6. 配置中绕不开的坑与排查链路
6.1 权限问题
这个可以排在第一大坑。症状就是公钥贴了、配置也开了,却还是提示Permission denied (publickey)。服务端日志里通常有这些字眼:bad ownership or modes。
原因我在上文提过,这里再补充一点实际操作中容易忽略的情况:当你用 root 帮某个用户贴公钥时,整个.ssh目录和 authorized_keys 文件的属主必须是那个用户,不能是 root。比如useradd出来的用户,如果你直接以 root 身份替他操作,文件会显示 root root,SSH 就会拒绝。需要先chown 用户:用户把归属改回来。
6.2 公钥格式与内容问题
第二大坑在格式。很多人从网页控制台复制公钥,复制下来的是一整段带注释的文本,看起来没问题,但隐藏的 CRLF 换行、多出来的空格、把公钥截断成两行,都足以让认证失败。
排查办法很简单:在服务器上cat -A ~/.ssh/authorized_keys,看到行尾有^M就说明存在 CRLF 问题;看到公钥内容被断行,就用文本编辑器重新整理成一行。另外注意 authorized_keys 里一行只能有一条公钥,不要多条挤在一行,也不要一条公钥断成两行。
我还见过有人把私钥内容误贴进 authorized_keys——这属于基础性错误,私钥和公钥长得不一样,公钥以ssh-ed25519或ssh-rsa开头,私钥则以-----BEGIN OPENSSH PRIVATE KEY-----开头,一眼就能区分。
6.3 SELinux 的干扰
在 CentOS/RHEL 这类系统上,restorecon和 SELinux 策略是常见的隐藏杀手。公钥和目录权限都符合要求,但 SSH 登录依然失败,日志里也不报权限问题,这时考虑 SELinux 把 authorized_keys 文件打上了错误的上下文标签。
处理方式不复杂:
restorecon -R -v ~/.ssh这只是恢复默认上下文。如果问题反复出现,可以检查/var/log/audit/audit.log里是否有avc denied记录,再用ausearch或grep看具体是哪项策略在拦截。一般 restorecon 就能解决 95% 的情况。
6.4 排查完整链路:从客户端到服务端
真遇到说不清的问题,我的排查顺序是固定的,从客户端到服务端一层层看:
- 客户端
ssh -vvv 目标:看本地加载了哪把私钥,有没有Offering public key字样,被拒绝时的提示是什么。 - 服务端看日志:
journalctl -u sshd -f或tail -f /var/log/auth.log,搜索Failed、Permission denied、bad ownership等关键词。服务端日志永远比客户端信息更直接。 - 确认服务正常:
systemctl status sshd,确认服务在跑,不是被防火墙挡了、也不是只监听了某个内网IP。 - 验证私钥与公钥是否配对:
ssh-keygen -y -f ~/.ssh/某私钥 > /tmp/check.pub,然后和服务器上的 authorized_keys 对比,看是否完全一致。 - 检查权限与上下文:一条条列出
ll -Z看权限和 SELinux 标签。
按这个链路走下来,基本没有查不出来的问题。关键在于不要跳步,不要第一步就在改配置,先确认是不是本来就能用、是不是权限问题,再看配置。
最后再多说两句我个人的经验。第一句:密钥认证上线之前,永远保留一个备用入口,不管是云厂商的网页控制台还是另一台跳板机,别让一次配置错误把你锁在门外。第二句:给密钥加上 passphrase 真的没有想象中麻烦,配合 ssh-agent 整个过程是无感的,但安全性高了一个量级。我见过太多人省了这一下,之后私钥文件被拷走时懊悔半天。公钥与私钥配置服务器这件事,本身不难,难得是把每个步骤背后的为什么搞清楚、把每一个权限细节落实到位,这套底子打好了,后面管再多的机器都有底气。