《服务器运维》模块一 · 第 1 ~ 3 讲 听课整理
目标:把一台"裸奔"的服务器加固成只认密钥、守得住、还会自动反击的服务器(本章不修改 SSH 端口)。
服务器接入公网的那一刻,就有自动化扫描程序昼夜不停地遍历 IP,一旦发现 22 端口(SSH)就疯狂尝试常见用户名密码——这叫暴力破解。
# 查看 SSH 服务日志中最近的登录失败记录 # journalctl -u sshd:查看 sshd 的系统日志 # grep -i "failed":只筛出含 failed 的行(-i 忽略大小写) # tail -30:只显示最后 30 条 sudo journalctl -u sshd | grep -i "failed" | tail -30
你会看到大量来自陌生 IP 的失败记录,绝大多数在尝试 root。
| 通道 | 走向 | 是否受 SSH 配置影响 |
|---|---|---|
| SSH(含控制台 Workbench) | 走公网 → 安全组 22 端口 → 服务器 sshd | 受影响:sshd 改崩或 22 被封,这条通道就断,不是后路 |
| VNC | 阿里云带外管理通道,相当于坐在物理显示器前 | 不受影响:不经过安全组、不经过 sshd,用系统账号密码登录 |
控制台 → 实例 → 远程连接 → VNC 远程连接,用普通用户登录后执行:
# 显示当前登录的用户名,应为 study whoami # 显示当前终端设备:VNC 显示 /dev/tty1,SSH 显示 /dev/pts/0 之类 # 两者不同,正好从系统内部印证"这是两条独立的通道" tty exit
登录时服务器用公钥出一道只有对应私钥才能解开的"数学难题",攻击者拿到公钥也没用。
# -t ed25519:现代、短小而安全的算法 # -C:给密钥加备注,方便日后辨认 ssh-keygen -t ed25519 -C "你的名字-服务器加固" # Windows(PowerShell)查看公钥 type $env:USERPROFILE\.ssh\id_ed25519.pub # Mac / Linux 查看公钥(不带 .pub 的那个是私钥,切勿外传) cat ~/.ssh/id_ed25519.pub
复制以 ssh-ed25519 AAAA... 开头的一整行,中间不能有换行。
# 切换到 study 用户(- 表示加载其环境,此后 ~ 就是 /home/study) sudo su - study # 创建存放密钥的目录 mkdir -p ~/.ssh # 目录权限 700:仅本人可进入读写 chmod 700 ~/.ssh # 编辑"已授权公钥"文件,粘贴公钥后 :wq 保存 vi ~/.ssh/authorized_keys # 文件权限 600:仅本人可读写(权限过松 SSH 会静默拒绝登录!) chmod 600 ~/.ssh/authorized_keys # 核对:authorized_keys 应为 -rw------- 且属主是自己 ls -l ~/.ssh
# 回自己的电脑执行:能直接登入且不要求输入密码,才算成功 ssh study@服务器公网IP
错误直觉:把私钥发给老师 ✘。正确做法:把老师的公钥安装到你的服务器上 ✔。
# 新建专供老师巡检的账号 sudo adduser teacher # 为 teacher 创建 .ssh 目录并写入【老师发给你的公钥】 sudo mkdir -p /home/teacher/.ssh sudo vi /home/teacher/.ssh/authorized_keys # 权限与属主 sudo chmod 700 /home/teacher/.ssh sudo chmod 600 /home/teacher/.ssh/authorized_keys sudo chown -R teacher:teacher /home/teacher/.ssh # teacher 没有密码,而 wheel 组用 sudo 必须输入本人密码, # 所以单独为 teacher 配置免密 sudo echo "teacher ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/teacher sudo chmod 440 /etc/sudoers.d/teacher # 检查所有 sudo 配置语法,全部 parsed OK 才放心 sudo visudo -c # 以 teacher 身份测试免密 sudo 是否可用 sudo -u teacher sudo -n true && echo "teacher 免密 sudo 可用"
# sshd_config 是 SSH 服务端主配置,决定谁能登录、用什么方式登录 sudo vi /etc/ssh/sshd_config
找到并修改(没有就新增)以下几项:
PubkeyAuthentication yes # 允许密钥登录(加固的基础) PasswordAuthentication no # 禁止密码登录(务必确认密钥能登录后再改) PermitRootLogin no # 禁止 root 直接远程登录,缩小攻击面 MaxAuthTries 3 # 单次连接最多 3 次认证尝试,拖慢爆破 ClientAliveInterval 300 # 每 300 秒探测一次客户端是否还在 ClientAliveCountMax 2 # 连续 2 次无响应就断开,清理空闲会话 AllowUsers study teacher # 登录白名单,其余用户一律拒绝
# 先验证语法:没有任何输出即正确 sudo sshd -t # reload 不会断开当前连接,比 restart 安全 sudo systemctl reload sshd
PasswordAuthentication no(必须先验证密钥可用);AllowUsers(写错用户名或漏掉 teacher,对应的人就被挡在门外)。两层相互独立、都要配置,原则是同一个词——最小化:只开必须的门。
# 永久放行 SSH 服务(--permanent 写入永久规则) sudo firewall-cmd --permanent --add-service=ssh # 重新加载,让永久规则真正生效(Rocky 上最容易忘的一步) sudo firewall-cmd --reload # 核对当前区域放行的服务/端口 sudo firewall-cmd --list-all # 查看本机实际在监听哪些端口,与安全组规则互相印证 # -t 只看 TCP -l 只看监听中 -n 用数字显示 -p 显示占用端口的进程 sudo ss -tlnp
安全组三点:
-1/-1 只是占位符,不代表把所有端口都打开了。放行 ICMP 只意味着别人能 ping 通你,既无法登录也无法传数据。# Rocky 默认源没有 fail2ban,先装 EPEL 扩展源 sudo dnf install -y epel-release # 再安装 fail2ban 本体 sudo dnf install -y fail2ban # 规范做法:在 jail.d/ 下新建 .local 文件,不直接改 jail.conf, # 这样软件升级时自己的配置不会被覆盖 sudo vi /etc/fail2ban/jail.d/sshd.local
[sshd] # 针对 sshd 服务的防护规则 enabled = true # 启用本规则 backend = systemd # Rocky 9 从 systemd 日志读取失败记录(别抄读日志文件的老写法) maxretry = 3 # 允许的最大失败次数 findtime = 600 # 统计窗口:600 秒(10 分钟) bantime = 3600 # 触发后封禁 3600 秒(1 小时)
连起来读:10 分钟内失败 3 次,就封这个 IP 一小时。
# 启动并设为开机自启 sudo systemctl enable --now fail2ban # 查看 sshd 规则运行状态,重点看 Banned IP list sudo fail2ban-client status sshd
/dev/tty1)ssh study@公网IP 免密码登录成功sudo sshd -t 无报错firewall-cmd --list-all 中只有 ssh,没有多余端口fail2ban-client status sshd 规则正在运行sudo -u teacher sudo -n true 成功目标:为已备案域名配置解析,安装 Nginx,发布带备案号的个人主页,让任何人输入域名都能访问。
| 环节 | 做什么 | 出问题时的表现 |
|---|---|---|
| DNS | 添加 A 记录,域名指向服务器 IP | 域名打不开,IP 能打开 |
| 开门 | 安全组 + firewalld 放行 80 端口 | 连接超时 |
| Nginx | 安装、配置站点、发布主页 | 403、404 或看到默认页 |
排障时按这三个环节从前往后逐一检查。
DNS 是互联网的"通讯录",负责把域名翻译成 IP。三个字段:
@ 代表域名本身,www 代表 www 子域名;在阿里云"云解析 DNS → 解析设置"里添加两条 A 记录:@ 和 www,记录值都填服务器公网 IP。
# 验证解析:看 Address 一栏是不是你的服务器公网 IP nslookup 你的域名 nslookup www.你的域名
"最小化"不是"什么都不开",而是只在服务真正上线时才开放对应的门。本章只开 80;443 等下一章配证书时再开。
安全组:实例 → 安全组 → 配置规则 → 入方向 → 添加规则,协议 TCP,端口 80,授权对象 0.0.0.0/0。
# 主机防火墙永久放行 HTTP 服务(即 TCP 80) sudo firewall-cmd --permanent --add-service=http sudo firewall-cmd --reload # 核对:应包含 ssh 和 http sudo firewall-cmd --list-services
# 从系统软件源安装 Nginx sudo dnf install -y nginx # 立即启动并设为开机自启 sudo systemctl enable --now nginx # 查看运行状态,应显示 active (running),按 q 退出 systemctl status nginx # 查看版本号 nginx -v
浏览器访问 http://你的服务器公网IP,看到 Nginx 默认测试页说明安装成功、80 端口已通。
# 为自己的站点单独建目录,结构清晰、以后加站点不互相干扰 sudo mkdir -p /var/www/mysite # 把目录交给 study,之后上传修改网页不必用 sudo sudo chown -R study:study /var/www/mysite # 755:属主可读写,其他人(包括 Nginx)可进入 sudo chmod 755 /var/www/mysite # 核对:应显示 drwxr-xr-x,属主为 study ls -ld /var/www/mysite # 查看 SELinux 状态:阿里云 Rocky 镜像默认 Disabled getenforce
Nginx 会自动加载 /etc/nginx/conf.d/ 下所有 .conf 结尾的文件。
sudo vi /etc/nginx/conf.d/mysite.conf
server { listen 80; # 监听 80 端口,接收 HTTP 请求 server_name example.com www.example.com; # 带 www 和不带 www 都要写 root /var/www/mysite; # 网站文件所在目录 index index.html; # 访问目录时默认返回的首页文件 access_log /var/log/nginx/mysite_access.log; # 访问日志:记录每一次访问 error_log /var/log/nginx/mysite_error.log; # 错误日志:出问题先看这里 location / { try_files $uri $uri/ =404; # 按路径找文件/目录,找不到返回 404 } }
# 先检查语法,显示 syntax is ok 和 test is successful 才算通过 sudo nginx -t # reload 不中断正在进行的访问 sudo systemctl reload nginx
硬性要求:首页底部必须显示本人 ICP 备案号,并链接到 https://beian.miit.gov.cn/。这是合规要求,不是可选项。
# 方式一:在自己电脑上用 scp 上传(用同一把密钥,不需要密码) scp index.html study@你的服务器公网IP:/var/www/mysite/ # 方式二:直接在服务器上编辑(目录已交给 study,不需要 sudo) vi /var/www/mysite/index.html
AI 生成的主页必须自己检查:备案号是不是自己的、链接是否正确、页面能否正常显示。
用 IP 访问时请求里的 Host 不是你的域名,匹配不上你的站点,于是交给系统默认站点处理——这正是 server_name 在起作用。
# -H 手动指定请求头中的 Host 字段,127.0.0.1 表示访问本机 # 带 Host: 你的域名 → 返回你的主页内容 curl -H "Host: 你的域名" http://127.0.0.1 # 不带你的域名 → 返回默认测试页;head -5 只看前 5 行 curl http://127.0.0.1 | head -5
# 在网站目录新建一个测试页面 echo "<h1>test</h1>" > /var/www/mysite/test.html # 权限改为 600:只有属主 study 能读写,其他人不可读 chmod 600 /var/www/mysite/test.html # -I 只看响应头:结果是 403 Forbidden curl -I -H "Host: 你的域名" http://127.0.0.1/test.html # 查证 Nginx 到底以什么身份运行:root(主进程)+ nginx(工作进程) ps -o user= -C nginx | sort -u # 核对权限:test.html 显示 -rw-------,只有 study 可读 ls -l /var/www/mysite/ # 错误日志里有 (13: Permission denied),直接说明原因 sudo tail -3 /var/log/nginx/mysite_error.log # 修复:644 = 属主可读写,其他人可读 chmod 644 /var/www/mysite/test.html curl -I -H "Host: 你的域名" http://127.0.0.1/test.html # 返回 200 OK # 实验结束,删除测试文件 rm /var/www/mysite/test.html
ls -l 核对权限。网页文件至少 644,目录至少 755,Nginx 才能正常读取。真正读文件的是 nginx 用户(工作进程),不是 study。# tail -f 持续显示文件末尾新增内容,按 Ctrl+C 退出 sudo tail -f /var/log/nginx/mysite_access.log
每刷新一次页面就多一行:来访 IP、时间、访问页面、状态码、浏览器。日志是运维最重要的信息来源,网站出问题第一件事就是看日志。
nslookup 你的域名 返回服务器公网 IP(带 www 的也一样)firewall-cmd --list-services 包含 httpsystemctl is-enabled nginx 显示 enabled,nginx -t 通过前提:http://你的域名 和 http://www.你的域名 都能看到主页,两个域名缺一不可。
HTTP 传输的内容全是明文,链路上任何一台设备都能原样看到你访问了什么页面、提交了什么内容。
# 安装 tcpdump:一个能抓取并显示网络数据包的工具 sudo dnf install -y tcpdump # -i any :所有网卡上抓包 # -A :以文本形式显示数据包内容 # -nn :IP 和端口都用数字显示,不做名称解析 # 'tcp port 80':只抓 80 端口(HTTP)流量 # grep -a -E:只显示含请求行、域名、浏览器信息或网页标题的行 sudo tcpdump -i any -A -nn 'tcp port 80' | grep -a -E "GET |Host: |User-Agent|title"
请同学用手机访问你的 http 站点,屏幕上会出现:请求的页面(GET /)、访问的域名(Host:)、手机和浏览器型号(User-Agent),甚至网页标题(title)。如果页面有登录表单,账号密码也会被看得一清二楚。按 Ctrl+C 停止抓包。
身份认证依靠数字证书——网站的"身份证",上面写明证书属于哪些域名、由谁颁发、什么时候到期。
证书由 CA(证书颁发机构)签发。浏览器和操作系统内置一份受信任的 CA 名单,只有名单上的 CA 签发的证书才会显示安全锁。本章使用 Let's Encrypt:免费、全自动的公益 CA。
http://你的域名/.well-known/acme-challenge/随机文件名;/var/www/mysite;Let's Encrypt 证书有效期只有 90 天,并宣布将在 2028 年 2 月之前逐步缩短到 45 天;同时已于 2025 年停止发送到期提醒邮件。靠人记着续期迟早会忘,自动续期是必须完成的步骤,不是可选项。
安全组:入方向添加规则,协议 TCP,端口 443,授权对象 0.0.0.0/0(控制台通常有"HTTPS(443)"快速添加)。
# 永久放行 HTTPS 服务(即 TCP 443 端口) sudo firewall-cmd --permanent --add-service=https # 重新加载,使永久规则生效 sudo firewall-cmd --reload # 核对:应包含 ssh、http、https sudo firewall-cmd --list-services
# 从 EPEL 源安装 certbot(第 1 章装 fail2ban 时已装过 EPEL) sudo dnf install -y certbot # 查看版本号,确认安装成功 certbot --version
Let's Encrypt 对失败次数有限制:同一域名一小时内验证失败太多次会被暂时拒绝。所以先演练——走完整流程但连接测试环境,既不真签发,也不占用正式环境次数。先演练,再正式,和前两章"改配置之前先测试"是同一个习惯。
# certonly :只申请证书,不自动修改 Nginx 配置 # --webroot :用"网站目录"方式验证,把验证文件放进网站目录由 Nginx 发出 # -w /var/www/mysite:网站目录,即 Nginx 配置里的 root # -d :证书要包含的域名,带 www 和不带 www 各写一个 # -m :联系邮箱 # --agree-tos :同意 Let's Encrypt 服务条款 # --no-eff-email :不订阅 EFF 的新闻邮件 # --dry-run :演练模式,使用测试环境,不真正签发证书 sudo certbot certonly --webroot -w /var/www/mysite \ -d example.com -d www.example.com \ -m 你的邮箱 --agree-tos --no-eff-email --dry-run
行尾的反斜杠 \ 表示"命令还没写完,下一行接着写",三行合起来是一条命令。
看到 The dry run was successful. 说明演练成功。失败则按"解析、80 端口、Nginx"三个方向逐一排查,修好后重新演练。
# 演练成功后,去掉 --dry-run sudo certbot certonly --webroot -w /var/www/mysite \ -d example.com -d www.example.com \ -m 你的邮箱 --agree-tos --no-eff-email
看到 Successfully received certificate. 说明证书已签发。
# 列出本机所有证书:包含哪些域名、到期时间、剩余天数、文件位置 sudo certbot certificates # 证书文件所在目录(目录名取自第一个 -d 域名) sudo ls -l /etc/letsencrypt/live/example.com/ # 查看证书具体内容 # -noout :不输出证书原始编码 # -issuer :显示颁发者,应包含 Let's Encrypt # -dates :显示生效时间 notBefore 和到期时间 notAfter # -ext subjectAltName:显示证书包含的域名列表 sudo openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem \ -noout -issuer -dates -ext subjectAltName
| 文件 | 内容 | 用途 |
|---|---|---|
| fullchain.pem | 你的证书 + 中间证书 | 发给访问者,证明网站身份(本章使用) |
| privkey.pem | 证书私钥 | 留在服务器上用于解密,绝不能泄露(本章使用) |
| cert.pem | 仅你的证书 | 本章不直接使用 |
| chain.pem | 仅中间证书 | 本章不直接使用 |
自己核对两件事:到期时间是否大约在 90 天之后?域名列表中两个域名是否都在?
# 备份文件名不能以 .conf 结尾,否则会被 Nginx 一并加载造成重复配置 sudo cp /etc/nginx/conf.d/mysite.conf /etc/nginx/conf.d/mysite.conf.bak
网站分成两部分:80 端口只负责为证书续期提供验证文件 + 把其余请求跳转到 HTTPS;443 端口才真正提供网站内容。
sudo vi /etc/nginx/conf.d/mysite.conf
# ===== 80 端口:只做两件事 ===== server { listen 80; server_name example.com www.example.com; # 证书续期时,Let's Encrypt 仍通过 HTTP 读取这里的验证文件 location /.well-known/acme-challenge/ { root /var/www/mysite; } # 其余请求永久跳转(301)到同一地址的 HTTPS 版本 location / { return 301 https://$host$request_uri; } } # ===== 443 端口:真正提供网站内容 ===== server { listen 443 ssl; # 监听 443,并启用 SSL/TLS 加密 server_name example.com www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # 证书(含中间证书) ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # 证书私钥 # 只允许较新的安全协议版本,淘汰有漏洞的旧版本 ssl_protocols TLSv1.2 TLSv1.3; root /var/www/mysite; index index.html; access_log /var/log/nginx/mysite_access.log; error_log /var/log/nginx/mysite_error.log; location / { try_files $uri $uri/ =404; } }
$host 和 $request_uri 是 Nginx 变量,分别代表访问者请求的域名和路径。这样访问 http://www.example.com/about.html 会被准确跳转到 https://www.example.com/about.html,而不是一律跳回首页。
注意:全文所有 example.com 都要换成自己的域名(共 6 处:两行 server_name 各 2 处,两行证书路径各 1 处)。
# 检查语法,显示 syntax is ok 和 test is successful 才算通过 sudo nginx -t # 重新加载配置使其生效 sudo systemctl reload nginx
nginx -t 报 cannot load certificate,多半是证书路径中的域名写错,对照 certbot certificates 的结果核对目录名。浏览器访问 http://你的域名:地址自动变为 https://,出现锁的图标;点击可查看颁发者和有效期。
# Windows PowerShell 里要写 curl.exe,因为 PowerShell 的 curl 是另一个命令的别名 # -I 只获取响应头 curl.exe -I http://example.com # 应返回 301,Location 指向 https://example.com/ curl.exe -I https://example.com # 应返回 200 OK curl.exe -I https://www.example.com # 带 www 的同样应返回 200 OK
# certbot 自带 systemd 定时器,每天检查一次,到期前约 30 天自动续期 # enable 保证重启后依然有效,--now 立即开始计时 sudo systemctl enable --now certbot-renew.timer # 查看定时器:NEXT 一列是下次检查的时间 systemctl list-timers 'certbot*'
证书续期只更新磁盘上的文件,Nginx 不会自动读取,必须重载才生效。放在 /etc/letsencrypt/renewal-hooks/deploy/ 目录下的脚本,会在每次续期成功后自动执行。
sudo vi /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
写入以下两行:
#!/bin/bash systemctl reload nginx # 续期成功后重载 Nginx,让新证书生效
# 钩子脚本必须有执行权限才会被调用 sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh # 手动运行一次,确认脚本本身没有问题 sudo /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh # 确认 Nginx 仍在正常运行,应显示 active systemctl is-active nginx
# 模拟一次续期全过程(使用测试环境,不会真的更换证书) sudo certbot renew --dry-run
看到 Congratulations, all simulated renewals succeeded 说明续期流程通了。
# -c 48:抓够 48 个数据包后自动停止 sudo tcpdump -i any -A -nn -c 48 'tcp port 443'
请同学用手机访问你的 https 站点,与第 1 节对比:这次看不到 GET、User-Agent,也看不到网页标题,屏幕上全是无法阅读的乱码——内容已经被加密了。
sudo certbot certificates 显示证书包含两个域名,剩余约 89 天sudo nginx -t 通过curl.exe -I http://域名 返回 301;https 与 https://www 都返回 200systemctl list-timers 'certbot*' 能看到续期定时器sudo certbot renew --dry-run 成功| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| certbot 报 DNS problem: NXDOMAIN | 域名或 www 子域名没有解析记录 | nslookup 两个域名,补上缺失的 A 记录 |
| certbot 报 Timeout during connect | 80 端口不通 | 检查安全组和 firewall-cmd --list-services |
| certbot 报 unauthorized(404) | 验证文件没能被访问到 | 核对 -w 是否与 Nginx 的 root 一致、server_name 是否含该域名 |
| certbot 报 too many failed authorizations | 一小时内失败次数过多 | 等一小时;之后先用 --dry-run 演练成功再正式申请 |
| nginx -t 报 cannot load certificate | 证书路径写错 | 对照 certbot certificates 中的路径修改 |
| https 访问超时 | 443 端口没开 | 检查安全组和 firewalld |
| 访问 http 没有跳转 | 配置未生效或浏览器缓存 | nginx -t 后 reload;用 curl.exe -I 查看响应 |
| 锁的图标带有警告 | 页面引用了 http:// 开头的图片或脚本 | 改为 https:// 或相对路径 |
| 改坏了配置想恢复 | — | sudo cp mysite.conf.bak mysite.conf,再 nginx -t 和 reload |
nginx -t 再 reload;先 --dry-run 再正式申请。从"看得见威胁"到"守得住、会还手",再到"网站能访问、有安全锁、证书自动续期"——三讲下来,一个带域名、备案号与 HTTPS 安全锁的完整个人主页就诞生了。