This article is a record of my own installation steps of Arch Linux on a KVM-based VPS server via Live CD.
The VPS was finally configured to a web server + proxy server + testing machine for development.
Presumptions:
- x86_64, KVM-based VPS
- VPS provider allows customed ISO
- a domain name
- public IP address (not NAT)
Many of the steps were learned from the Arch Linux wiki.
# make a single partition here (mbr table), instructions:
# `o` create dos mbr table,
# `n` create new partition,
# `w` write mbr,
# `q` quit,
[root@arch /]# fdisk /dev/sda
[root@arch /]# mkfs.ext4 /dev/sda1
[root@arch /]# mount /dev/sda1 /mnt
Select fastest mirrors.
[root@arch /]# cp /etc/pacman.d/mirrorlist{,.old}
[root@arch /]# rankmirrors -n 3 /etc/pacman.d/mirrorlist.old > /etc/pacman.d/mirrorlist
Install all the packages (core packages, basic tools, ergonomic tools, administrator utils, http server programs, proxy softwares, and development tools).
[root@arch /]# pacstrap /mnt \
base linux linux-firmware grub sudo \
dhcpcd dhclient systemd-resolvconf curl wget aria2 \
nano vim bash-completion fish man-db pacman-contrib pkgfile \
openssh mosh tmux ntp net-tools bind-tools gnu-netcat iftop \
nginx nginx-mod-brotli nginx-mod-naxsi fail2ban acme.sh \
v2ray trojan wireguard-arch wireguard-tools \
base-devel cmake git docker go rust npm
If you need a non-volatile kernel, try linux-lts
instead of linux
(also replace wireguard-arch
with wireguard-lts
).
[root@arch /]# genfstab -U /mnt >> /mnt/etc/fstab
[root@arch /]# arch-chroot /mnt
[root@arch /]# echo 'LANG=en_US.UTF-8' > /etc/locale.conf
[root@arch /]# nano /etc/locale.gen #toggle: en_US.UTF-8 UTF-8
[root@arch /]# locale-gen
[root@arch /]# ln -sf /usr/share/zoneinfo/REGION/CITY /etc/localtime
[root@arch /]# hwclock --systohc
[root@arch /]# echo 'HOST' > /etc/hostname
[root@arch /]# cat >> /etc/hosts <<'EOF'
127.0.0.1 localhost
::1 localhost
127.0.0.1 DOMAIN.com HOST
EOF
[root@arch /]# passwd
[root@arch /]# useradd USER
[root@arch /]# passwd USER
[root@arch /]# mkdir -p ~USER
[root@arch /]# chown USER:USER ~USER
[root@arch /]# chmod 0755 ~USER
[root@arch /]# nano /etc/sudoers #toggle: %wheel ALL=(ALL) ALL
[root@arch /]# gpasswd -a USER wheel
[root@arch /]# grub-install --target=i386-pc /dev/sda
[root@arch /]# grub-mkconfig -o /boot/grub/grub.cfg
[root@arch /]# reboot
Reboot to the new system (eject Live CD if necessary) and login as root. Then configure Arch Linux server.
Assuming that your VPS provider uses DHCP.
To configure DHCP for all the existing ethernet interfaces, edit /etc/systemd/network/20-wired.network
.
[Match]
Name = en*
[Network]
DHCP = yes
Make systemd-networkd
works.
[root@HOST ~]# systemctl enable systemd-networkd.service
[root@HOST ~]# systemctl start systemd-networkd.service
The preinstalled systemd-resolved provides resolver services for DNS
, DNSSEC
, DNS over TLS
, mDNS
and LLMNR
.
A simplest /etc/systemd/resolved.conf
.
[Resolve]
DNS = 8.8.8.8
FallbackDNS = 8.8.4.4
Make systemd-resolved
works in DNS stub file mode
.
[root@HOST ~]# ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
[root@HOST ~]# systemctl enable systemd-resolved.service
[root@HOST ~]# systemctl start systemd-resolved.service
Google's BBR TCP congestion control algorithm could achieve higher bandwidth and lower latency. Since 4.9, BBRv1 is available in kernel.
Create a sysctl
configuration for BBR in /etc/sysctl.d/90-bbr.conf
.
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
Create another configuration to enable IPv4/IPv6 forwarding in /etc/sysctl.d/95-forward.conf
. It is required by docker and NAT.
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
Reload sysctl
.
[root@HOST ~]# sysctl --system
[root@HOST ~]# systemctl enable ntp.service
[root@HOST ~]# systemctl start ntp.service
Push your public keys.
[root@HOST ~]# sudo -u USER bash
[USER@HOST root]$ cd ~
[USER@HOST ~]$ mkdir .ssh
[USER@HOST ~]$ nano .ssh/authorized_keys #insert your public keys
[USER@HOST ~]$ chmod 0700 .ssh
[USER@HOST ~]$ chmod 0600 .ssh/authorized_keys
[USER@HOST ~]$ chown -R USER:USER .ssh
[USER@HOST ~]$ exit
Modify the following lines in /etc/ssh/sshd_config
, replace SSHD_PORT
with a number in 10000-65535
.
Port SSHD_PORT
PasswordAuthentication no
ClientAliveInterval 30
ClientAliveCountMax 60
LogLevel VERBOSE
Enable and start sshd.
[root@HOST ~]# systemctl enable sshd.service
[root@HOST ~]# systemctl start sshd.service
Simply banned TCP/UDP port 1-9999
except 80
and 443
(remember to set a sshd port > 9999
later). This can block various kinds of annoying bots worldwide.
Add rules into /etc/iptables/iptables.rules
and /etc/iptables/ip6tables.rules
.
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 1:9999 -j DROP
COMMIT
Make iptables
/ip6tables
works.
[root@HOST ~]# systemctl enable iptables.service
[root@HOST ~]# systemctl start iptables.service
[root@HOST ~]# systemctl enable ip6tables.service
[root@HOST ~]# systemctl start ip6tables.service
fail2ban is a fully functional python script which analyses logs (usually from journald
in Arch) and manages firewall rules to block suspicious IP addresses.
- Basic settings.
Edit /etc/fail2ban/jail.local
.
[DEFAULT]
banaction = iptables
ignoreip = 127.0.0.1/8 ::1
- Settings for sshd.
Create /etc/fail2ban/jail.d/sshd.local
, replace with your SSHD_PORT
.
[sshd]
enabled = true
backend = systemd
filter = sshd
port = SSHD_PORT
maxretry = 5
findtime = 1d
bantime = 2w
bantime.rndtime = 12h
bantime.maxtime = 6mo
Toggle LogLevel VERBOSE
in /etc/ssh/sshd_config
, then reload sshd
if needed.
- Enable and start fail2ban.
[root@HOST ~]# systemctl enable fail2ban.service
[root@HOST ~]# systemctl start fail2ban.service
Now you can login as non-privileged user via SSH.
[USER@HOST ~]$ chsh -s /usr/bin/fish
# relogin to see that shell has been changed to fish
Note that though fish is an convenient interactive shell, but it is incompatible with the bash grammar (e.g. pacman -Qo $(which nc)
whould be write as pacman -Qo (which nc)
in fish).
[USER@HOST ~]$ sudo pkgfile -u
It hints which package to install when command not found.
Clean up pacman caches and remove unnecessary packages.
[USER@HOST ~]$ yes | sudo pacman -Scc
[USER@HOST ~]$ pacman -Qtdq | sudo pacman -Rsd -
Upgrade system if necessary.
[USER@HOST ~]$ sudo pacman -Syu
[USER@HOST ~]$ git clone https://aur.archlinux.org/yay.git
[USER@HOST ~]$ cd yay
[USER@HOST ~]$ makepkg -si
[USER@HOST ~]$ cd ..
[USER@HOST ~]$ rm -rf yay
- Setup Nginx HTTP server for ACME challenge.
Make a simple configuration that serves ACME chanllenges in /PATH/TO/ACME/CHALLENGE/ROOT
and redirect other requests to the https server (not configured yet).
Edit /etc/nginx/modules.conf
and /etc/nginx/nginx.conf
.
load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so;
load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;
include modules.conf;
error_log /var/log/nginx/error.log;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
charset utf-8;
gzip on;
brotli on;
sendfile on;
keepalive_timeout 65;
server {
server_name DOMAIN.com www.DOMAIN.com;
listen 80;
location ^~ /.well-known/acme-challenge/ {
root /PATH/TO/ACME/CHALLENGE/ROOT;
}
location / {
return 301 https://DOMAIN.com$request_uri;
}
}
}
Note: The super easy https/http2 server Caddy written in Go might also be a good alternative to Nginx for non-complex situations.
- Issue a Let's Encrypt certificate.
Start Nginx service, then issue a cert using acme.sh, a simpler alternative to certbot.
The web root mode of acme.sh seems do not support wildcard in domain name. Try DNS API/manual mode instead if you really need that.
[USER@HOST ~]$ sudo systemctl start nginx.service
[USER@HOST ~]$ acme.sh --issue -w /PATH/TO/ACME/CHALLENGE/ROOT -d DOMAIN.com -d www.DOMAIN.com
Make sure that /PATH/TO/ACME/CHALLENGE/ROOT
is absolute path and the current user has write permission to it.
If successed, acme.sh would save certificates and configurations in $HOME/.acme.sh/DOMAIN.com
.
- Install certificate.
Firstly, add a server into the http
block of /etc/nginx/nginx.conf
that serves /PATH/TO/WEBSITE
.
server {
server_name DOMAIN.com www.DOMAIN.com;
listen 443 ssl;
ssl_certificate /PATH/TO/fullchain.pem;
ssl_certificate_key /PATH/TO/privkey.pem;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4:!DH:!DHE;
#ssl_prefer_server_ciphers off;
access_log /var/log/nginx/https_access.log;
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location / {
root /PATH/TO/WEBSITE;
index index.html;
}
}
Then install certificates and restart nginx
. Run acme.sh
as root to install certificates into destination (e.g. /etc/ssl
). Since running as root might result in wrong ownership of files under $HOME/.acme.sh
. Work around it using chown
.
[USER@HOST ~]$ sudo acme.sh --force --home HOME_DIR/.acme.sh \
--install-cert -d DOMAIN.com \
--key-file /PATH/TO/privkey.pem \
--fullchain-file /PATH/TO/fullchain.pem \
--reloadcmd 'systemctl restart nginx.service'
[USER@HOST ~]$ sudo chown -R USER:USER HOME_DIR/.acme.sh
All the installation and issuing parameters you passed to acme.sh
would finally be recorded under $HOME/.acme.sh
. The automatical renew process acme.sh --cron
would reuse these parameters implicitly.
- Setup certificates renewal.
By default, Arch Linux use systemd timers instead of cron. Therefore, we need to setup automatical renewal by ourselves.
Firstly, create a systemd service in /etc/systemd/system/acme_renew.service
for certificates renewal. Change USER
and HOME_DIR
to your own.
[Unit]
Description = Renew Lets Encrypt certificates using acme.sh
After = network-online.target
Require = nginx.service
[Service]
Type = oneshot
ExecStart = /usr/bin/acme.sh --log --home HOME_DIR/.acme.sh --cron
ExecStartPost = chown -R USER:USER HOME_DIR/.acme.sh
[Install]
WantedBy = multi-user.target
Then create a timer for that service, in /etc/systemd/system/acme_renew.timer
. Also, change HOME_DIR
to your $HOME
.
[Unit]
Description = Daily renewal of Lets Encrypt certificates using acme.sh
[Timer]
OnCalendar = daily
Persistent = true
[Install]
WantedBy = timers.target
Finally, enable and start certificate renewal service along with timer.
[USER@HOST ~]$ sudo systemctl daemon-reload
[USER@HOST ~]$ sudo systemctl enable acme_renew.service
[USER@HOST ~]$ sudo systemctl start acme_renew.service
[USER@HOST ~]$ sudo systemctl enable acme_renew.timer
[USER@HOST ~]$ sudo systemctl start acme_renew.timer
The auto renew script acme.sh --cron
would update and reinstall certificates every 00:00 AM then restart Nginx if the certificates need update.
Create a simple robots.txt
, in /PATH/TO/WEBSITE/robots.txt
.
User-agent: *
Allow: /
Install htpasswd
and nginx-mod-dav-ext
from AUR.
[USER@HOST ~]$ yay -S nginx-mod-dav-ext apache-tools
Generate /etc/nginx/htpasswd.conf
.
[USER@HOST ~]$ sudo htpasswd -c /etc/nginx/htpasswd.conf USER
# htpasswd will prompt you for password
Insert WebDAV settings into the location /
block in /etc/nginx/nginx.conf
.
location ^~ /WEBDAV/LOCATION/ {
alias /PATH/TO/WEBDAV/DIRECTORY/;
dav_methods PUT DELETE MKCOL COPY MOVE;
dav_ext_methods PROPFIND OPTIONS;
auth_basic "Restricted access";
auth_basic_user_file htpasswd.conf;
dav_access user:rw group:rw all:r;
autoindex on;
create_full_put_path on;
client_max_body_size 0;
client_body_temp_path /srv/client-temp;
}
Where dav_access user:rw group:rw all:r;
indicates Nginx use 0664
permissions to create new files.
Change permissions of /PATH/TO/WEBDAV/DIRECTORY/
and reload Nginx.
[USER@HOST ~]$ sudo gpasswd -a USER http
[USER@HOST ~]$ sudo chown -R http:http /PATH/TO/WEBDAV/DIRECTORY/
[USER@HOST ~]$ find /PATH/TO/WEBDAV/DIRECTORY/ -type d | xargs sudo chmod -R 0775
[USER@HOST ~]$ find /PATH/TO/WEBDAV/DIRECTORY/ -type f | xargs sudo chmod -R 0664
[USER@HOST ~]$ sudo systemctl restart nginx.service
Add the NAXSI module to /etc/nginx/modules.conf
.
load_module /usr/lib/nginx/modules/ngx_http_naxsi_module.so;
Insert NAXSI core rules into the http
block, edit /etc/nginx/nginx.conf
.
include naxsi_core.rules;
- Add/modify fail2ban filters for Nginx.
[USER@HOST ~]$ sudo cp /etc/fail2ban/filter.d/{apache,nginx}-badbots.conf
[USER@HOST ~]$ # insert additional rule for blank user/password
[USER@HOST ~]$ echo ' ^ \[error\] \d+#\d+: \*\d+ no user/password was provided for basic authentication, client: <HOST>, server: \S+, request: "\S+ \S+ HTTP/\d+\.\d+", host: "\S+"\s*$' | sudo sed -ie '/^failregex/r /dev/stdin' /etc/fail2ban/filter.d/nginx-http-auth.conf
- Configure Nginx request limitation.
Add global request limitation of each IP into http
block in /etc/nginx/nginx.conf
.
limit_req_zone $binary_remote_addr zone=each_ip:10m rate=10r/s;
Add limit_req
to desired server
block or location
block in /etc/nginx/nginx.conf
.
limit_req zone=each_ip burst=5;
Reload Nginx.
[USER@HOST ~]$ sudo systemctl restart nginx.service
- Configure fail2ban jails for Nginx.
Create /etc/fail2ban/jail.d/nginx.local
.
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
backend = polling
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 6
findtime = 1h
bantime = 5d
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
backend = polling
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 600
bantime = 7200
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
backend = polling
port = http,https
logpath = /var/log/nginx/https_access.log
maxretry = 10
findtime = 1h
bantime = 3d
[nginx-badbots]
enabled = true
filter = nginx-badbots
backend = polling
port = http,https
logpath = /var/log/nginx/https_access.log
maxretry = 1
findtime = 1h
bantime = 3d
- Reload fail2ban.
[USER@HOST ~]$ sudo systemctl restart fail2ban.service
V2Ray is a highly configurable proxy that supports various protocols and transport methods. The following steps are to setup a V2Ray server of shadowsocks
protocol over WebSocket
+ TLS
using Nginx
(ss+wss://
).
Edit /etc/v2ray/config.json
firstly.
{
"inbounds": [
{
"port": INTERNAL_PORT,
"listen":"127.0.0.1",
"protocol": "shadowsocks",
"settings": {
"ota": false,
"method": "chacha20-poly1305",
"password": "PASSWORD"
},
"streamSettings": {
"network": "ws",
"wsSettings": { "path": "/LOCATION/TO/WS" }
}
}
],
"outbounds": [ { "protocol": "freedom" } ]
}
Insert following lines into the server
block of https in /etc/nginx/nginx.conf
. Make sure that there is no limit_req
in this location
block or in the upper server
/location
block.
location = /LOCATION/TO/WS {
proxy_redirect off;
proxy_pass http://127.0.0.1:INTERNAL_PORT;
proxy_http_version 1.1;
proxy_read_timeout 300s;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Accept-Encoding "";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Enable and start proxy server.
[USER@HOST ~]$ sudo systemctl enable v2ray.service
[USER@HOST ~]$ sudo systemctl start v2ray.service
[USER@HOST ~]$ sudo systemctl restart nginx.service
The client side configuration of V2Ray should be something like:
{
"inbounds": [
{
"port": LOCAL_SOCKS5_PORT,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": true
}
}
],
"outbounds": [
{
"protocol": "shadowsocks",
"settings": {
"servers": [
{
"address": "DOMAIN.com",
"port": 443,
"ota": false,
"method": "chacha20-poly1305",
"password": "PASSWORD"
}
]
},
"streamSettings": {
"network": "ws",
"security": "tls",
"wsSettings": { "path":"/LOCATION/TO/WS" },
"tlsSettings": { "serverName": "DOMAIN.com" }
}
}
]
}
Trojan is a simplified standalone socks5-like proxy protocol over TLS, similar to the above V2Ray ss+wss://
way.
If there is no certificate, generate a self-signed SSL certificate (and disable certificate verify in client side).
[USER@HOST ~]$ openssl req -newkey rsa:2048 -nodes -keyout /PATH/TO/key.pem \
-x509 -days 3650 -out /PATH/TO/cert.pem
Edit /etc/trojan/config.json
.
{
"run_type": "server",
"local_addr": "0.0.0.0",
"local_port": TROJAN_PORT,
"password": [ "PASSWORD" ],
"log_level": 1,
"ssl": {
"cert": "/PATH/TO/cert.pem",
"key": "/PATH/TO/key.pem",
"cipher": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384",
"cipher_tls13": "TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384",
"prefer_server_cipher": true,
"alpn": [ "h2", "http/1.1" ],
"reuse_session": true,
"session_ticket": false,
"session_timeout": 300
},
"tcp": {
"prefer_ipv4": true,
"no_delay": true,
"keep_alive": true,
"reuse_port": false,
"fast_open": false,
"fast_open_qlen": 20
},
"mysql": { "enabled": false }
}
Enable and start the Trojan server.
[USER@HOST ~]$ sudo systemctl enable trojan.service
[USER@HOST ~]$ sudo systemctl start trojan.service
Mobile clients of Trojan was under development now. They were currently (2020.03) not that stable and convenient.
WirdGuard is a fast, cross-platform and easy-to-use VPN protocol that could work in Linux kernel to encapsulate IP packets over UDP.
It sends traffic via UDP as many other VPN softwares will do, which might be QoS-ed by some ISPs. Therefore, the network quality and speed might be not as good as ordinary proxies/tunnels like ShadowSocks, V2Ray, Trojan and Gost.
The following steps demostrate how to manually start a wireguard VPN server and how to manually stop it.
- Generate server side private key and public key pair.
[USER@HOST ~]$ wg genkey | tee SERVER_PRIVATE_KEY | wg pubkey > SERVER_PUBLIC_KEY
- Start wireguard server.
[USER@HOST ~]$ sudo ip link add dev wg0 type wireguard
[USER@HOST ~]$ sudo ip addr add 10.0.0.1/24 dev wg0
[USER@HOST ~]$ sudo wg set wg0 listen-port SERVER_PORT private-key SERVER_PRIVATE_KEY
[USER@HOST ~]$ sudo wg set wg0 peer CLIENT_PUBLIC_KEY persistent-keepalive 180 allowed-ips 0.0.0.0/0,::/0
[USER@HOST ~]$ sudo ip link set wg0 up
The allowed-ips
field valued 0.0.0.0/0,::/0
means allow any client IPs. Please change this value to your client IP 10.0.0.xxx
.
- Turn on NAT (
wg0
->ens1
).
[USER@HOST ~]$ sudo iptables -A FORWARD -i wg0 -j ACCEPT
[USER@HOST ~]$ sudo iptables -t nat -A POSTROUTING -o ens1 -j MASQUERADE
- Turn off NAT.
[USER@HOST ~]$ sudo iptables -D FORWARD -i wg0 -j ACCEPT
[USER@HOST ~]$ sudo iptables -t nat -D POSTROUTING -o ens1 -j MASQUERADE
- Stop wireguard server.
[USER@HOST ~]$ sudo ip link set wg0 down
[USER@HOST ~]$ sudo ip link del dev wg0
The wg-quick
script included in wireguard-tools
package seems to not work properly,
maybe because it automatically setup additional rules/routes that would block SSH connection.
Fortunately, systemd-networkd
works well with wireguard (without NAT). Following is the configuration.
- Configure the WireGuard NIC.
Edit /etc/systemd/network/90-wireguard.netdev
.
[NetDev]
Name = wg0
Kind = wireguard
Description = WireGuard VPN Server (without NAT)
[WireGuard]
ListenPort = SERVER_PORT
PrivateKey = SERVER_PRIVATE_KEY
[WireGuardPeer]
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 180
- Configure the WireGuard network.
Edit /etc/systemd/network/90-wireguard.network
.
[Match]
Name = wg0
[Network]
Address = 10.0.0.1/32
[Route]
Gateway = 10.0.0.1
Destination = 10.0.0.0/24
- Turn on the WireGuard network.
[USER@HOST ~]$ sudo networkctl reload
- Manually turn on/off NAT then.
Same as above.