1. SSH认证机制中的用户角色:密钥、密码与账户的对应关系
1.1 密码认证的运转逻辑
SSH默认不维护独立的用户数据库,这一点和很多自建系统不一样。你在SSH协议里提到的“用户”,本质上就是服务器操作系统中已经存在的账户。登录时,客户端通过-l参数或者user@host的形式指定用户名,服务端拿到用户名后去系统账户库中查找,存在就继续验证密码,不存在就直接拒绝。整个过程里,服务端不会把失败原因说得太细,统一的“Permission denied”就是为了避免攻击者逐个试探用户名是否存在。
密码认证的校验过程和传统登录没有本质区别:用户输入密码,服务端将其与存储的哈希值比对。但要注意的是,SSH协议本身是加密传输的,所以密码不会以明文形式在网络中暴露,也不需要在客户端和服务端之间传递比对结果以外的信息。不过,密码认证最大的隐患不在协议设计,而在密码强度和使用习惯。只要密码简单,或者多个服务器共用同一套密码,那么任何一台机器被拖库,其他机器的SSH防线也会同步失守。这也是为什么现在几乎所有云厂商和主流Linux发行版都在默认推荐、甚至强制要求使用密钥认证。
1.2 公钥认证的本质:证明你持有私钥
密钥认证是SSH里最常用也是安全性最高的认证方式。它由一对非对称密钥组成:私钥留在客户端,公钥放在服务器上。登录时,服务端先确认客户端的公钥在目标用户的authorized_keys文件里,然后生成一个随机挑战值,用公钥加密后发给客户端;客户端必须用私钥解密并回应,才能证明自己确实持有与该公钥匹配的私钥。整个验证过程中,私钥不会离开客户端,更不会上传到服务器。
这套机制有个直观的类比:公钥是一把锁,私钥是唯一的钥匙。你把锁挂在服务器门口,只有持有钥匙的人才能打开。别人即使把锁复制成百上千份,也开不了任何一扇门,因为锁本身不包含打开自己的方法。
很多刚接触的人会有一个误区,认为把私钥也放到服务器上就能实现“免密登录”。这样做等于把钥匙挂在锁旁边,纯粹是自欺欺人。正确的方式永远是客户端保存私钥、服务器保存公钥,私钥的权限还要收紧到仅当前用户可读,最好再设置一个口令短语保护私钥文件,这样即使私钥文件落到别人手里,没有口令也解不开。
1.3 公钥与用户名映射背后的隐藏逻辑
公钥本质上只是一串没有语义的字节,它本身不包含“这是谁的钥匙”的信息。同一个公钥可以被授权到多个用户、多台服务器,也可以被放到多个账户的authorized_keys里。这一点在多人协同时经常被忽视:如果某个公钥被同时授权到了root和普通用户,那么持有该私钥的人就等于同时拥有了两个不同级别的入口。
还有一个容易被忽略的点:同一个用户账户下可以放置多把公钥,对应多台设备或多位维护者。这在服务器运维里很常见,一台测试机的某个运维账户下,可能挂了项目经理、开发、DBA三个人的公钥。这样做的好处是,某人离职时只需从他的账户里删掉对应公钥,不需要修改密码或者重建账户。
但坏处也在这里——公钥一旦授权,就是长期有效的凭证。比密码更隐蔽,因为密码可以定期更换,而公钥常常被遗忘在角落里。我见过不少团队对所有服务器统一使用同一个运维账户和同一把公钥,一旦有人离职,整把公钥都要换,所有服务器都要重新部署,工程量巨大。这些都是典型的“用户与权限边界没有划分清楚”的案例,后面我会专门讲多用户场景下的设计思路。
2. 连接层的账户准入:从用户列表到登录限制规则
2.1 创建账户时就要定好登录边界
Linux里创建用户的命令很简单,useradd加几个参数就完成了,但很多问题恰恰出在最开始。创建SSH可登录用户时,至少要确定三件事:账户是否具备可交互的登录Shell、账户是否加入特定的组、以及家目录的初始权限是否干净。
如果你只是想让某个用户能通过SSH登录并且执行普通命令,可以这样创建:
useradd -m -s /bin/bash zhangsan passwd zhangsan这里-m表示创建家目录,-s /bin/bash指定登录Shell。如果不需要交互式Shell,比如只希望该用户通过SFTP上传下载文件,那可以指定/usr/sbin/nologin或者/bin/false:
useradd -m -s /usr/sbin/nologin uploader这样设置之后,即使该用户通过SSH尝试认证成功,也会在建立Shell阶段被直接拒绝,日志里的表现是“no shell”或者“This account is currently not available”。这在限制服务账户登录时非常实用,既保留了文件传输能力,又封死了命令执行通道。
创建完用户之后,还要留意家目录的所有权和权限。正确状态是:家目录属主是该用户,属组可以是该用户自己的组,权限不能是组可写或全局可写。很多密钥认证失败的问题,根源都出在家目录权限过宽,这一点我在第三部分会详细拆解。
2.2 sshd_config中的用户过滤规则
/etc/ssh/sshd_config是服务端行为的总开关。除了认证方式,它还控制了哪些用户能登录,哪些不能。最常用的两个配置项是AllowUsers和DenyUsers,它们的匹配对象是用户名,可以用空格分隔多个名字,也支持user@host形式做更细粒度的限制。
举一个比较典型的白名单配置:
AllowUsers alice bob zhangsan这样配置之后,只有这三个用户能通过SSH登录,其他所有账户都会被拒绝,包括很多发行版默认开放的其他系统账户。白名单模式的好处在于思路清晰:默认全部拒绝,只放行明确需要的账户。相比之下,黑名单模式(DenyUsers)只能挡住已知的坏账户,一旦某个账户不在列表中,它依然拥有一切默认权限,容易遗漏。
如果公司有多台服务器,维护一份用户名白名单看起来很麻烦,但长远看比黑名单更可控。一个比较折中的做法是配合AllowGroups,按组来放行:
AllowGroups sshlogin这样只要用户加入了sshlogin组,就有权限登录;没有加入的,不管账户是否存在,一律拒绝。组级别的过滤比用户级别更灵活,因为管理员只需要维护一个组,而不需要逐个修改sshd_config。新同事入职时加入sshlogin组即可,离职时移出该组,服务器端配置完全不需要动。
2.3 root登录的默认策略和调整原则
root账户是Linux系统里权限最高的账户,也是SSH爆破攻击最常尝试的目标账户。因此,服务端对root登录的限制是安全基线的重要组成部分。不同发行版对PermitRootLogin默认值并不相同,有的默认允许密码登录,有的默认只允许密钥登录。
我的建议是尽量不要开放root的密码登录,至少设置为prohibit-password:
PermitRootLogin prohibit-password这样root用户只能用密钥登录,密码认证被禁用。即便密码设置得再简单,也不会暴露给爆破脚本。更严格的做法是直接no,完全禁止root通过SSH登录。日常操作建议使用普通用户登录,再通过sudo切换到root执行命令,这样所有操作都有审计记录,同时把爆破攻击的面缩小到普通用户名上。
在配置这些规则的时候,始终记得设置完之后要执行一次配置检查和服务重载:
sshd -t systemctl reload sshdsshd -t是检查配置语法是否正确,如果语法错了,重载会失败,甚至可能导致正常登录中断。这是操作中最容易踩的坑:改完配置没检查就reload,结果配置文件里写错一行,把所有人锁在门外。
3. 家目录与文件权限:约束SSH的最强隐含条件
3.1 为什么authorized_keys必须是600
很多人第一次配置密钥登录时,明明把公钥正确放进了~/.ssh/authorized_keys,客户端也在用对应的私钥去连,却依然收到“Permission denied (publickey)”。这时候去翻服务端日志,通常会看到这样一条提示:
Authentication refused: bad ownership or modes for directory ...
或者是针对authorized_keys文件的权限报错。原因是SSH服务端为了安全,检查了家目录和相关文件的权限位。如果权限过宽,比如整个文件可以给组写,或者家目录全局可读,服务端会认为文件不可信,直接拒绝使用。
核心要求有三条:
- 家目录(
~)不能被组写或全局写,通常是755或700 ~/.ssh目录权限不能超过700~/.ssh/authorized_keys文件权限不能超过600
对应的设置命令是:
chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys chmod 755 ~3.2 权限过宽的潜在安全风险
为什么SSH对权限如此苛刻?理由其实很直白:authorized_keys里的每一行公钥,都代表“允许某把私钥登录本账户”的授权凭证。如果这个文件可以被其他用户写入,那么攻击者只需要往里面追加一行自己的公钥,就能在无人知晓的情况下获得该账户的完整登录权限。这比破解密码要轻松得多,也更难被发现。
同样,如果.ssh目录权限是777,任何人都可以往里面新建文件、修改现有文件,结果和直接写authorized_keys没有本质区别。SSH服务器选择用一种比较机械但有效的方式防住这类攻击:权限不符合预期,干脆不用这些文件,也不管里面的内容对不对。这种“宁可错杀也不放行”的策略,虽然会让新手头疼,但确实是安全上的务实选择。
3.3 批量修复权限的最佳实践
在多服务器场景下,手动逐台chmod太容易遗漏。我习惯写一段简单的脚本,通过遍历用户列表批量修复:
for u in alice bob zhangsan; do home=$(getent passwd "$u" | cut -d: -f6) if [ -d "$home/.ssh" ]; then chmod 700 "$home/.ssh" chmod 600 "$home/.ssh/authorized_keys" 2>/dev/null chown -R "$u":"$(id -gn "$u")" "$home/.ssh" fi done这里不仅重置权限,还顺带修了属主和属组。很多线上问题其实不是权限位不对,而是.ssh目录的属主莫名其妙变成了root,或者另一个用户,结果SSH服务端同样判定不可信。
还有一点容易被忽略:如果你用rsync、git clone等方式向服务器同步文件,同步工具往往会保留源端的权限位,可能把authorized_keys改成644甚至755。所以每次同步完,最好老老实实执行一遍chmod,不要总指望一次性配置好之后永远不变。
3.4 StrictModes的开关问题
sshd_config里有一个StrictModes配置,默认是yes。它的作用就是执行上面说的那些权限校验。有人会为了省事把它改成no,让SSH不再检查权限,结果是一些权限问题被“压”下去了,但安全性也同时被压下去了。
我的建议是不要把StrictModes no当作解决问题的方案。如果遇到权限类报错,宁可花时间找出具体是哪个文件权限不对,也不要一关了之。毕竟,关掉安全检查来换取便利的路径,历史上已经坑过太多团队了。
4. 登录后的权限边界:Shell、命令与文件系统控制
4.1 从全功能Shell到受限Shell
用户在通过SSH认证之后,服务端会为它启动一个Shell会话。默认情况下,这个Shell是完整的交互式Shell,用户能执行系统里全部命令,只要文件系统权限允许。换句话说,SSH认证只是第一道门,进门之后能走多远,取决于用户本身的系统权限和Shell配置。
最轻松的收紧办法,是给用户分配一个受限Shell或者完全不分配Shell。在/etc/passwd中,用户Shell字段可以写成/bin/rbash(受限Bash),也可以写成/usr/sbin/nologin。区别在于,rbash允许用户执行一些命令,但限制了cd、环境变量修改和绝对路径执行等行为;nologin则是完全禁止登录。
实际使用中,nologin更适合服务账户,比如只跑应用、只做备份的账户;rbash的用法比较小众,因为它对用户操作的限制方式不够直观,配置文件一复杂就容易出现“能登录但什么都干不了”的尴尬。真正常见的做法是,把用户按角色拆分到不同的组里,再通过命令权限做好细分控制。
4.2 ForceCommand与SFTP专用账户
比Shell更精细的控制手段是ForceCommand。它可以让用户登录后不进入交互式Shell,而是强制执行指定命令或进入指定子系统。最典型的应用是搭建一个只能SFTP的账户:
Match Group sftponly ChrootDirectory /home/sftp ForceCommand internal-sftp X11Forwarding no AllowTcpForwarding no这段配置的意思是:凡是属于sftponly组的用户,SSH登录后会被强制进入internal-sftp子系统,而不是打开Shell。同时,ChrootDirectory将他们的可见范围锁定在/home/sftp目录下,用户无法看到或访问服务器上的其他路径。X11Forwarding和AllowTcpForwarding都关掉,进一步杜绝端口转发和图形界面转发这类容易变成攻击通道的能力。
这样权限边界就非常清晰:某个团队的文件交换通道,就只能做文件交换,不会被“顺便”当成跳板去连接内网其他机器。
4.3 sudo规则的精髓是最小化
如果说SSH控制了“谁能进门”,那么sudo规则控制的就是“进门之后能拍板做什么”。对大多数运维团队来说,用户的日常操作并不需要真正的root权限,更多是集中在服务重启、日志查看、配置修改等特定动作上。
为运维用户配置精细化sudo规则可以这样写,新建/etc/sudoers.d/ops:
ops_user ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx ops_user ALL=(root) NOPASSWD: /usr/bin/tail -f /var/log/nginx/*.log这样ops_user不需要输入密码就能重启nginx和查看nginx日志,但执行其他需要root权限的命令时,仍然会被拒绝。最小权限原则的最大好处是,即使某人的SSH密钥泄露,攻击者获得的也只是有限的命令执行能力,而不是一句话的root Shell。
需要注意,多条sudo规则之间是或的关系,不是与的关系。只要有一条规则匹配,命令就可能被放行。如果你希望严格控制命令参数,就要写清命令路径并配合参数匹配,否则用户可以通过sudo vim /etc/nginx/nginx.conf这类操作,实际上获得修改任意文件的权限。
5. 多用户场景下的SSH权限体系设计
5.1 共用账户还是个人账户:一次审计之后的教训
某团队在管理一台生产服务器时,长期使用一个叫admin的共用账户,所有人共享同一把密钥。表面上看,登录方便、操作习惯统一,一切都很顺。直到某天有人执行了一条误操作命令,把整个配置目录清空了,复盘时面对的问题变成了:到底是谁执行的?日志里只能看到一个admin账户的登录记录,却无法定位到具体的人。
从那之后,我把这个团队的SSH策略改成了强制个人账户。每个人有自己的用户名和密钥,服务器上不再设置共用运维账户。这个改动付出的代价是新建和维护账户的工作量增加了,但换来的是操作审计的准确性和问题溯源的可能性。
在设计时有一个折中方案:保留共用的操作习惯,但用个人账户加sudo的方式实现。比如所有人登录自己的zhangsan、lisi账户,通过sudo执行运维命令,日志里记录的是真实用户名而不是一个模糊的admin。这样既保留了操作便利,又能在关键时刻追查责任人。
5.2 基于组与角色的批量授权模型
用户少的时候,逐用户配置SSH完全可行;但规模一上来,一个用户一个策略就成了运维泥潭。按角色分组是更合理的演进方向。
常见的分组方式是把运维角色分成三类:
- 管理员组:拥有完整sudo权限,负责服务器全局配置
- 开发组:只能查看日志、重启自己负责的服务
- 审计组:只能读取关键配置文件,不能执行任何修改操作
在服务端体现为几个不同的系统组,通过sshd_config里的AllowGroups放行需要登录的组,再通过sudoers针对不同组配置不同权限:
AllowGroups admin dev audit配合sudo规则:
%admin ALL=(ALL) ALL %dev ALL=(root) NOPASSWD: /usr/bin/systemctl restart app-* %audit ALL=(root) NOPASSWD: /usr/bin/cat /etc/*.conf这种模型的优势在于,人员的增删改只需要维护组成员关系,不需要碰服务器上的SSH配置文件。新人加入dev组自动获得开发权限,调岗时从dev组移出加入audit组,权限变更随之生效,不易遗漏。
5.3 规模化管理:从手工分发公钥到证书签名
当服务器数量达到几十台甚至更多,手工把公钥逐个复制到每台服务器的authorized_keys里会变成一种灾难。一台服务器还好说,几十台服务器意味着一个公钥要维护几十份副本,任何一台漏发或者漏删,都会留下权限漏洞。
更优雅的方案是使用SSH证书认证。思路和CA签名很像:管理员生成一对CA密钥,用CA私钥为用户公钥签名,生成用户证书;所有服务器信任同一个CA公钥,配置TrustedUserCAKeys指向CA公钥文件。用户登录时,服务端不仅验证用户的公钥,还验证这张用户证书是否由受信任的CA签发。
这个方案在实际运维中有几个明显优势:第一,用户证书可以设置有效期,不再需要手动吊销;第二,服务器端只需要配置一次CA公钥,新增用户不需要逐台修改authorized_keys;第三,证书里可以携带principals字段,用来限制该证书允许登录的用户名。
配置一个最简示例,首先在CA服务器上生成CA密钥对:
ssh-keygen -t ed25519 -f /etc/ssh/ca_user_key -C "SSH User CA"为用户公钥签发证书:
ssh-keygen -s /etc/ssh/ca_user_key -I user01 -n zhangsan -V +52w ~/zhangsan.pub在目标服务器的/etc/ssh/sshd_config中配置:
TrustedUserCAKeys /etc/ssh/ca_user_key.pub然后重启sshd服务即可。之后只要使用由该CA签发的证书登录,服务端就会自动信任。这套机制特别适合账号变动频繁、服务器数量大的团队,属于SSH权限管理中的进阶方案,对新手而言可以从了解原理开始,不需要一上来就全量切换。
6. 权限类问题的定位与排查:从现象到根因
6.1 登录失败时先看服务端日志
SSH问题排查有着比较固定的路径,不要一上来就怀疑网络或者防火墙。第一步永远是看服务端的认证日志。主流Linux发行版上,日志位置一般是/var/log/secure或/var/log/auth.log。搜索当前登录尝试的用户名和来源IP,往往能直接看到拒绝原因。
比较典型的日志特征有这些:
No supported authentication methods available:服务端可用认证方式和客户端能提供的对不上,最常见的是服务端禁用了密码认证,而客户端没有提供密钥。Authentication refused: bad ownership or modes:权限问题,家目录或.ssh目录/文件权限过宽。User xxx from IP not allowed because none of user's groups are listed in AllowGroups:用户被组白名单挡在外边。Connection closed by authenticating user:可能是客户端在认证过程中主动断开,需要结合客户端日志查看。
6.2 客户端如何抓有效信息
服务端日志只是问题的一半,客户端的输出同样关键。执行连接时加上多个v可以提升日志详细度:
ssh -vvv zhangsan@server输出里比较值得关注的是Offering public key和Authentications that can continue这类行。前者表示客户端正在尝试拿出哪把公钥进行认证;后者表示目前服务端接受了哪些认证方式。如果客户端一直尝试公钥但服务端始终不认账,问题大概率在服务端上配置错误,而不是网络问题。
如果连ssh命令都执行不了,还可以先测一下端口和协议层连通性:
nc -vz server 22nc只能确认端口是否打开、有没有协议响应,看不了SSH内部的认证细节,真正的排障还是得靠上面两条日志路径。
6.3 一份亲测有效的检查清单
结合多年运维经验,我总结了一份SSH登录和权限问题的检查清单,遇到问题按顺序走一遍,基本能覆盖绝大多数场景:
| 检查项 | 命令/位置 | 说明 |
|---|---|---|
| 用户是否存在 | id zhangsan | 确认用户名没有拼错 |
| 家目录所有权 | ls -ld ~zhangsan | 属主必须是自己,不能是group或world可写 |
| .ssh目录权限 | ls -ld ~zhangsan/.ssh | 不超过700 |
| authorized_keys权限 | ls -l ~zhangsan/.ssh/authorized_keys | 不超过600 |
| 公钥内容格式 | cat ~zhangsan/.ssh/authorized_keys | 必须是ssh-ed25519 AAAA...格式,不能粘贴成多行 |
| 客户端私钥权限 | ls -l ~/.ssh/id_ed25519 | 私钥文件不能是644,必须为600 |
| sshd配置语法 | sshd -t | 修改配置后务必先检查再reload |
| SELinux状态 | getenforce | 如果SELinux为Enforcing,需要检查ssh_home_dir相关布尔值 |
第八项SELinux一般被很多人忽略。开启SELinux的系统上,即使用户和文件权限都正确,某些环境下仍然可能因为SELinux布尔值限制导致authorized_keys不被读取。比如ssh_home_dir布尔值未开启时,SSH无法访问用户家目录下的配置文件,表现也和权限问题几乎一模一样。这时候临时验证可以执行:
setsebool -P ssh_home_dir 1如果问题立即消失,说明是SELinux策略卡住了访问路径,需要结合自身的安全策略决定是否永久放开,还是改用更精确的SELinux文件上下文配置。
6.4 一次真实权限问题的完整复盘
之前有次处理模拟项目X的跳板机权限问题,现象是某位开发新配的密钥始终无法登录,而其他同事的密钥都正常。我按照服务端日志开始查,看到的是典型的bad ownership or modes。
进一步排查发现,这台服务器上另一位管理员为新同事创建账户时,直接执行了useradd -m zhangsan,然后又用root把整个/home/zhangsan的权限递归设置成了777以为这样能“方便开发上传文件”。结果.ssh目录也被带成了777,SSH服务端自然判定文件不可信。
修复过程很简单:
chmod 755 /home/zhangsan chmod 700 /home/zhangsan/.ssh chmod 600 /home/zhangsan/.ssh/authorized_keys然后重新登录,立即恢复正常。这个案例的教训是:不要为了图方便给整个家目录甚至整个/home目录设置过宽的权限;更好的做法是为开发人员单独分配一个共享目录,而不是放开整个家目录。家目录的权限边界,某种意义上比sshd_config里的配置更加重要,因为配置文件出错还能被日志提示,权限越界则往往悄无声息地埋下隐患。
组权限和sftp专用目录的设计也遵循同样的道理,权限收紧在目录层级做,认证控制在sshd配置层做,两者各司其职,整条SSH链路才会清晰可控。