本文内容适用于 Ubuntu 系统;部分本地操作以在 macOS 下完成为例。

本文部分命令需要以 root 用户或 sudo 权限执行。

最近在 VPS 上搭好了自己的 Tiny Tiny RSS 服务和这个 Blog(有关这两部分的内容我稍后也会写)。而在此之前,我在 VPS 上重新安装了 Ubuntu 18.04 LTS,本文就记录下重装后的一些简单配置过程。

安全

SSH 安全

为了避免自己的 VPS 被 ssh 弱口令扫描,我们不妨使用公私钥对进行 ssh 登录,并关闭密码登录。

生成并添加密钥

1. 在本地生成密钥

在本机 Terminal 中执行:

ssh-keygen -t ed25519  # ED25519 算法,长度固定为 256 bits,安全性和性能最佳,兼容性稍差

# 或者
ssh-keygen -t rsa -b 2048  # RSA 算法,指定密钥长度为 2048 bits,兼容性最佳

提示 Enter file in which to save the key (/Users/~/.ssh/id_ed25519):
# 使用默认文件位置,直接回车即可

提示 Enter passphrase (empty for no passphrase):
# 设置私钥的密码(留空即为免密码)

提示 Enter same passphrase again:
# 再次输入私钥的密码

此时就已经在 ~/.ssh/ 下生成了 id_ed25519 (或 id_rsa) (私钥) 和 id_ed25519.pub (或 id_rsa.pub) (公钥) 两个文件。

2. 在服务器端添加公钥

使用 scp 命令将公钥上传到服务器:

scp ~/.ssh/id_ed25519.pub root@<VPS域名或IP>:/root/

# 输入 ssh 密码并回车确认

ssh 到服务器上,随后执行

mkdir ~/.ssh && chmod 700 ~/.ssh

mv id_ed25519.pub ~/.ssh && cd ~/.ssh

mv id_ed25519.pub authorized_keys && chmod 600 authorized_keys

此时就可以使用密钥进行 ssh 登录了。

3. macOS 本地配置[1]

如果不想每次使用密钥登录时都要输入密码,那么你可以编辑 ssh 配置文件将密码存储到钥匙串:

编辑 ssh 配置文件:vim ~/.ssh/config

写入如下内容:

Host *
  AddKeysToAgent yes  # 用 ssh-agent 管理密钥
  UseKeychain yes  # 将密码存储到钥匙串中
  IdentityFile ~/.ssh/id_ed25519  # 指定私钥路径

如果你想通过代理 ssh 到你的服务器,那么再写入如下内容:

Host <VPS域名或IP>
  ProxyCommand nc -X 5 -x 127.0.0.1:6153 %h %p

# nc, netcat
# -X 5, 指定使用 SOCKS5 代理
# -x, 指定代理的主机地址和端口,示例中为本机的 6153 端口
# %h %p, 用于替换 ssh 实际连接的主机地址和端口
关闭 ssh 的密码登录 & 防止 Broken pipe 错误

设置好密钥登录后,我们就可以关闭 ssh 的密码登录了。同时,为了避免在使用 ssh 时空闲连接时间过长而导致 Write failed: Broken pipe 错误,我们可以在 ssh 配置文件中设置 ClientAliveInterval 参数,这会允许服务器在一定时间内发送一个特定的包给客户端,一旦超时,则说明断线,就关闭连接,避免了空闲连接时间过长报错。

编辑 ssh 配置文件:vim /etc/ssh/sshd_config

PasswordAuthentication yes 改为 PasswordAuthentication no,并去除行前的注释符号 #
ClientAliveInterval 一行改为 ClientAliveInterval 60,并去除行前的注释符号 #
保存退出,执行 /etc/init.d/ssh restart 重启 ssh 服务即可。

创建普通用户并授予 sudo 权限

创建用户[2]

首先,使用 useradd 命令创建用户:

useradd -m -s /bin/bash <username>

# 添加用户名为 <username> 的用户
# -m, 自动创建用户家目录,并将 /etc/skel 中的文件复制到家目录中
# -s /bin/bash, 指定用户默认 shell 为 bash

然后为该用户设置密码:

passwd <username>
授予用户 sudo 权限[3]

一般来说,Ubuntu 系统已经自带了 sudo;如果没有,则先安装:

apt install sudo

由于直接修改 etc/sudoers 文件有一定的危险性,因此我们采用将配置文件追加到 /etc/sudoers.d/ 目录下的方式进行配置。

echo '<username> ALL=(ALL) ALL' >> /etc/sudoers.d/<username>

# 请自行将 <username> 改为你的用户名

<username> ALL=(ALL) ALL 表示普通用户 <username> 可以在任何主机上,通过 sudo 提权到任何用户/用户组,执行任何命令[4]

如果你想免密使用 sudo(不安全),那么上一步可以改为:

echo '<username> ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/<username>
解决部分机器 sudo 提示 unable to resolve host[5]

部分服务器在执行 sudo 时,会弹出以下错误:

$ sudo true

sudo: unable to resolve host <hostname>

尽管不影响使用,但看着总让人不爽。这个错误是因为服务商提供的默认系统镜像没有将主机名 (hostname) 添加到 /etc/hosts 文件中,导致系统无法解析 hostname。使用以下命令即可解决:

echo "127.0.0.1 $(hostname)" | sudo tee -a /etc/hosts
为用户启用 SSH[6]

首先切换到刚刚创建的用户:

su - <username>

随后,建立 ~/.ssh 目录,并将 root 用户下的 authorized_keys 复制过来:

mkdir -p ~/.ssh/

sudo cp /root/.ssh/authorized_keys ~/.ssh/

最后设置正确的访问权限:

sudo chown -R <username>:<username> ~/.ssh
sudo chmod 700 ~/.ssh
sudo chmod 600 ~/.ssh/authorized_keys

为了提高安全性,你还可以关闭 root 用户的 ssh 登录。编辑 ssh 配置文件:vim /etc/ssh/sshd_config

PermitRootLogin yes 改为 PermitRootLogin no,并去除行前的注释符号 #
保存退出,执行 /etc/init.d/ssh restart 重启 ssh 服务即可。

防火墙配置

安装 fail2ban

fail2ban 是 Linux 上一个著名的入侵保护的开源框架,它能通过 iptables 将尝试暴破 ssh 密码的 IP 封停,有效防止暴力破解攻击。

apt install fail2ban
安装并配置 UFW 防火墙[7]

ufw,即 uncomplicated firewall(简单防火墙),是 Ubuntu 系统默认的防火墙组件,它可以简化对 iptables 的配置。

# Ubuntu 系统已经自带了 ufw 防火墙

# 会提示你可能影响 ssh 连接,务必确认已将你的 ssh 端口加入到 ufw rules 中,然后按 y 确认开启

使用 NTP 同步系统时间

许多服务都要求服务器时间与标准时间误差不能过大,因此我们使用 NTP 进行时间同步。

apt install ntp

设置时区[8]

切换到与所在地相同的时区可以方便 crontab 等服务的设置。

查看可用时区列表:

timedatectl list-timezones

设定时区:

timedatectl set-timezone Asia/Hong_Kong  # 将时区设置为 Asia/Hong_Kong

查看当前时间设定:

timedatectl

配置无人值守更新(可选)[9]

一般来说,Ubuntu 系统已经自带了 unattended-upgrades 包;如果没有,则先安装:

apt install unattended-upgrades

随后,通过以下命令启用无人值守更新:

echo "unattended-upgrades unattended-upgrades/enable_auto_updates boolean true" | debconf-set-selections

DEBIAN_FRONTEND=noninteractive dpkg-reconfigure unattended-upgrades

通过编辑 /etc/apt/apt.conf.d/ 下的 20auto-upgrades50unattended-upgrades 两个配置文件,我们可以设定无人值守更新。

以下为一些关键的设置项:

Unattended-Upgrade::Allowed-Origins {
	... ...
//	"${distro_id}:${distro_codename}-updates";
// 自动更新(除安全更新外的)软件包,去除行前的注释符号 // 以开启
// 注意:开启此选项有风险
    ... ...
};

... ...

//Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
// 自动移除无用内核,去除行前的注释符号 // 以开启

... ...

//Unattended-Upgrade::Remove-Unused-Dependencies "false";
// 自动移除无用依赖,去除行前的注释符号 // 并将 false 改为 true 以开启
// 等同于自动 apt-get autoremove

本配置文件还提供了许多其他的设置项,并且有详细的注释,可自行查看编辑。

启用 Canonical Livepatch 服务(可选)

在 Ubuntu 16.04 及以上的系统中,可以使用 Canonical Livepatch 服务实现无需重启系统就能安装内核安全补丁。同时,Canonical Livepatch 服务对于不超过 3 台机器的个人用户是免费的。

1. 在 Canonical Livepatch Service 官网 上选择 Ubuntu user,点击 Get your Livepatch token

2. 按要求注册(或登录)Ubuntu One 账号,获取 token

3. ssh 到服务器上,随后安装 Snapd 守护程序:

apt install snapd

4. 安装 Canonical Livepatch 守护程序:

snap install canonical-livepatch

5. 启用 Canonical Livepatch:

canonical-livepatch enable <token>

# 请自行将 <token> 换成第二步中获取的 token

6. 查看 Canonical Livepatch 状态:

canonical-livepatch status

7. 手动更新补丁:

canonical-livepatch refresh

8. 停用 Canonical Livepatch:

snap run canonical-livepatch disable

注意:安装内核补丁与安装新内核不同,如果安装了新内核,则必须重新启动系统以应用新内核。

优化

提高文件并发数

echo '* soft nofile 51200
* hard nofile 51200' >> /etc/security/limits.conf

然后,执行:

ulimit -n 51200

设置虚拟内存

某些 VPS 服务商会预先设置好虚拟内存,所以我们先查看系统当前可用内存:

free -m

输出样例:

$ free -m

              total        used        free      shared  buff/cache   available

Mem:            982         277         112           1         592         533

Swap:          1023           2        1021

如果输出中没有上面的 Swap 一行或 Swap 后为 0,则表示系统没有设置虚拟内存,可以参考下面的方法设置:

1. 创建 swapfile

dd if=/dev/zero of=/root/swapfile bs=1M count=1024
# if 表示 input file (输入文件),of 表示 output file (输出文件),bs 表示 block size (块大小),count 表示计数

示例中采用的数据块大小为 1M,数据块计数为 1024,所以分配的空间就是 1G 大小。

2. 设置正确的 swapfile 权限

chmod 600 /root/swapfile

3. 格式化 swapfile

mkswap /root/swapfile

4. 启用 swapfile

swapon /root/swapfile

5. 设置开机自动加载虚拟内存

echo "/root/swapfile swap swap defaults 0 0" >> /etc/fstab

此时再执行 free -m 即可看到虚拟内存已经启用了。

开启 BBR 拥塞控制算法

BBR 是 Google 出品的 TCP 拥塞控制算法,其目的是尽量跑满带宽,可以起到单边加速 TCP 连接的效果。BBR 是内嵌在 Linux 内核中的,而 Linux kernel 4.9 已加入了该算法。如果你的系统内核版本低于 4.9,则需要先升级 Linux 内核。
# Ubuntu 18.04 及以上系统内核版本已高于 4.9,可直接开启 BBR

1. 开启 BBR

echo "net.core.default_qdisc=fq" >> /etc/sysctl.conf

echo "net.ipv4.tcp_congestion_control=bbr" >> /etc/sysctl.conf

然后,执行 sysctl -p 保存生效。

2. 查看 BBR 是否成功开启

执行 sysctl net.ipv4.tcp_available_congestion_control, 结果中应有 bbr
执行 lsmod | grep bbr,结果中应有 tcp_bbr

输出样例:

自动升级内核

默认情况下,Ubuntu LTS 将保持在最初发布的 Linux 内核上。而 Ubuntu 硬件支持栈(Hardware Enablement Stacks, HWE)可以为现有的 Ubuntu LTS 提供更新的内核支持,实现内核自动升级,方便我们体验较新内核的特性[10]

1. 查看当前系统内核版本

uname -r

2. 安装 HWE

使用包管理器即可安装 HWE:

apt install --install-recommends linux-generic-hwe-20.04  # For Ubuntu 20.04 LTS

apt install --install-recommends linux-generic-hwe-18.04  # For Ubuntu 18.04 LTS

3. 重启系统以应用新内核

reboot

4. 删除旧内核(可选)[11]

使用包管理器自动卸载即可:

apt autoremove --purge

如果只想保留当前最新的内核,可以使用以下命令(有风险,慎用)

apt purge $(dpkg -l | grep -E "linux-(image|headers|modules)" | grep -v hwe | grep -v $(uname -r | cut -d '-' -f1,2) | awk '{print $2}')

或者也可以手动卸载:

卸载旧版 linux-image:

获取 linux-image 列表:

dpkg -l | grep linux-image

卸载旧版 linux-image:

apt purge <旧linux-image名>

卸载旧版 linux-headers:

获取 linux-headers 列表:

dpkg -l | grep linux-headers

卸载旧版 linux-headers:

apt purge <旧linux-headers名>

卸载旧版 linux-modules:

获取 linux-modules 列表:

dpkg -l | grep linux-modules

卸载旧版 linux-modules:

apt purge <旧linux-modules名>

其他

安装 Docker-CE

Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。iOS 上的 HyperApp 也正是借助 Docker 实现了自动部署应用。

安装 Docker 最方便的方式是使用官方脚本[12]

wget -qO- get.docker.com | bash

对于国内 VPS,还可以指定使用阿里云镜像进行安装:

wget -qO- get.docker.com | bash -s docker --mirror Aliyun
以普通用户管理 Docker[13]

由于 Docker 守护进程绑定在 Unix socket 上,而 Unix socket 默认仅属于 root 用户,因此在通过普通用户管理 Docker 时,我们需要在命令前加上 sudo。为了方便使用,我们可以将普通用户添加到 docker 用户组中,这样就能直接使用 docker 命令而不需要 sudo

1. 创建 docker 用户组

sudo groupadd docker

# 较新的 Docker 安装中可能已经添加了 docker 用户组

2. 将当前用户加入 docker

sudo gpasswd -a $USER docker

3. 登出并重新登入当前用户

或者,你也可以使用 newgrp 命令以 docker 组重新登入系统:

newgrp docker

此时就可以直接使用 docker 命令管理容器了。

限制 Docker 容器日志大小

Docker 容器运行过程中会产生大量日志文件,如果不加限制可能会导致 VPS 磁盘空间不足。通过编辑 Docker 配置文件,我们可以限制容器日志大小。

编辑 /etc/docker/daemon.json 文件:vim /etc/docker/daemon.json

添加 log-dirverlog-opts 参数:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" }
}

# 样例中一个容器的日志大小上限是 50 M,最多创建 3 个日志文件

对于国内 VPS,还可以在配置文件中配置国内镜像源:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "50m", "max-file": "3" },
  "registry-mirrors": ["https://<阿里云ID>.mirror.aliyuncs.com","https://registry.docker-cn.com"]
}

# 阿里云源地址可以在 容器镜像服务 > 镜像加速器 下查看

保存退出,随后加载配置文件并重启 Docker:

systemctl daemon-reload

systemctl restart docker

# 设置的日志大小,只对新建的容器有效

手动清理 Docker 容器日志
docker ps | awk '{if (NR>1){print $1}}' | xargs docker inspect --format='{{.LogPath}}' | xargs truncate -s 0

解决 UFW 对 Docker 容器无效的问题[14]

在使用 Docker 暴露容器端口时,Docker 会直接操作 iptables NAT 规则,导致 ufw 无法管理 Docker 所发布的端口,这是一个严重的安全问题。好在通过编辑 ufw 的 after.rules,我们能够解决这一问题。

编辑 ufw 的 after.rulesvim /etc/ufw/after.rules

在末尾写入如下内容:

# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16

-A DOCKER-USER -j ufw-user-forward

-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8
-A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12

-A DOCKER-USER -j RETURN
COMMIT
# END UFW AND DOCKER

保存退出,随后重启 ufw:

systemctl restart ufw

# 如果重启 ufw 之后规则没有生效,则需要重启服务器。

如果你要允许外部网络访问所有 Docker 发布的、内部端口为 80 的服务,则执行:

ufw route allow from any to any port 80

ufw route allow proto tcp from any to any port 80  # 仅允许 TCP 协议

# 此处的 80 是容器内部端口,而非使用 -p 8080:80 选项发布在服务器上的 8080 端口。


↩︎ 注

  1. 参考 GitHub Help - Generating a new SSH key and adding it to the ssh-agent 

  2. 参考 Linux 使用 adduser 与 useradd 添加普通用户的正确姿势

  3. 参考 Linux 中授予普通用户 sudo 权限的正确方法

  4. 参考 sudoers 的深入剖析与用户权限控制

  5. 参考 askubuntu - Error message “sudo: unable to resolve host (none)”

  6. 参考 askubuntu - Create a new SSH user on Ubuntu Server

  7. 参考 Ubuntu Community Help Wiki - UFW

  8. 参考 How To Set or Change Timezone on Ubuntu 18.04

  9. 参考 Ubuntu Community Help Wiki - AutomaticSecurityUpdates

  10. 参考 Ubuntu Community Help Wiki - LTSEnablementStack

  11. 参考 Ubuntu Community Help Wiki - RemoveOldKernels

  12. 参考 Docker Documentation - Install Docker Engine on Ubuntu

  13. 参考 Docker Documentation - Post-installation steps for Linux

  14. 参考 不用禁用 iptables 来解决 UFW 和 Docker 的安全问题