☰
Linux服务器安全加固实战:从SSH、用户权限到防火墙配置全面解析
2026/10/2 4:08:17 网站建设 项目流程

引言:默认配置,就是最大的攻击面

一台刚交付的云主机,默认状态通常是这样的:22 端口对全网开放、允许 root 用密码直接登录、防火墙处于关闭或"全放行"状态、系统里躺着十几个从没人用过的交互式账户。

把它挂上公网,不出 24 小时,/var/log/secure里就会堆满来自全球各地的暴力破解记录——这已经是被验证过无数次的常态。

真正的痛点不是"不知道该加固",而是加固做不完整、做错了顺序,甚至把自己锁在门外。很多团队的做法是:改个 SSH 端口、装个 Fail2ban,然后心安理得。但攻击者一旦通过某个被遗忘的弱口令账户进来,剩下的横向移动几乎是畅通无阻的。

一个真实案例是:某创业公司上线了一台测试机,只把 SSH 端口从 22 改成了 2222,密码认证没关,也没做账户清理。三天后服务器 CPU 跑满,排查发现攻击者用离职员工留下的test账户登录——该账户口令是test123,从未被禁用。攻击者进来后写入挖矿程序,并尝试用~/.ssh里的旧密钥横向连接内网其他机器。这个案例说明:单点加固无效,必须让网络、身份、权限三个平面同时收敛,缺一不可。

本文不罗列"100 条安全清单",而是围绕三个平面讲清加固的底层逻辑,并给出一套可直接落地的顺序化操作流程。

一、加固的底层模型:三个平面的收敛

任何一台 Linux 服务器的访问控制,都可以拆成三个平面:

平面回答的问题对应机制
网络平面谁的网络包能到达这台机器云安全组、firewalld/nftables
身份平面连上来的人是谁,如何证明SSH 密钥、PAM、密码策略
权限平面证明身份后能做什么sudo、SELinux、文件权限、Capabilities

加固的本质就是在这三个平面上做攻击面收敛,遵循四条原则:默认拒绝(Default Deny)、最小权限(Least Privilege)、纵深防御(Defense in Depth)、可审计可回滚。

顺序上一定是:先网络、再身份、最后权限。反过来做,你会先失去连接。

这三条原则不是口号,它们各自有非常具体的工程含义。默认拒绝意味着每一条放行规则都必须有明确的业务理由,而不是"先开着,以后再说";最小权限意味着任何一个身份(人、进程、服务)拿到的权限,应该刚好够它完成工作,多一分都不要;纵深防御意味着不要指望任何单一机制,SSH 密钥失效时还有防火墙,防火墙被绕过后还有 SELinux;可审计可回滚则是最容易被忽略的一条——所有加固动作都要能回答"谁在什么时候改了什么",并且能在改坏时快速回退。

还有一个容易被忽视的维度:时间。加固不是一次性动作,而是一个持续过程。系统上线时是安全的,不代表三个月后还是安全的——新装的软件可能开了新端口,新入职的同事可能加了一个弱口令账户,某个服务升级后可能把 SELinux 策略重置了。所以本文后面会专门讲审计与监控,让加固"可持续"。

一个常见的顺序反例是:管理员先登录服务器,把/etc/passwd、/etc/shadow权限收紧,又关闭了 root 登录,结果发现自己唯一的普通账户还没加 sudo 权限,SSH 配置又写错了,最终只能通过云控制台的 VNC 救援。正确的做法是:先在网络平面确认只允许自己的管理 IP 访问 SSH,再在身份平面配置好普通账户和密钥,最后才在权限平面做 sudo 与文件权限收敛。每一步都保留一条可回退的通道。

二、SSH 加固:把住唯一的入口

2.1 用密钥认证彻底取代密码

密码认证的根本问题是它"可被猜测"。ed25519 密钥在 256 位安全强度下密钥长度只有 68 字节,比 RSA-4096 更短、更快、更安全。

# 客户端:生成 ed25519 密钥对ssh-keygen-ted25519-a100-C"ops@example.com-2024"-f~/.ssh/id_ed25519_prod# 参数逐条解释:# -t ed25519 指定密钥类型,基于 Curve25519 椭圆曲线,抗侧信道# -a 100 KDF 轮数,对私钥口令做 100 轮派生,暴力破解成本指数级上升# -C 注释,强烈建议写清"用途+归属+年份",多人协作时这是审计线索# -f 私钥输出路径,不同环境用不同密钥,避免一把钥匙开所有门# 分发公钥到服务器(推荐 ssh-copy-id,它会自动处理目录与权限)ssh-copy-id-i~/.ssh/id_ed25519_prod.pub ops@203.0.113.10# 若环境没有 ssh-copy-id,可手工追加,注意 umask 077 防止权限过宽cat~/.ssh/id_ed25519_prod.pub|sshops@203.0.113.10\'umask 077; mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'

密钥认证的安全性来自非对称密码学:私钥永远不离开客户端,认证时服务器发一段随机 challenge,客户端用私钥签名后回传,服务器用authorized_keys里的公钥验证签名。整个过程私钥不上网、密码不传输,中间人即使抓包也拿不到任何可用于登录的东西。这和密码认证有本质区别——密码在网络里(哪怕是加密通道里)是"可被重放的凭证",而签名是"一次性的、绑定本次会话的"。

生成之后,服务端的权限必须严格校准,否则 sshd 会因为"权限过宽"直接拒绝使用该密钥:

# 服务端:权限收敛chmod700~/.sshchmod600~/.ssh/authorized_keyschmod600~/.ssh/id_ed25519_prod# 私钥只有属主可读写chmod644~/.ssh/id_ed25519_prod.pub# 公钥可以宽松# 关键:家目录本身不能被 group/other 写,否则 sshd 的 StrictModes 会拒绝chmodg-w,o-w ~

这里有一个极其常见的坑:很多人只改了.ssh和authorized_keys的权限,却忘了家目录。sshd 的StrictModes yes(默认开启)会检查整条路径,只要家目录是775或777,公钥认证就会失败,而日志里往往只写一句含糊的Authentication refused: bad ownership or modes。排查时用namei -l ~/.ssh/authorized_keys可以一次性看到路径上每一级的权限。

如果系统启用了 SELinux,还要确认文件的上下文标签正确:

restorecon-Rv~/.ssh# 递归恢复默认上下文ls-Z~/.ssh/authorized_keys# 正常应显示 ssh_home_t

手工cp或scp过来的文件经常带着user_home_t之类的标签,SELinux 会默默拦掉,而journalctl -u sshd里会出现 AVC 拒绝记录。用ausearch -m avc -ts recent可以快速定位。

常见问题(FAQ):为什么我配置了密钥,登录时还是提示输入密码?
第一,检查服务端sshd_config中PubkeyAuthentication是否为yes,且没有被Include文件覆盖;第二,检查authorized_keys是否在正确用户的家目录下,文件属主是否为该用户;第三,用ssh -vvv ops@host查看客户端调试输出,重点看Offering public key和Server accepts key两行;第四,确认AllowUsers或AllowGroups是否包含该用户。多数情况下,问题出在权限或 SELinux 上下文,而不是密钥本身。

密钥轮换与备份建议:生产密钥建议每年至少轮换一次,离职人员密钥立即从authorized_keys移除。可以在authorized_keys每行末尾用#注释标注归属和过期时间,例如ssh-ed25519 AAA... ops@example.com-2024#expire=2025-12-31。私钥必须加密存储,并保留一份离线备份;但不要把私钥放在跳板机或 CI 系统的明文变量里。

2.2 sshd_config 的关键参数:逐条讲清为什么

不要直接在/etc/ssh/sshd_config里改,而是用Include引入一个独立的加固片段(OpenSSH 7.3+ 支持),这样升级时不会被覆盖,回滚也只需要删一个文件:

# /etc/ssh/sshd_config.d/99-hardening.conf# ---- 监听与入口 ----Port22022# 换端口只减少扫描噪声,不是安全边界AddressFamily inet# 只用 IPv4,避免 IPv6 规则被遗忘ListenAddress0.0.0.0# ---- 身份认证 ----PermitRootLogin no# 彻底禁止 root 直连,改用普通账户 + sudoPasswordAuthentication no# 关闭密码,只留公钥PermitEmptyPasswords no# 空口令账户一律拒绝KbdInteractiveAuthentication no# 关闭键盘交互(除非上双因素)ChallengeResponseAuthentication no PubkeyAuthenticationyesAuthenticationMethods publickey# 显式声明只允许公钥,防止配置被覆盖后回退# ---- 认证节流 ----MaxAuthTries3# 单连接最多 3 次认证失败即断开MaxSessions4# 单连接最多 4 个会话,限制复用隧道LoginGraceTime30# 30 秒内未完成认证就断开,压缩慢速攻击窗口ClientAliveInterval300# 每 5 分钟探活一次ClientAliveCountMax2# 连续 2 次无响应则断开,回收僵尸会话# ---- 账户白名单 ----AllowUsers deploy ops# 只允许这两个账户登录AllowGroups sshusers# 或者用组来管理,便于批量授权# ---- 功能面收敛 ----X11Forwarding no# 服务器不需要转发 X11AllowAgentForwarding no# 关闭 agent 转发,防止跳板机窃取私钥AllowTcpForwarding no# 不需要隧道就关掉,需要时再按用户开PermitTunnel no GatewayPorts no# ---- 日志 ----UsePAMyesLogLevel VERBOSE# 记录密钥指纹,便于审计"谁用了哪把钥匙"

逐条说一下容易被误解的地方:

PermitRootLogin no而不是prohibit-password。后者允许 root 用密钥登录,看似安全,但它保留了"root 是一个可登录身份"这一事实。一旦某台跳板机上的 root 私钥泄露,攻击者直接就是 root。正确做法是所有人用普通账户登录,需要提权时走sudo,让每一次提权都留下审计记录。

AuthenticationMethods publickey是"锁死"而非"默认"。如果只写PasswordAuthentication no,某次配置合并或软件升级可能把它改回来。显式声明AuthenticationMethods后,即使PasswordAuthentication被改成yes,认证方法仍然只认公钥。这是纵深防御在配置层面的体现。

MaxAuthTries 3与LoginGraceTime 30是一对。前者限制"每次连接能试几次密码",后者限制"每次连接能占用多久"。只改一个,攻击者仍可以用大量并发连接慢慢试。两个一起收紧,单 IP 的爆破速率会被压到很低。

AllowUsers是最容易被低估的一道闸。系统里可能有一堆你不知道的服务账户,它们理论上不该登录,但默认并没有被禁止。加上白名单后,即使某个账户被设了弱口令,只要不在名单里,SSH 这一层就进不来。

实战踩坑:Include文件不生效。OpenSSH 读取Include的顺序是从上到下,如果sshd_config主文件里Include写在了某个参数之后,后面的参数可能覆盖前面的。建议把Include /etc/ssh/sshd_config.d/*.conf放在主文件最顶部,并确保片段文件以99-这类靠后的数字命名。改完后用sshd -T | grep -i include确认加载顺序,用sshd -T | grep -E 'permitrootlogin|passwordauthentication|allowusers'检查最终生效值。

优化建议:用Match块做差异化策略。如果确实有自动化账户需要 TCP 转发,不要全局放开,而是精确授权:

# 只对 tunneluser 开放转发,并限制目标地址Match User tunneluser AllowTcpForwardingyesPermitOpen db.internal:3306 ForceCommand /bin/false

ForceCommand /bin/false让该账户无法获得交互式 shell,只能做端口转发。这样既满足业务,又不会把整个服务器的转发能力暴露给所有人。

改完之后,永远先校验再重载:

sshd-t# 语法校验,配置写错会在这里报出来sshd-T|head-50# 打印最终生效配置,确认没有被其它文件覆盖systemctl reload sshd# 用 reload 而不是 restart,不断开已有连接

reload是这里的生命线。restart会杀掉当前所有 SSH 会话,如果你只有一条连接、配置又写错了,那就直接失联。reload只让新连接使用新配置,已有会话保持不变,给你留了一条"验证新配置是否可用"的退路。务必先开一个新终端验证能登录,再关闭旧终端。

2.3 双因素与 Fail2ban:把暴力破解的成本抬到不划算

公钥认证已经很强,但在合规场景(等保、PCI-DSS)下,往往还要求"双因素"。SSH 上最轻量的做法是publickey + TOTP:

# 服务端安装yuminstall-ygoogle-authenticator# 或 apt install libpam-google-authenticator# 为指定用户生成 TOTP 配置(会输出二维码和备用码,务必备份)google-authenticator-t-d-f-r3-R30-w3# -t 使用 TOTP(基于时间)而非 HOTP(基于计数)# -d 禁止重复使用同一验证码# -f 写入 ~/.google_authenticator# -r 3 -R 30 限速:30 秒内最多 3 次尝试# -w 3 允许前后 3 个时间窗的时钟漂移

然后在/etc/pam.d/sshd顶部加一行,并把 sshd 的认证方法改成两步:

# /etc/pam.d/sshd 第一行auth required pam_google_authenticator.so nullok# nullok 表示"没配置 TOTP 的用户仍可登录",全量上线后应去掉
# sshd 配置KbdInteractiveAuthenticationyesAuthenticationMethods publickey,keyboard-interactive

注意顺序:publickey,keyboard-interactive表示"先验公钥,再验 TOTP",两者必须都过。如果写成keyboard-interactive,publickey,顺序就反了。改完同样要sshd -t+reload,并且先留一个已登录的会话。

TOTP 常见问题:验证码总是错误。九成以上是服务器时间不同步。TOTP 依赖时间窗口,服务器与手机时间差超过 30 秒就可能失败。用chronyc tracking或timedatectl确认 NTP 同步状态,必要时重启chronyd。另外,-w 3虽然容忍时钟漂移,但也会略微降低安全性,内网可设 1 到 2,公网建议保持 3。

Fail2ban 解决的是另一个问题:即使密码认证关了,日志里仍然会有大量无效连接尝试,占用连接数、污染日志。Fail2ban 通过读日志、识别失败模式、调用防火墙封禁来源 IP 来降低噪声:

# /etc/fail2ban/jail.d/sshd.local [sshd] enabled = true port = 22022 filter = sshd backend = systemd # 现代 systemd 系统优先用 systemd 后端 maxretry = 3 findtime = 10m bantime = 24h bantime.increment = true # 累犯递增封禁 bantime.factor = 2 bantime.maxtime = 4w ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24 198.51.100.7 # ignoreip 必须写:本地回环、办公网出口、监控探针、跳板机, # 否则很容易把健康检查或自己的办公 IP 封掉

ignoreip是这里最重要的配置。真实事故里最常见的场景是:某天在家办公,IP 变了,连错两次密码,Fail2ban 直接封了 24 小时,而你又没有控制台权限,只能干等。另一个常见事故是云厂商的负载均衡健康检查源 IP 被误封,导致服务被判定为不健康而摘除。上线 Fail2ban 之前,先把所有"合法但会失败"的来源列出来,全部加进 ignoreip。

验证与排查:

fail2ban-client status sshd# 看当前封禁列表与统计fail2ban-clientsetsshd unbanip1.2.3.4tail-f/var/log/fail2ban.log

如果fail2ban-client status sshd显示Total failed: 0,通常是backend配错了:用systemd后端时读的是 journald,用auto/文件后端时读的是/var/log/secure(RHEL 系)或/var/log/auth.log(Debian 系)。日志系统不一致,Fail2ban 就"看不见"攻击。另一个常见问题是 Fail2ban 的封禁动作与 firewalld 冲突,导致规则被覆盖。可以用fail2ban-client get sshd actions查看当前动作,必要时显式指定banaction = firewallcmd-ipset或banaction = nftables-multiport。

2.4 会话与转发的收敛

SSH 不只是一个登录通道,它还是隧道、转发、代理。默认开启的很多能力在服务器场景下根本用不到,却给攻击者提供了横向移动的跳板:

  • Agent forwarding:开启后,服务器上的 root 可以借用你本地 ssh-agent 里的私钥,去登录你能登录的任何机器。跳板机一旦被拿下,攻击者就能顺着 agent 一路横向。需要跳板时,用ProxyJump而不是 agent 转发:
# 客户端 ~/.ssh/configHost jump HostName203.0.113.10 User ops Port22022IdentityFile ~/.ssh/id_ed25519_prod Host backend HostName10.0.0.20 User deploy ProxyJump jump IdentityFile ~/.ssh/id_ed25519_prod

这样本地 SSH 会先连接 jump,再通过它建立到 backend 的加密通道,私钥始终只在本地使用,jump 上不残留任何可用于登录 backend 的凭证。ProxyJump替代了早期的ProxyCommand nc写法,更简洁也更安全。如果必须用 agent forwarding,至少用ssh-add -t 300设置私钥在 agent 中的存活时间,并避免在跳板机上以 root 身份操作。

  • TcpForwarding:如果业务确实需要隧道,不要全局AllowTcpForwarding yes,而是用Match User tunneluser只对特定账户开放,并配合PermitOpen限制可转发的目标。前面 2.2 的Match示例已经给出,这里再强调一次:PermitOpen是白名单,只允许列出的目标地址和端口,能有效防止攻击者把服务器当成内网扫描代理。

  • X11Forwarding:X11 转发不仅会启动额外进程,还可能把服务器的 X 授权信息暴露给客户端。现代运维几乎不需要它,关闭是零成本。如果确实需要运行图形程序,优先使用 VNC 或 Web 终端,而不是 X11 转发。

  • PermitTunnel:允许 tun 设备隧道,等于把服务器变成 VPN 端点。

更多硬核网安与AI工具包,请扫码获取完整源码!
除非明确要做 VPN 网关,否则关闭。开启后,攻击

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询