找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4322

积分

0

好友

564

主题
发表于 1 小时前 | 查看: 3| 回复: 0

SSH 加固的难点不在于写出三行配置,而在于避免把自己锁在服务器外。可靠做法是保留现有会话,先确认第二条管理通道和密钥有效,再分层修改 sshd、主机防火墙与上游访问控制,最后从新的独立终端验证。本文以使用 OpenSSH、systemd 和 firewalld 的 RHEL 8/9 系列为基线;Ubuntu/Debian 的包名、防火墙工具和服务名不同,不应直接照搬。文中的 <管理网CIDR><新端口><管理员用户><服务器IP> 首次使用前必须替换,例如管理网 CIDR 可为 192.0.2.0/24,新端口应选择 1024~65535 中未占用的 TCP 端口。

先定义边界,而不是立即改配置

禁用密码能消除弱口令和撞库入口;改端口只能降低互联网扫描噪声,不能替代认证加固;限制 IP 的安全收益最大,但依赖稳定的办公出口、堡垒机或 VPN 网段。三项措施属于不同控制层,必须分别验证。实施前应确认云控制台、虚拟机控制台、带外管理或同机房救援终端可用,并记录当前允许登录的账号、来源地址、端口和跳板链路。

先固定审计上下文,避免在错误主机上操作:

hostnamectl
who -u
ip -brief address
ip route

who -u 可确认当前登录会话和来源;路由信息用于识别默认出口。不要仅凭 shell 提示符判断主机身份。

确认 OpenSSH 与服务单元。RHEL 8/9 通常使用 sshd.service

rpm -q openssh-server
sshd -V 2>&1 | head -n 1
systemctl status sshd --no-pager
systemctl cat sshd

部分 OpenSSH 版本的 sshd -V 输出到标准错误,因此需要 2>&1systemctl cat 能发现单元覆盖文件,防止遗漏通过 EnvironmentFile 或启动参数指定的其他配置。

检查监听端口和占用进程:

ss -lntp | awk 'NR==1 || /sshd/'
sudo lsof -nP -iTCP -sTCP:LISTEN | grep -E 'sshd|:<新端口>'

如果新端口已被其他进程占用,必须先重新选端口;不要杀掉未知进程来“腾端口”。

证明密钥登录已经可用

禁用密码前,管理员账户必须存在、未锁定、具有可用 shell,且公钥文件权限正确:

getent passwd <管理员用户>
sudo passwd -S <管理员用户>
sudo namei -l /home/<管理员用户>/.ssh/authorized_keys
sudo stat -c '%U:%G %a %n' \
  /home/<管理员用户> \
  /home/<管理员用户>/.ssh \
  /home/<管理员用户>/.ssh/authorized_keys

authorized_keys 一般应由目标用户拥有,目录权限建议 700、文件权限 600;家目录不能由其他用户可写。StrictModes yes 时权限异常会导致公钥被拒绝。

在客户端创建 Ed25519 密钥。若组织要求 FIPS 模式,Ed25519 可能不可用,应依合规基线选择 RSA 3072/4096 并确认客户端与服务端算法支持:

ssh-keygen -t ed25519 -a 100 \
  -f ~/.ssh/<服务器别名>_ed25519 \
  -C '<管理员用户>@<服务器别名>'
ssh-keygen -lf ~/.ssh/<服务器别名>_ed25519.pub

私钥应设置口令并存放在受控终端或密钥代理中。-a 100 增加私钥口令派生轮次,不影响服务器认证速度。

在仍可用的既有登录方式下安装公钥,并显式修复权限:

#!/usr/bin/env bash
set -euo pipefail

ADMIN_USER="<管理员用户>"
PUBKEY_FILE="<本地公钥文件>"
SERVER="<服务器IP>"

ssh-copy-id -i "${PUBKEY_FILE}" "${ADMIN_USER}@${SERVER}"
ssh "${ADMIN_USER}@${SERVER}" \
  'umask 077; chmod 700 ~/.ssh; chmod 600 ~/.ssh/authorized_keys'

<本地公钥文件> 替换为 .pub 文件路径。生产环境若禁止 ssh-copy-id,可通过配置管理系统分发,避免聊天或工单传递私钥。

强制客户端只尝试指定密钥,证明成功不是密码回退:

ssh -vv \
  -o PreferredAuthentications=publickey \
  -o PasswordAuthentication=no \
  -o IdentitiesOnly=yes \
  -i ~/.ssh/<服务器别名>_ed25519 \
  <管理员用户>@<服务器IP>

调试信息中应看到服务端接受所选公钥并进入会话。若仍要求密码,先查服务端日志,不得继续禁用密码。

识别真正生效的 sshd 配置

OpenSSH 配置可能来自主文件、Include 目录以及 Match 条件块。文本搜索只能发现声明,sshd -T 才能展示解析后的全局有效值:

sudo grep -RInE \
  '^[[:space:]]*(Include|Port|PasswordAuthentication|KbdInteractiveAuthentication|PubkeyAuthentication|PermitRootLogin|AllowUsers|AllowGroups|Match)\b' \
  /etc/ssh/sshd_config /etc/ssh/sshd_config.d 2>/dev/null
sudo sshd -T | grep -E \
  '^(port|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|permitrootlogin|usepam) '

Match 会按用户、来源和地址改变结果,应使用 -C 模拟真实连接:

sudo sshd -T \
  -C user=<管理员用户>,addr=<管理端IP>,host=<服务器主机名> \
  | grep -E '^(port|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|permitrootlogin|allowusers|allowgroups) '

<管理端IP> 是客户端经过 NAT 后被服务器看到的源地址,可从当前 whoss 或日志确认,不能直接填笔记本内网地址。

备份并使用 drop-in 加固

先创建带时间戳、权限受控的备份,并保存校验和:

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="/root/sshd-backup-$(date +%Y%m%d-%H%M%S)"
install -d -m 0700 "${BACKUP_DIR}"
cp -a /etc/ssh/sshd_config "${BACKUP_DIR}/"
if [[ -d /etc/ssh/sshd_config.d ]]; then
  cp -a /etc/ssh/sshd_config.d "${BACKUP_DIR}/"
fi
sha256sum "${BACKUP_DIR}/sshd_config" >"${BACKUP_DIR}/SHA256SUMS"
printf 'backup=%s\n' "${BACKUP_DIR}"

记录输出路径到变更单。RHEL 的 OpenSSH 配置通常支持 /etc/ssh/sshd_config.d/*.conf,但必须先确认主文件存在对应 Include;否则 drop-in 不会加载。

创建一个职责单一的加固文件。以下配置保留 PAM 的账户和会话处理,但关闭密码与键盘交互认证:

# /etc/ssh/sshd_config.d/60-login-hardening.conf
Port <新端口>
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
UsePAM yes
MaxAuthTries 3
LoginGraceTime 30
AllowGroups ssh-admins

AllowGroups ssh-admins 要求目标账号确实属于该组。启用前检查并建立授权关系:

getent group ssh-admins
id <管理员用户>
sudo usermod -aG ssh-admins <管理员用户>
getent group ssh-admins

新组成员资格对新会话生效;当前 shell 的 groups 结果不会自动刷新。至少保留一个已验证的管理会话。

不要直接用重定向覆盖配置。可以安全地暂存、设置所有者与权限后原子安装:

#!/usr/bin/env bash
set -euo pipefail

NEW_PORT="<新端口>"
TMP_FILE="$(mktemp)"
trap 'rm -f "${TMP_FILE}"' EXIT

sed "s/<新端口>/${NEW_PORT}/g" \
  < /root/60-login-hardening.conf.template > "${TMP_FILE}"
sudo install -o root -g root -m 0600 "${TMP_FILE}" \
  /etc/ssh/sshd_config.d/60-login-hardening.conf
sudo sshd -t

这里模板文件应由变更系统预先下发。sshd -t 无输出通常表示语法通过;它不能证明防火墙、SELinux、授权组或密钥均正确。

端口变更涉及 SELinux 与防火墙

RHEL 启用 SELinux enforcing 时,sshd 监听非标准端口需要为 ssh_port_t 增加端口标签。先读后改:

getenforce
sudo semanage port -l | grep '^ssh_port_t'
sudo semanage port -l | awk -v p='<新端口>' '$1=="ssh_port_t" && $0 ~ p {print}'

semanage 不存在,RHEL 8/9 可安装 policycoreutils-python-utils。新增端口用 -a,已经存在但属于其他类型时不能盲目 -m,应先评估该服务影响:

sudo dnf install -y policycoreutils-python-utils
sudo semanage port -a -t ssh_port_t -p tcp <新端口>
sudo semanage port -l | grep '^ssh_port_t'

这是安全策略修改。回滚时只有确认该端口标签是本次新增且没有其他 sshd 实例依赖,才执行 semanage port -d -t ssh_port_t -p tcp <新端口>

确认 firewalld 当前区域与网卡绑定,避免把规则加到未生效区域:

sudo firewall-cmd --state
sudo firewall-cmd --get-active-zones
sudo firewall-cmd --get-zone-of-interface=<网卡名>
sudo firewall-cmd --zone=<区域名> --list-all

<网卡名> 可从 ip -brief link 获取,<区域名> 替换为实际活动区域。远程修改防火墙属于高风险操作:先确认控制台可用、当前来源 CIDR 正确,并暂时保留旧 SSH 端口。

使用 rich rule 只允许管理网访问新端口。先加运行时规则并验证,不要一开始就写永久配置:

sudo firewall-cmd --zone=<区域名> \
  --add-rich-rule='rule family="ipv4" source address="<管理网CIDR>" port port="<新端口>" protocol="tcp" accept'
sudo firewall-cmd --zone=<区域名> --list-rich-rules

若还有 IPv6 管理网络,应另建 family="ipv6" 规则并填写 IPv6 CIDR;不能以 IPv4 规则推断 IPv6 已受限。云安全组、边界防火墙和主机规则都要同步评估。

确认内核和防火墙看到新端口前,不要关闭 22:

sudo systemctl reload sshd
sudo systemctl status sshd --no-pager
sudo ss -lntp '( sport = :<新端口> )'
sudo journalctl -u sshd --since '-5 minutes' --no-pager

优先 reload,因为它通常不终止已建立会话;若服务单元不支持 reload,再评估 restart。配置语法错误时 systemd 日志和 sshd -t 是第一证据。

从另一终端完成闭环验证

保持旧会话不动,从管理网内另一终端连接新端口:

ssh -p <新端口> \
  -o PreferredAuthentications=publickey \
  -o PasswordAuthentication=no \
  -o IdentitiesOnly=yes \
  -i ~/.ssh/<服务器别名>_ed25519 \
  <管理员用户>@<服务器IP>

登录后运行 idsudo -n true 或按组织流程验证提权。若 sudo 需要密码,禁用 SSH 密码并不影响 sudo 的本地认证策略。

验证密码确实被拒绝。该测试应从授权来源执行,且不要连续大量尝试触发封禁:

ssh -p <新端口> \
  -o PubkeyAuthentication=no \
  -o PreferredAuthentications=password,keyboard-interactive \
  -o NumberOfPasswordPrompts=1 \
  <管理员用户>@<服务器IP>

预期结果是服务端不提供可用的密码或键盘交互方法。任何成功登录都说明 Match、配置覆盖或其他认证方式仍在生效,应立即检查 sshd -T -C

在服务端用日志把客户端现象转换为证据:

sudo journalctl -u sshd --since '-15 minutes' --no-pager \
  | grep -E 'Accepted|Failed|Invalid user|Connection|error|refused'
sudo ausearch -m AVC,USER_LOGIN -ts recent -i 2>/dev/null

公钥被拒绝常见于权限、SELinux 上下文、算法策略或 AllowGroups。若文件从其他目录复制,修复上下文:

sudo restorecon -RFv /home/<管理员用户>/.ssh
sudo ls -lZ /home/<管理员用户>/.ssh
sudo sshd -T -C user=<管理员用户>,addr=<管理端IP>,host=<服务器主机名> \
  | grep -E 'authorizedkeysfile|pubkeyauthentication|allowgroups'

固化规则并撤销旧入口

只有在新端口、公钥、sudo、日志和至少一个独立会话均验证后,才把新规则写入永久配置:

sudo firewall-cmd --permanent --zone=<区域名> \
  --add-rich-rule='rule family="ipv4" source address="<管理网CIDR>" port port="<新端口>" protocol="tcp" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --zone=<区域名> --list-rich-rules

--reload 会用永久配置重建运行时规则,应先检查永久区是否存在其他未同步的重要临时规则。随后从授权网再次连接。

删除旧端口前确认它来自 service、port 还是 rich rule:

sudo firewall-cmd --zone=<区域名> --query-service=ssh
sudo firewall-cmd --permanent --zone=<区域名> --query-service=ssh
sudo firewall-cmd --zone=<区域名> --list-ports
sudo firewall-cmd --zone=<区域名> --list-rich-rules

若旧入口由 ssh service 开放,在新通道验证完成后再撤销。该操作会阻断仍使用 22 端口的新连接,执行前需通知使用者并确认自动化、监控和备份任务已改端口:

sudo firewall-cmd --permanent --zone=<区域名> --remove-service=ssh
sudo firewall-cmd --reload
sudo firewall-cmd --zone=<区域名> --query-service=ssh

同时检查 sshd 配置中不再监听 22。多个 Port 指令会让 sshd 同时监听多个端口;以 sshd -Tss 为准。

上游访问控制与自动化影响

主机防火墙不是唯一入口。云安全组或硬件防火墙也应只允许堡垒机/VPN CIDR 到新端口,并在灰度验证后移除 22。变更前导出规则或截图留档;通过云平台变更时优先使用预览、策略差异和审批功能。切勿先删除旧规则再创建新规则。

扫描依赖和硬编码端口:

sudo grep -RInE '(^|[^0-9])22([^0-9]|$)|ssh .*-[pP]' \
  /etc/cron.d /etc/cron.daily /etc/systemd/system /opt 2>/dev/null
systemctl list-timers --all
sudo crontab -l

还需检查 Ansible inventory、Git 部署密钥、SFTP、rsync、监控探针、备份软件和堡垒机资产配置。自动化使用的客户端配置可明确主机、端口和身份文件:

# ~/.ssh/config
Host <服务器别名>
    HostName <服务器IP>
    User <管理员用户>
    Port <新端口>
    IdentityFile ~/.ssh/<服务器别名>_ed25519
    IdentitiesOnly yes
    PreferredAuthentications publickey

客户端执行 ssh -G <服务器别名> 可查看最终解析值,避免通配 Host * 覆盖预期。

典型故障的证据链

“连接超时”通常指网络路径丢弃:依次检查服务端监听、主机防火墙计数、云安全组、路由与 NAT 出口。“Connection refused”通常表示目标可达但没有进程监听或被主动拒绝。“Permission denied (publickey)”说明网络与 SSH 握手已经完成,重点转向账号、公钥、权限和策略。

抓取有限的数据包确认请求是否抵达。抓包可能包含地址等敏感元数据,应限定接口、端口和时长:

sudo timeout 30 tcpdump -ni <网卡名> \
  'tcp port <新端口> and host <管理端IP>'

若看不到 SYN,问题在主机之前或客户端路由;看到 SYN 但无 SYN-ACK,应查监听和防火墙;三次握手完成后断开,则查 sshd 日志。这个判断必须与实际抓包和日志互证。

配置变更后做一次可重复巡检:

#!/usr/bin/env bash
set -euo pipefail

PORT="<新端口>"
ADMIN_USER="<管理员用户>"
CLIENT_IP="<管理端IP>"
HOST_NAME="$(hostname -f 2>/dev/null || hostname)"

sshd -t
sshd -T -C "user=${ADMIN_USER},addr=${CLIENT_IP},host=${HOST_NAME}" \
  | grep -E '^(port|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|permitrootlogin) '
ss -lnt "sport = :${PORT}"
systemctl is-active --quiet sshd
journalctl -u sshd --since '-10 minutes' --no-pager | tail -n 50

该脚本只做本机检查,不能替代外部网络验证。输出应作为变更证据保存,而不是只记录“测试成功”。

回滚设计

回滚顺序应保证始终存在一条入口:在控制台或既有会话中恢复旧端口的 SELinux 与防火墙许可,恢复 sshd 配置并通过语法检查,reload 后确认 22 监听,再从外部验证。不要先删除新规则。

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="<备份目录>"
ZONE="<区域名>"

firewall-cmd --zone="${ZONE}" --add-service=ssh
firewall-cmd --permanent --zone="${ZONE}" --add-service=ssh
cp -a "${BACKUP_DIR}/sshd_config" /etc/ssh/sshd_config
if [[ -d "${BACKUP_DIR}/sshd_config.d" ]]; then
  rm -f /etc/ssh/sshd_config.d/60-login-hardening.conf
  cp -a "${BACKUP_DIR}/sshd_config.d/." /etc/ssh/sshd_config.d/
fi
sshd -t
systemctl reload sshd
ss -lntp '( sport = :22 )'

脚本中的 rm -f 只针对本次明确创建的文件,执行前核对备份目录和文件差异。若原环境没有 drop-in 目录,不应复制不存在的内容。恢复后,从外部使用原端口验证,再删除本次新端口规则和 SELinux 标签。

最终验收至少包括:授权来源可用密钥登录;非授权来源在网络层不可达;密码与键盘交互认证被拒;root 不能直接登录;新端口在 sshd、SELinux、防火墙和上游策略四处一致;自动化任务已更新;旧端口没有监听或开放;回滚路径经过只读核对。安全配置的结论不能来自单一配置文件,必须由有效配置、监听状态、网络策略、登录测试和日志共同证明。更多系统安全加固实践,欢迎到 云栈社区 深入探讨。

进一步收紧密钥与账号边界

禁用密码后,密钥本身就成为主要凭据。应建立发放人、使用人、用途、指纹、创建时间和撤销时间台账。多人共用一个 Unix 账号会削弱审计能力;更合理的是一人一账号,通过统一的 ssh-admins 组授予登录权限,再使用 sudo 完成特权操作。离职或终端遗失时,只撤销对应公钥,不影响其他管理员。

检查授权文件中的密钥类型、注释与重复项,但不要把完整公钥复制到普通工单:

sudo awk 'NF >= 2 && $1 !~ /^#/ {print $1, $2}' \
  /home/<管理员用户>/.ssh/authorized_keys \
  | while read -r type key; do
      printf '%s ' "${type}"
      printf '%s %s\n' "${type}" "${key}" | ssh-keygen -lf -
    done

若行首含 from=command= 等选项,上述简化解析不适用,应逐行审查。自动化密钥宜增加来源限制和最小命令权限,例如只允许固定网段并禁止转发:

from="<自动化网CIDR>",restrict ssh-ed25519 <公钥内容> <用途说明>

restrict 需要较新的 OpenSSH,等价于关闭多类转发与代理能力;具体支持情况用 man authorized_keys 和当前版本确认。若任务需要端口转发或分配 PTY,不应盲目使用。<公钥内容> 仅放公钥的 Base64 字段,绝不能放私钥。

OpenSSH 还可使用证书认证,由内部 CA 为用户公钥签发短期证书,从而避免逐台维护永久密钥。引入 CA 会扩大信任域,必须保护 CA 私钥、限制签发主体并测试撤销流程。服务端信任配置示意:

TrustedUserCAKeys /etc/ssh/user_ca.pub
AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u

AuthorizedPrincipalsFile 必须由 root 管理且权限正确。证书 principals 应映射实际角色,不能让所有证书默认登录所有账号。变更仍需经过 sshd -tsshd -T -C 和独立会话验证。

IPv4、IPv6 与多网卡陷阱

只限制 IPv4 时,sshd 仍可能通过 IPv6 全局地址开放。检查所有监听族和地址:

sudo ss -lntp | grep sshd
ip -6 address show scope global
sudo firewall-cmd --zone=<区域名> --list-all

若环境明确不使用 IPv6,不应仅为 SSH 临时在内核全局禁用 IPv6,因为这会影响其他服务。更稳妥的是在网络策略中建立对应 IPv6 规则,或经架构评审后用 sshd 的 AddressFamily/ListenAddress 限定监听。多网卡服务器还需确认管理流量进入的接口属于哪个 firewalld zone;错误的 zone 绑定会造成规则看似存在但不生效。

通过指定监听地址缩小暴露面时,必须确认该地址稳定存在:

# 仅示意,地址必须属于本机稳定的管理接口
ListenAddress <管理接口IP>:<新端口>

DHCP 地址、漂移 IP 或集群 VIP 不应未经验证直接绑定。地址不存在时 sshd 可能无法启动或无法监听预期入口,语法检查也未必能覆盖启动时的地址状态。

审计与持续检测

加固不是一次性操作。应监控失败登录、未知来源、配置漂移和监听端口变化。查询近一天失败与成功事件:

sudo journalctl -u sshd --since '-24 hours' --no-pager \
  | grep -E 'Accepted publickey|Failed publickey|Failed password|Invalid user' \
  | tail -n 500

日志保留周期和转发策略应满足审计要求。对集中日志告警时,需要按源地址、用户、主机和时间窗口聚合,避免一次拼写错误触发高优先级事件,也不能用改端口后扫描减少来宣称认证风险消失。

保存一份已批准的有效配置基线,并定期比较:

sudo sshd -T | sort > /var/lib/<巡检目录>/sshd-effective.current
sudo diff -u \
  /var/lib/<巡检目录>/sshd-effective.approved \
  /var/lib/<巡检目录>/sshd-effective.current

<巡检目录> 应是 root 管理的专用目录。有效配置可能因软件升级新增默认项,差异需要人工判断,不能自动把当前值覆盖成批准基线。

软件更新也可能改变默认算法或废弃选项。补丁前后分别运行:

rpm -q openssh openssh-server openssl
sudo sshd -t
sudo sshd -T | grep -E '^(ciphers|macs|kexalgorithms|hostkeyalgorithms) '
ssh -Q cipher
ssh -Q kex

不要从网上复制一份静态算法列表长期冻结;过度限制会造成旧客户端中断,也可能阻止未来安全默认值生效。算法策略应来自组织合规基线,并在客户端矩阵中验证。

堡垒机与紧急账号

限制 IP 最适合与堡垒机或 VPN 配合。服务器日志看到的来源可能只有堡垒机地址,因此用户身份审计必须在堡垒机和目标机之间保持关联。不要共享堡垒机账号,也不要让普通用户任意使用 ProxyCommand 绕开记录策略。客户端跳转配置示意:

Host <目标别名>
    HostName <服务器IP>
    Port <新端口>
    User <管理员用户>
    ProxyJump <堡垒机别名>
    IdentityFile ~/.ssh/<服务器别名>_ed25519

紧急账号(break-glass)应有独立、受控、可审计的凭据和启用流程,而不是为了“保险”永久保留互联网密码登录。定期演练控制台、恢复旧端口、恢复密钥和撤销临时规则,才能证明回滚真实可用。

变更窗口的最终清单

执行前核对资产、窗口、影响方、控制台、备份、管理网 CIDR、云策略权限和至少两名验证人员;执行中按“密钥验证—配置语法—SELinux—临时防火墙—reload—外部验证—永久规则—关闭旧入口”的顺序推进;执行后观察认证日志、自动化任务与集中监控至少一个业务周期。任何一步证据不完整,都应停在仍保留旧通道的状态,而不是继续叠加变化。




上一篇:Meta新编程模型Muse Spark 1.2发布:价格低于DeepSeek,代价是贡献数据
下一篇:n8n漏洞挖掘实录:从公网Form到容器RCE的5步攻击链
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-7 04:19 , Processed in 0.820802 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表