简介
本文介绍每次上手一台新的 Linux 服务器后一些 基本设置步骤,包括:
使用一键 DD 脚本重装纯净的 Debian GNU/Linux 操作系统
- SSH 安全和 Fail2ban 设置
- 配置 UFW 防火墙
- 安装 Nginx Web 服务器
- 安装 Docker CE 及必要插件
- 使用 Cloudflare CDN 保护服务和全球加速
并不一定适合所有人的需求,仅作为快速设置新服务器的速查手册。建议搭配以下内容一起使用:
Cloudflare WAF 防护策略简易指南
服务器使用 Cloudflare CDN 的最佳实践
重装系统
选择脚本
网络上支持一键 DD 重装系统的脚本有很多,主要使用这两个:
前者支持的系统选择更多,后者则专注于 Debian GNU/Linux 的最小发行版。两者都在 GitHub 上开源,且经过长期的社区关注和审查,安全性大可以放心。
| 信息 |
|---|
| 无论如何,在正式开始之前,仔细阅读一遍脚本作者的说明(文档说明详细易读),知道自己在做什么,预期会出现什么。 |
我在所有服务器上都一直使用 Debian,下面以 debi 脚本为例。
下载脚本
| 代码块 |
|---|
|
curl -fLO https://raw.githubusercontent.com/bohanyang/debi/master/debi.sh |
执行脚本
赋予脚本可执行权限并开始重装系统
| 代码块 |
|---|
|
chmod +x debi.sh
sudo ./debi.sh \
--version 13 \
--architecture arm64 \
--cloudflare \
--user viamoe \
--authorized-keys-url https://github.com/githubUserName.keys \
--ssh-port 22122 \
--bbr |
上面参数的意义:
- --version 13 指定 Debian 13(代号 Trixie)为安装的版本
- --architecture arm64 指定系统架构,这里的 arm64,只适用于 ARM64 架构的机器(比如甲骨文 ARM VPS)
- --cloudflare 使用 Cloudflare 作为系统默认 DNS
- --user viamoe 创建一个普通用户,用户名为 viamoe,脚本自动配置该用户的 sudo 权限
- --authorized-keys-url https://github.com/githubUserName.keys 指定 SSH 公钥 URL 来源,用于设置 SSH 免密码登录
- --ssh-port 22122 修改 SSH 默认端口为 22122,你应当修改它,减少被自动扫描攻击的风险
- --bbr 启用 BBR(TCP 拥塞控制算法),提升网络在高延迟或跨国网络环境下传输性能
一切就绪,开始重装系统
期间可以使用 VNC 查看系统安装进度,根据服务器性能或网络环境,稍等几分钟,新的系统即将就绪。之后,使用重装阶段预设的用户名密码或 SSH 密钥登录服务器。
安装常用软件包
我们的服务器正在运行的是一个纯净、最小、最新的 Debian GNU/Linux 操作系统,需要安装一些基础或常用的软件包。
基本工具包
| 代码块 |
|---|
|
sudo apt update
sudo apt install \
ca-certificates \
apt-transport-https \
man \
build-essential \
git \
curl \
wget \
unzip \
screen \
btop \
net-tools \
tree \
neovim |
安装 Docker
参考 Docker 官方文档 说明的步骤,下列命令不保证时效性
| 代码块 |
|---|
|
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl status docker |
安装 Nginx
同样地,参考 Nginx 项目文档——在 Debian 上的 安装步骤
| 代码块 |
|---|
|
# install the prerequisites
sudo apt install curl gnupg2 ca-certificates lsb-release debian-archive-keyring
# import gpg key
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
# verify gpg key
mkdir -m 700 ~/.gnupg
gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/debian `lsb_release -cs` nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list
# install nginx
sudo apt update
sudo apt install nginx
# start nginx
sudo systemctl start nginx |
设置 Swap
一般而言,swap 设置为 RAM 大小即可,适当 的 swap 在一定程度上避免 Linux 内核 OOM。这里我们通过设置 Swap File 的方式,它更加灵活。
| 代码块 |
|---|
|
# check swap status, there should be no output
sudo swapon --show
# create a swapfile equal to RAM
# my vds has a 8GB RAM
sudo fallocate -l 8G /swapfile
# or instead of fallocate, use dd command
# 8192 * 1MB = 8GB
sudo dd if=/dev/zero of=/swapfile bs=1M count=8192
# set permission
sudo chmod 600 /swapfile
# format a swap area
sudo mkswap /swapfile
# enable swapfile
sudo swapon /swapfile
# check swap file
sudo swapon --show
free -h
# update fstab auto mount when linux started
sudo nano /etc/fstab
# add following line to end
/swapfile none swap sw 0 0
# check fstab
sudo mount -a |
Linux 还有一个 swappiness 参数控制系统对 swap 的倾向,感兴趣可以 Google 一下。
SSH 基本加固
编辑 SSH 配置文件
| 代码块 |
|---|
|
sudo nvim /etc/ssh/sshd_config |
这里有一些需要关注的配置项目:
- Port 22122 SSH 服务监听的默认端口,我们已在重装脚本中指定
- PermitRootLogin no 禁止 Root 用户直接登录,使用普通用户登录,使用 sudo 提权
- PasswordAuthentication no 禁止使用密码登录 SSH,我们应当使用密钥对或实体安全密钥(如 Yubikey、Canokey 等)进行登
- PermitEmptyPasswords no 禁止空密码登录
- ChallengeResponseAuthentication no 禁用键盘交互式密码认证
- PubkeyAuthentication yes 启用公钥认证,此前已在重装脚本中指定
- UseDNS no 禁用 DNS 反查
- X11Forwarding no 禁用 X11 转发,如果你不知道这是什么那就可以禁用
保持当前的 SSH 会话不要断开连接,重启 SSH 服务
| 代码块 |
|---|
|
sudo systemctl restart sshd |
重开一个 SSH 会话,再次登录当前服务器,确保连接正常,否则返回第一个 SSH 会话中进行检查。
配置 Fail2ban
使用 Fail2ban 保护我们的服务器免受 SSH 爆破,并合理封禁异常的 IP 地址
安装 Fail2ban
| 代码块 |
|---|
|
sudo apt update
sudo apt install fail2ban |
SSH 守护配置
创建一个最小的,仅用于守护 SSH 登录的 Fail2ban 配置文件
| 代码块 |
|---|
|
sudo nvim /etc/fail2ban/jail.local |
配置内容如下:
# 如果您使用动态 IP,不要设置您的公网 IP。 |
# 127.0.0.1/8 和 ::1 忽略本地回环地址。 |
ignoreip = 127.0.0.1/8 ::1 |
# 最大重试次数:如果在 findtime (10 分钟) 内失败了 3 次,则封禁。 |
# SSH 服务的监听端口:确保这里设置为您的自定义端口 22122 |
# 日志文件路径:Debian/Ubuntu/CentOS 通常使用 auth.log,默认配置会自动检测。 |
# logpath = /var/log/auth.log |
# filter:使用默认的 sshd 过滤器(无需修改) |
启动 Fail2ban
| 代码块 |
|---|
|
sudo systemctl enable fail2ban
sudo systemctl restart fail2ban |
检查配置
| 代码块 |
|---|
|
sudo fail2ban-client status sshd
sudo fail2ban-client status |
如果输出显示 Jail status: ... Status: running,在 Jail list 中看到了 sshd,应该就配置成功了。
配置 UFW 防火墙
安装 ufw
设置默认策略
默认拒绝所有传入/入站连接
| 代码块 |
|---|
|
sudo ufw default deny incoming |
默认允许所有传出/出站连接
| 代码块 |
|---|
|
sudo ufw default allow outgoing |
现在大多数服务器有 IPv6 公网 IP,启用 IPv6 支持
| 代码块 |
|---|
|
sudo sed -i 's/IPV6=no/IPV6=yes/' /etc/default/ufw |
允许 SSH 传入/入站连接
| 信息 |
|---|
| 千万不要忘记 SSH 端口,以免自己被「锁在」UFW 外面! |
替换为实际 SSH 端口
| 代码块 |
|---|
|
sudo ufw allow 22122/tcp |
Cloudflare 回源访问规则
Nginx 默认监听端口 80(HTTP) 和 443(HTTPS),要允许 Cloudflare CDN 回源访问,必需将 Cloudflare IP 段添加到 UFW 白名单里。
新建一个 Cloudflare IPs 自动更新脚本
| 代码块 |
|---|
|
sudo nvim /usr/local/bin/ufw-cf-whitelist |
该脚本用于:
- 移除之前旧的 Cloudflare IP 白名单规则
- 获取 Cloudflare 最新的 IPv4 和 IPv6 列表
- 允许列表中每个 IP 段访问 80/443 端口
脚本内容如下:
| 代码块 |
|---|
|
#!/bin/bash
# Cloudflare 官方 IP 列表
IPV4_URL="https://www.cloudflare.com/ips-v4"
IPV6_URL="https://www.cloudflare.com/ips-v6"
echo "=== 1. 清除现有的 Cloudflare UFW 规则..."
# 清除旧的规则(通过删除带有 "Cloudflare" 注释的规则)
# 注意:UFW 不直接支持注释删除,所以这个步骤需要手动或使用更复杂的工具,
# 这里我们采用简单的清理,如果规则不多,可以手动检查 `ufw status numbered`。
# 更好的方法是在每次运行前删除所有 80/443 的 ALLOW 规则,但风险更高。
# 暂时只删除明确的 Cloudflare 规则,以防万一
# sudo ufw delete allow in on any to any port 80/tcp
# sudo ufw delete allow in on any to any port 443/tcp
echo "=== 2. 下载最新的 Cloudflare IP 范围列表..."
# 使用 tempfile 确保下载的文件安全
IPV4_TEMP=$(mktemp)
IPV6_TEMP=$(mktemp)
curl -s $IPV4_URL -o $IPV4_TEMP
curl -s $IPV6_URL -o $IPV6_TEMP
echo "=== 3. 添加 IPv4 规则 (80/443)..."
while read ip; do
if [[ ! -z "$ip" ]]; then
sudo ufw allow proto tcp from $ip to any port 80 comment 'Cloudflare IPv4 HTTP'
sudo ufw allow proto tcp from $ip to any port 443 comment 'Cloudflare IPv4 HTTPS'
fi
done < $IPV4_TEMP
echo "=== 4. 添加 IPv6 规则 (80/443)..."
while read ip; do
if [[ ! -z "$ip" ]]; then
sudo ufw allow proto tcp from $ip to any port 80 comment 'Cloudflare IPv6 HTTP'
sudo ufw allow proto tcp from $ip to any port 443 comment 'Cloudflare IPv6 HTTPS'
fi
done < $IPV6_TEMP
echo "=== 5. 清理临时文件..."
rm $IPV4_TEMP $IPV6_TEMP
echo "=== 6. 完成 Cloudflare 白名单配置。"
ufw status numbered |
赋予脚本可执行权限
| 代码块 |
|---|
|
sudo chmod +x /usr/local/bin/ufw-cf-whitelist |
执行脚本,更新 ufw 规则
启动 ufw 服务
| 代码块 |
|---|
|
sudo ufw enable
sudo ufw reload |
若是提示:
Command may disrupt existing ssh connections. Proceed with operation (y|n)?
检查 ufw 规则
| 代码块 |
|---|
|
sudo ufw status numbered |
计划任务
实际上 Cloudflare IP 段几乎很少变动(最起码最近两年多完全没有变化),因此这个步骤不是必须的。使用 Crontab 计划:
比如每天凌晨 3 点执行一次
| 代码块 |
|---|
|
0 3 * * * /usr/local/bin/ufw-cf-whitelist.sh > /dev/null 2>&1 |
防止 Docker 在 UFW 上打洞
Docker 容器的网络,如果没有做到最佳实践,它是可以在 ufw 上「打洞」的(本质是 iptables),详情可见 chaifeng/ufw-docker。
这个 check-if-docker-break-ufw.sh 脚本用于检查是否出现了这种情况
| 代码块 |
|---|
|
#!/usr/bin/env bash
# ===============================================================
# check-docker-ports.sh
# 检查 Docker 容器端口暴露与 UFW 状态
# ===============================================================
RED="\033[0;31m"
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
RESET="\033[0m"
echo "🔍 正在检查 Docker 端口暴露情况..."
echo "=============================================================="
# 获取 docker ps 输出
docker_ps=$(docker ps --format '{{.Names}}|{{.Ports}}')
if [ -z "$docker_ps" ]; then
echo "❌ 没有正在运行的容器。"
exit 0
fi
printf "%-25s %-25s %-20s\n" "容器名" "绑定地址" "状态"
echo "--------------------------------------------------------------"
while IFS='|' read -r name ports; do
if [[ -z "$ports" ]]; then
printf "%-25s %-25s ${GREEN}%-20s${RESET}\n" "$name" "无对外端口" "安全"
continue
fi
# 分析每个映射
for port in $(echo "$ports" | tr ',' '\n'); do
port=$(echo "$port" | sed 's/ //g')
if [[ "$port" =~ 0\.0\.0\.0 ]]; then
printf "%-25s %-25s ${RED}%-20s${RESET}\n" "$name" "$port" "⚠️ 公网暴露"
elif [[ "$port" =~ 127\.0\.0\.1 ]]; then
printf "%-25s %-25s ${GREEN}%-20s${RESET}\n" "$name" "$port" "本机安全"
else
printf "%-25s %-25s ${YELLOW}%-20s${RESET}\n" "$name" "$port" "内部网络"
fi
done
done <<< "$docker_ps"
echo "--------------------------------------------------------------"
echo "🧱 检查宿主机端口监听状态..."
ss -tlnp | grep -E 'docker|LISTEN' | awk '{print $4, $6}' | sed 's/users://g' | sed 's/"//g' | sed 's/,//g' | sed 's/\[::\]://g'
echo "--------------------------------------------------------------"
echo "🧩 检查 UFW 规则..."
sudo ufw status numbered
echo "=============================================================="
echo "✅ 检查完成。绿色=安全,黄色=内部安全,红色=公网暴露。" |
最简单的方式,不要将任何 Docker 容器的端口绑定到 0.0.0.0 上,只需将暴露端口绑定到 127.0.0.1 上即可。
检查 Nginx 日志
Cloudflare IP 段是纯网段,检查时需要做 CIDR 运算,这里使用 ipcalc 和 sipcalc
| 代码块 |
|---|
|
sudo apt install ipcalc sipcalc |
下面是一个巡检脚本,用于检查所有的 Nginx 虚拟主机的访问日志中的 IP 是否属于 Cloudflare IPs
| 代码块 |
|---|
|
#!/usr/bin/env bash
set -u
NGINX_DIR="/var/log/nginx"
RECENT_LINES=2000
TMP_CF="/tmp/cf_all_ips.txt"
TMP_LOG="/tmp/nginx_logs_combined.txt"
cleanup(){
rm -f "$TMP_CF" "$TMP_LOG"
}
trap cleanup EXIT
echo "=== 获取 Cloudflare IPv4/IPv6 列表… ==="
curl -s https://www.cloudflare.com/ips-v4/ > "$TMP_CF"
curl -s https://www.cloudflare.com/ips-v6/ >> "$TMP_CF"
if [ ! -s "$TMP_CF" ]; then
echo "❌ 失败:无法获取 Cloudflare IP 列表"
exit 1
fi
echo "Cloudflare IP 段已加载:"
wc -l "$TMP_CF"
echo ""
echo "=== 收集 Nginx 日志… ==="
mapfile -d $'\0' LOGS < <(find "$NGINX_DIR" -maxdepth 1 -type f \( -iname "*access.log" -o -iname "*.log" -o -iname "*.gz" \) -print0)
if [ ${#LOGS[@]} -eq 0 ]; then
echo "未找到任何 Nginx access 日志"
exit 0
fi
# 合并最近日志
> "$TMP_LOG"
for LOG in "${LOGS[@]}"; do
if [[ "$LOG" =~ \.gz$ ]]; then
zcat "$LOG" 2>/dev/null >> "$TMP_LOG"
else
cat "$LOG" 2>/dev/null >> "$TMP_LOG"
fi
done
echo "合并日志完成:$(wc -l < "$TMP_LOG") 行"
echo ""
###################################
# 提取最近访问 IP 并对比 CF
###################################
echo "=== 检查最近 $RECENT_LINES 行访问中非 Cloudflare IP ==="
# 提取 IP
tail -n "$RECENT_LINES" "$TMP_LOG" | awk '{print $1}' | sort -u > /tmp/nginx_ips.txt
echo ""
echo "开始比对..."
# 逐个判断是否属于 CF 段
NON_CF_IPS=()
while read -r ip; do
if ! grep -qE "^$(echo "$ip" | sed 's/\./\\./g')(/|$)" "$TMP_CF"; then
# 不是直接文本匹配,而是 CIDR 检查
match=""
while read -r cidr; do
if [[ "$cidr" == *.* ]]; then
# IPv4 CIDR check
if ipcalc -c "$ip" "$cidr" >/dev/null 2>&1; then
match="1"
break
fi
else
# IPv6 CIDR
if sipcalc "$ip" "$cidr" >/dev/null 2>&1; then
match="1"
break
fi
fi
done < "$TMP_CF"
if [ -z "$match" ]; then
NON_CF_IPS+=("$ip")
fi
fi
done < /tmp/nginx_ips.txt
echo ""
if [ ${#NON_CF_IPS[@]} -eq 0 ]; then
echo "✔ 没有发现绕过 Cloudflare 的 IP(全部访问来自 Cloudflare)"
else
echo "⚠ 检测到 **非 Cloudflare** 源站访问(注意安全):"
printf '%s\n' "${NON_CF_IPS[@]}"
fi
echo ""
echo "=== 检查完成,临时文件已删除 ✔ ===" |
示例效果:
| 代码块 |
|---|
|
sudo ./check-security.sh
=== 获取 Cloudflare IPv4/IPv6 列表… ===
Cloudflare IP 段已加载:
20 /tmp/cf_all_ips.txt
=== 收集 Nginx 日志… ===
合并日志完成:26384 行
=== 检查最近 2000 行访问中非 Cloudflare IP ===
开始比对...
✔ 没有发现绕过 Cloudflare 的 IP(全部访问来自 Cloudflare)
=== 检查完成,临时文件已删除 ✔ === |