跳到主要内容

阿里云 Ubuntu 配置

修改 SSH 端口

注意要 daemon reload 才能生效。

sudo sed -i 's/^#\?Port.*/Port 23333/' /etc/ssh/sshd_config
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket

内存优化

配置选的 2C2G, 开机一看

# free
total used free shared buff/cache available
Mem: 1651696 393420 418248 2652 1012996 1258276
Swap: 0 0 0

内存才 1.6 GiB!搜索了一下看到 V2EX 有人说是 crashkernel 占用的. 修改 /etc/default/grub 删除相关内核选项后,

grub-mkconfig -o /boot/grub/grub.cfg

然后重启。现在再来看内存:

# free
total used free shared buff/cache available
Mem: 1913840 404420 1308076 2672 353696 1509420
Swap: 0 0 0

约为 1.825 GiB, 使用 dmesg | grep -i "Memory:" 查看实际物理内存:

# dmesg | grep -i "Memory:"
[ 0.045721] Memory: 1830108K/1995668K available (22528K kernel code, 4436K rwdata, 14408K rodata, 4920K init, 4792K bss, 165300K reserved, 0K cma-reserved)
[ 0.151181] Freeing SMP alternatives memory: 48K
[ 0.597818] Freeing initrd memory: 54708K
[ 0.854832] Freeing unused decrypted memory: 2028K
[ 0.860629] Freeing unused kernel image (initmem) memory: 4920K
[ 0.867992] Freeing unused kernel image (rodata/data gap) memory: 1976K
# free
total used free shared buff/cache available
Mem: 1913840 361872 1305672 2664 399288 1551968
Swap: 0 0 0

1995668 KiB 和 2048 MiB 相比还是有 96.7 MiB 左右的差值,可能是 BIOS 等占的内存。最终可用内存为 1.825 GiB.

另外可以关掉 aliyun-service

systemctl disable --now aliyun.service

日志大小限制

修改 /etc/systemd/journald.conf

[Journal]
SystemMaxUse=500M
RuntimeMaxUse=32M

每月流量限制

安装 vnstat

apt install -y vnstat iproute2
systemctl enable --now vnstat

查看网卡名:

ip route get 1.1.1.1

比如显示 dev eth0,那网卡就是 eth0

先初始化统计:

vnstat -i eth0
systemctl restart vnstat

写脚本

cat <<EOF > /usr/local/sbin/monthly-egress-limit.sh
#!/bin/bash
set -e

IFACE="eth0"
LIMIT_GB=17
RATE="500kbit"
STATE_FILE="/run/egress-limited"

# vnstat --oneline b:
TX_BYTES=$(vnstat -i "$IFACE" --oneline b 2>/dev/null | awk -F';' '{print $10}')

[ -z "$TX_BYTES" ] && exit 0

LIMIT_BYTES=$((LIMIT_GB * 1000 * 1000 * 1000))

if [ "$TX_BYTES" -ge "$LIMIT_BYTES" ]; then
if [ ! -f "$STATE_FILE" ]; then
tc qdisc replace dev "$IFACE" root tbf rate "$RATE" burst 32kbit latency 400ms
touch "$STATE_FILE"
fi
else
if [ -f "$STATE_FILE" ]; then
tc qdisc del dev "$IFACE" root 2>/dev/null || true
rm -f "$STATE_FILE"
fi
fi
EOF
chmod +x /usr/local/sbin/monthly-egress-limit.sh

用 systemd timer 每 5 分钟检查一次:

cat > /etc/systemd/system/monthly-egress-limit.service <<'EOF'
[Unit]
Description=Monthly egress traffic limiter

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/monthly-egress-limit.sh
EOF

cat > /etc/systemd/system/monthly-egress-limit.timer <<'EOF'
[Unit]
Description=Run monthly egress traffic limiter periodically

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Persistent=true

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now monthly-egress-limit.timer

每月流量超限关机保险

为了最大程度确保流量不超限,设置一个最简单的定时检测脚本,不成功即关机。参考 为云主机实现网络达量停机

cat <<EOF > /usr/local/sbin/traffic.sh
#!/usr/bin/env bash

LIMIT="${1:-50000000000}"

result=$(
vnstat -i eth0 -m --json \
| jq -r ".interfaces[0].traffic.month[0] | if .tx > $LIMIT then \"err\" else \"ok\" end"
)

if [ "$result" != "ok" ]; then
systemctl poweroff
fi
EOF
chmod +x /usr/local/sbin/traffic.sh

然后配置 crontab

*/15 * * * * /usr/local/sbin/traffic.sh 30000000000

Docker/Podman 相关

打开 IP 转发。

echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
sysctl -p

设置 Podman 默认 registries.

cat <<EOF > /etc/containers/registries.conf
unqualified-search-registries = ["docker.io", "quay.io"]
EOF

Vaultwarden

创建数据目录和环境变量文件

mkdir -p /opt/vaultwarden/data
install -o0 -g0 -m600 /dev/null /etc/vaultwarden.env
cat <<EOF > /etc/vaultwarden.env
ROCKET_PORT=8080
ROCKET_ADDRESS=127.0.0.1
DOMAIN=https://vw.example.com
SIGNUPS_ALLOWED=false
WEBSOCKET_ENABLED=true
EOF

设置 container

mkdir -p /etc/containers/systemd
cat <<EOF > /etc/containers/systemd/vaultwarden.container
[Unit]
Description=Vaultwarden container
After=network-online.target

[Container]
ContainerName=vaultwarden
Image=ghcr.io/dani-garcia/vaultwarden:latest
AutoUpdate=registry
EnvironmentFile=/etc/vaultwarden.env
Volume=/opt/vaultwarden/data:/data
Network=host
Exec=/start.sh

[Service]
Restart=always

[Install]
WantedBy=multi-user.target
EOF

根据 podman-systemd.unit 文档,由于 transient 的 unit 不支持 systemd enable,为了能实现开机自启的效果 systemd generator 在看到 WantedBy=multi-user.target 这一行之后就会自动将其添加到开机启动项中。

手动启动:

systemctl start vaultwarden.service

Caddy 配置:

example.com {
reverse_proxy 127.0.0.1:8080
}

OpenGist

创建数据目录

sudo mkdir -p /opt/opengist
sudo vim /etc/containers/systemd/opengist.container

写入:

[Unit]
Description=Opengist container
After=network-online.target
Wants=network-online.target

[Container]
ContainerName=opengist
Image=ghcr.io/thomiceli/opengist:1
AutoUpdate=registry
PublishPort=127.0.0.1:6157:6157
PublishPort=2222:2222
Volume=/opt/opengist:/opengist

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

SSH 端口 2222 直接暴露,因为 Git SSH clone/push 需要外部访问。官方说明如果不用 SSH,可以删掉 2222:2222 这个端口。

Caddy 反代

gist.example.com {
reverse_proxy 127.0.0.1:6157
}

备份:

sudo tar czf opengist-backup-$(date +%F).tar.gz /opt/opengist

Coturn

1. 安装 coturn

sudo apt update
sudo apt install -y coturn openssl

启用 coturn:

sudo sed -i 's/^#\?TURNSERVER_ENABLED=.*/TURNSERVER_ENABLED=1/' /etc/default/coturn

2. 先让 Caddy 给域名签证书

如果 turn.example.com 现在没有被 Caddy 使用,可以在 /etc/caddy/Caddyfile 里放一个占位站点:

turn.example.com {
respond "coturn cert ok"
}

然后:

sudo systemctl reload caddy

确认 Caddy 已经拿到证书:

sudo find /var/lib/caddy/.local/share/caddy/certificates \
-type f \( -name 'turn.example.com.crt' -o -name 'turn.example.com.key' \) -print

3. 把 Caddy 证书复制给 coturn 用

不建议让 turnserver 直接读 Caddy 的私钥目录,权限会很别扭。更稳的是复制一份到 coturn 自己的目录。

sudo tee /usr/local/sbin/sync-caddy-cert-to-coturn.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

DOMAIN="turn.example.com"
SRC_BASE="/var/lib/caddy/.local/share/caddy/certificates"
DST_DIR="/etc/coturn/certs"

CERT="$(find "$SRC_BASE" -type f -path "*/$DOMAIN/$DOMAIN.crt" | sort | tail -n1)"
KEY="$(find "$SRC_BASE" -type f -path "*/$DOMAIN/$DOMAIN.key" | sort | tail -n1)"

if [ -z "$CERT" ] || [ -z "$KEY" ]; then
echo "Caddy certificate for $DOMAIN not found" >&2
exit 1
fi

install -d -o turnserver -g turnserver -m 0750 "$DST_DIR"

changed=0

if ! [ -f "$DST_DIR/$DOMAIN.crt" ] || ! cmp -s "$CERT" "$DST_DIR/$DOMAIN.crt"; then
install -o turnserver -g turnserver -m 0440 "$CERT" "$DST_DIR/$DOMAIN.crt"
changed=1
fi

if ! [ -f "$DST_DIR/$DOMAIN.key" ] || ! cmp -s "$KEY" "$DST_DIR/$DOMAIN.key"; then
install -o turnserver -g turnserver -m 0440 "$KEY" "$DST_DIR/$DOMAIN.key"
changed=1
fi

if [ "$changed" = 1 ]; then
systemctl restart coturn || true
fi
EOF

sudo chmod +x /usr/local/sbin/sync-caddy-cert-to-coturn.sh
sudo /usr/local/sbin/sync-caddy-cert-to-coturn.sh

coturn 的 cert=pkey= 要用 PEM 文件,支持绝对路径。(GitHub)

再加一个定时同步,防止 Caddy 续期后 coturn 还用旧证书:

sudo tee /etc/systemd/system/coturn-cert-sync.service >/dev/null <<'EOF'
[Unit]
Description=Sync Caddy certificate to coturn

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/sync-caddy-cert-to-coturn.sh
EOF

sudo tee /etc/systemd/system/coturn-cert-sync.timer >/dev/null <<'EOF'
[Unit]
Description=Periodically sync Caddy certificate to coturn

[Timer]
OnBootSec=5min
OnUnitActiveSec=12h
Persistent=true

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now coturn-cert-sync.timer

4. 确认阿里云 IPv4/IPv6 情况

PUBLIC4="$(curl -4s https://ifconfig.co)"
PRIVATE4="$(ip -4 -o addr show scope global | awk '!/docker|podman|br-|virbr|wg|tun|tailscale/ {print $4; exit}' | cut -d/ -f1)"

PUBLIC6="$(curl -6s https://ifconfig.co || true)"

echo "PUBLIC4=$PUBLIC4"
echo "PRIVATE4=$PRIVATE4"
echo "PUBLIC6=$PUBLIC6"
ip -6 addr show scope global

阿里云 VPC 里,公网 IPv4 / EIP 往往是网关 NAT 映射,不一定直接出现在系统网卡上;阿里云文档也说明,在安全组里控制具体 ECS 时通常用实例的私网 IP,因为公网 IP/EIP 是云网关上的 NAT 地址。(alibabacloud.com)

所以:

如果 PUBLIC4 != PRIVATE4,coturn 里用:

external-ip=公网IPv4/私网IPv4

例如:

external-ip=8.8.8.8/172.16.1.23

IPv6 一般是直接分配到网卡上的公网 IPv6,确认 ip -6 addr show scope global 里有它即可。阿里云 IPv6 需要 VPC/vSwitch 开 IPv6,系统网卡识别到 global IPv6,并配置 IPv6 安全组规则。

5. 写 coturn 配置

生成一个用户密码:

TURN_USER="turnuser"
TURN_PASS="$(openssl rand -base64 32)"
TURN_KEY="$(turnadmin -k -u "$TURN_USER" -r turn.example.com -p "$TURN_PASS")"

echo "TURN_USER=$TURN_USER"
echo "TURN_PASS=$TURN_PASS"
echo "TURN_KEY=$TURN_KEY"

把输出里的 TURN_PASS 保存好,客户端要用。TURN_KEY 写进配置。coturn 官方示例里也建议可以用 turnadmin -k 生成 key,避免把明文密码直接写进配置。(GitHub)

编辑:

sudo nano /etc/turnserver.conf

写入下面配置。注意把 external-ip=user= 改成你自己的实际值。

# 基本身份
server-name=turn.example.com
realm=turn.example.com

# 监听端口
listening-port=3478
tls-listening-port=5349

# IPv4 + IPv6 双栈监听
listening-ip=0.0.0.0
listening-ip=::

# 阿里云 IPv4 如果是公网 NAT 映射,写成 公网IPv4/私网IPv4
# 示例:
# external-ip=8.8.8.8/172.16.1.23
external-ip=你的公网IPv4/你的私网IPv4

# 如果你的公网 IPv4 直接在网卡上,也可以写:
# external-ip=你的公网IPv4

# TLS 证书,来自 Caddy 的复制副本
cert=/etc/coturn/certs/turn.example.com.crt
pkey=/etc/coturn/certs/turn.example.com.key

# 认证
fingerprint
lt-cred-mech

# 把这里替换成openssl rand -base64 32生成的密码
user=turnuser:替换成密码

# relay 端口范围,小规模够用;人多就扩大
min-port=49160
max-port=49200

# 配额,防止被滥用
user-quota=12
total-quota=100
stale-nonce=600

# 日志走 syslog / journalctl
syslog

# 安全:不要允许 multicast peer
no-multicast-peers

# 安全:禁止被当成内网代理打私网/链路本地地址
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff

denied-peer-ip 列表来源于 这里.

coturn 支持 listening-ip=0.0.0.0listening-ip=:: 来监听 IPv4/IPv6;默认普通 TURN 端口是 3478,TLS 端口是 5349;relay UDP 端口范围默认是 49152-65535,可以用 min-port / max-port 收窄。(GitHub)

启动:

sudo systemctl enable --now coturn
sudo systemctl restart coturn

看日志:

sudo journalctl -u coturn -e --no-pager

6. 阿里云安全组 / UFW 放行

阿里云安全组入方向至少放这些:

TCP 3478
UDP 3478
TCP 5349
UDP 5349
UDP 49160-49200

IPv6 也要单独加 IPv6 入方向规则,来源可以是 ::/0。阿里云安全组规则本来就是用于控制 ECS 入/出方向流量的,规则按协议、端口和授权对象匹配。

如果系统开了 UFW:

sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 5349/tcp
sudo ufw allow 5349/udp
sudo ufw allow 49160:49200/udp

确认 /etc/default/ufw 里:

IPV6=yes

然后:

sudo ufw reload

7. 测试

本机检查监听:

sudo ss -lntup | grep -E '3478|5349|turnserver'

测试 TLS 证书:

openssl s_client -connect turn.example.com:5349 -servername turn.example.com -brief

测试 TURN:

turnutils_uclient -v -u turnuser -w '你的TURN_PASS' turn.example.com

测试 TURNS:

turnutils_uclient -v -S -p 5349 -u turnuser -w '你的TURN_PASS' turn.example.com

客户端配置一般写:

[
{
urls: [
"stun:turn.example.com:3478",
"turn:turn.example.com:3478?transport=udp",
"turn:turn.example.com:3478?transport=tcp",
"turns:turn.example.com:5349?transport=tcp"
],
username: "turnuser",
credential: "你的密码"
}
]

PairDrop

浏览器
├─ HTTPS 443 → Caddy → PairDrop 容器 127.0.0.1:3000
└─ STUN/TURN → coturn 3478 / 5349 / relay UDP ports

PairDrop 官方文档明确说,跨网络传输需要自己的 TURN server;PairDrop 通过 RTC_CONFIG 指定给浏览器使用的 STUN/TURN 配置文件。

重点:PairDrop 容器不需要访问 coturnrtc_config.json 是发给浏览器的,浏览器会直接连接。

给 PairDrop 写 RTC 配置

创建目录:

sudo mkdir -p /opt/pairdrop

/opt/pairdrop/rtc_config.json

sudo tee /opt/pairdrop/rtc_config.json >/dev/null <<'EOF'
{
"sdpSemantics": "unified-plan",
"iceServers": [
{
"urls": [
"stun:cttf.example.com:3478",
"turn:cttf.example.com:3478?transport=udp",
"turn:cttf.example.com:3478?transport=tcp",
"turns:cttf.example.com:5349?transport=tcp"
],
"username": "pairdrop",
"credential": "换成coturn里user=pairdrop:后面的那个密码"
}
]
}
EOF

这个文件会被 PairDrop 提供给浏览器,所以这里的 TURN 用户名密码本来就会暴露给访问你 PairDrop 的客户端。自用没问题;如果公开给很多人用,就要靠长密码、配额、防火墙和日志监控控制滥用。


Podman Quadlet 部署 PairDrop

用 system-level Quadlet:

sudo tee /etc/containers/systemd/pairdrop.container >/dev/null <<'EOF'
[Unit]
Description=PairDrop container
After=network-online.target
Wants=network-online.target

[Container]
ContainerName=pairdrop
Image=lscr.io/linuxserver/pairdrop:latest

PublishPort=127.0.0.1:3000:3000

Volume=/opt/pairdrop/rtc_config.json:/home/node/app/rtc_config.json:ro

Environment=PUID=1000
Environment=PGID=1000
Environment=TZ=Asia/Shanghai
Environment=RATE_LIMIT=false
Environment=WS_FALLBACK=false
Environment=RTC_CONFIG=/home/node/app/rtc_config.json
Environment=DEBUG_MODE=false

AutoUpdate=registry

[Service]
Restart=always
TimeoutStartSec=900

[Install]
WantedBy=multi-user.target
EOF

  1. Caddy 反代 PairDrop

例如 /etc/caddy/Caddyfile

cttf.example.com {
reverse_proxy 127.0.0.1:3000
}

然后:

sudo systemctl reload caddy

Caddy 的 reverse_proxy 默认会设置或追加 X-Forwarded-For,并设置 X-Forwarded-ProtoX-Forwarded-Host。(Caddy Web Server)


  1. 测试

看 PairDrop:

curl -I http://127.0.0.1:3000
curl -I https://cttf.example.com

看 coturn:

sudo ss -lntup | grep -E '3478|5349|turnserver'

测 TLS:

openssl s_client -connect cttf.example.com:5349 -servername cttf.example.com -brief

测 TURN:

turnutils_uclient -v -u pairdrop -w '你的TURN密码' cttf.example.com

关于 WS_FALLBACK

我建议你先保持:

WS_FALLBACK=false

因为你已经有 coturn 了。WS_FALLBACK=true 会在 WebRTC 不可用时通过 PairDrop 服务器中转,但官方文档提醒,这样就不是 peer-to-peer 了,流量会走服务器,并且服务器可读这部分 fallback 流量。

Authelia

建议架构这样分:

Caddy
├─ auth.example.com -> Authelia
├─ drop.example.com -> PairDrop,Caddy forward_auth 到 Authelia
├─ gist.example.com -> Opengist,应用内 OIDC 登录到 Authelia
└─ hs.example.com -> Headscale,不要加 forward_auth

原因:PairDrop 本身没有完善登录系统,适合用 Caddy + Authelia 反代保护;Opengist 原生支持 OpenID Connect,所以更适合直接接 Authelia 做 SSO;Headscale 也支持 OIDC,但不要用 Caddy forward_auth 套在 Headscale 外面。Authelia 的 Caddy 集成用的是 Caddy 官方 forward_auth 机制,Authelia 文档要求 Caddy v2.5.1+;Opengist 官方文档也明确支持 GitHub、Gitea、GitLab 和 OpenID Connect;Headscale 反代必须支持 WebSocket,Caddy 的 reverse_proxy 可以正常处理。(Authelia)

下面我用这些占位域名,你自己替换:

auth.example.com
drop.example.com
gist.example.com
hs.example.com
example.com

部署 Authelia

创建目录:

sudo mkdir -p /opt/authelia/config /opt/authelia/secrets
sudo chmod 700 /opt/authelia /opt/authelia/config /opt/authelia/secrets

生成几个密钥:

sudo bash -c 'tr -dc A-Za-z0-9 </dev/urandom | head -c 64 > /opt/authelia/secrets/session_secret'
sudo bash -c 'tr -dc A-Za-z0-9 </dev/urandom | head -c 64 > /opt/authelia/secrets/storage_encryption_key'
sudo bash -c 'tr -dc A-Za-z0-9 </dev/urandom | head -c 64 > /opt/authelia/secrets/jwt_secret'
sudo bash -c 'tr -dc A-Za-z0-9 </dev/urandom | head -c 64 > /opt/authelia/secrets/oidc_hmac_secret'
sudo chmod 600 /opt/authelia/secrets/*

生成 OIDC RSA 私钥:

sudo openssl genrsa -out /opt/authelia/secrets/oidc.rsa.pem 2048
sudo openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt \
-in /opt/authelia/secrets/oidc.rsa.pem \
-out /opt/authelia/secrets/oidc.private.pem
sudo chmod 600 /opt/authelia/secrets/oidc.private.pem

Authelia 的 OIDC Provider 需要 HMAC secret 和至少一个用于签名的 JWK;RSA key 最低 2048 bit,RS256 是常见配置。(GitHub)

生成你的 Authelia 用户密码 hash:

sudo podman run --rm -it ghcr.io/authelia/authelia:4.39.19 \
authelia crypto hash generate argon2

它会让你输入密码,然后输出类似:

Digest: $argon2id$v=19$...

记下 Digest: 后面的整段。Authelia 官方建议用 Authelia CLI 或容器生成密码 hash。(Authelia)

创建用户文件:

sudo nano /opt/authelia/config/users_database.yml

内容:

users:
fallrain: # 用于登录的用户名,注意用用户名登录而不是邮箱!
disabled: false
displayname: "fallrain"
password: "$argon2id$v=19$这里换成你的hash"
email: "you@example.com"
groups:
- admins

生成 Opengist OIDC secret

这个 secret 有两份:

一份明文给 Opengist 用;一份 hash 放进 Authelia 配置。

OPENGIST_SECRET="$(tr -dc A-Za-z0-9 </dev/urandom | head -c 64)"
echo "$OPENGIST_SECRET" | sudo tee /opt/authelia/secrets/opengist_oidc_secret_plain
sudo chmod 600 /opt/authelia/secrets/opengist_oidc_secret_plain

sudo podman run --rm ghcr.io/authelia/authelia:4.39.19 \
authelia crypto hash generate pbkdf2 --variant sha512 --password "$OPENGIST_SECRET"

记下输出的 PBKDF2 hash,放到后面的 client_secret

Authelia 的 OIDC client 配置支持 client_secret 存 hash;官方 Opengist 集成示例也使用 PBKDF2 hash,并要求 Opengist 回调地址为 /oauth/openid-connect/callback。(Authelia)


写 Authelia 配置

先把几个 secret 读出来:

sudo cat /opt/authelia/secrets/session_secret
sudo cat /opt/authelia/secrets/storage_encryption_key
sudo cat /opt/authelia/secrets/jwt_secret
sudo cat /opt/authelia/secrets/oidc_hmac_secret
sudo cat /opt/authelia/secrets/oidc.private.pem

创建配置:

sudo nano /opt/authelia/config/configuration.yml

内容如下,把域名、secret、OIDC 私钥、Opengist client hash 全部替换掉:

theme: auto

server:
address: tcp://0.0.0.0:9091/

log:
level: info
format: text

totp:
issuer: example.com

identity_validation:
reset_password:
jwt_secret: "替换为 /opt/authelia/secrets/jwt_secret 的内容"

authentication_backend:
file:
path: /config/users_database.yml

access_control:
default_policy: deny
rules:
- domain: drop.example.com
policy: one_factor

session:
secret: "替换为 /opt/authelia/secrets/session_secret 的内容"
cookies:
- domain: example.com
authelia_url: https://auth.example.com
default_redirection_url: https://drop.example.com

storage:
encryption_key: "替换为 /opt/authelia/secrets/storage_encryption_key 的内容"
local:
path: /config/db.sqlite3

notifier:
filesystem:
filename: /config/notification.txt

identity_providers:
oidc:
hmac_secret: "替换为 /opt/authelia/secrets/oidc_hmac_secret 的内容"
jwks:
- key: |
-----BEGIN PRIVATE KEY-----
这里粘贴 /opt/authelia/secrets/oidc.private.pem 内容
注意每行前面保留 10 个空格
-----END PRIVATE KEY-----

clients:
- client_id: opengist
client_name: Opengist
client_secret: "$pbkdf2-sha512$这里换成刚才生成的Opengist secret hash"
authorization_policy: one_factor
redirect_uris:
- https://gist.example.com/oauth/openid-connect/callback
scopes:
- openid
- email
- profile
- groups
grant_types:
- authorization_code
token_endpoint_auth_method: client_secret_post

注意:auth.example.com 必须是 HTTPS,Authelia 官方明确要求 Authelia 通过 HTTPS 提供服务。(Authelia)


Authelia Quadlet

sudo nano /etc/containers/systemd/authelia.container

内容:

[Unit]
Description=Authelia container
After=network-online.target
Wants=network-online.target

[Container]
ContainerName=authelia
Image=ghcr.io/authelia/authelia:4.39.19
AutoUpdate=registry
PublishPort=127.0.0.1:9091:9091
Volume=/opt/authelia/config:/config

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

启动:

sudo systemctl daemon-reload
sudo systemctl start authelia.service
sudo journalctl -u authelia.service -f

Caddy 配置:Authelia + PairDrop

你的 Caddyfile 加:

auth.example.com {
reverse_proxy 127.0.0.1:9091
}

drop.example.com {
forward_auth 127.0.0.1:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}

reverse_proxy 127.0.0.1:3000
}

这里 127.0.0.1:3000 改成你的 PairDrop 实际监听端口。

然后:

sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo systemctl reload caddy

访问 https://drop.example.com,应该会先跳到 Authelia 登录。


给 Opengist 配 OIDC(可选)

创建 Opengist 环境文件:

sudo install -o root -g root -m 600 /dev/null /etc/opengist.env
sudo nano /etc/opengist.env

内容:

OG_EXTERNAL_URL=https://gist.example.com

OG_OIDC_PROVIDER_NAME=Authelia
OG_OIDC_CLIENT_KEY=opengist
OG_OIDC_SECRET=这里填 /opt/authelia/secrets/opengist_oidc_secret_plain 的明文内容
OG_OIDC_DISCOVERY_URL=https://auth.example.com/.well-known/openid-configuration
OG_OIDC_GROUP_CLAIM_NAME=groups
OG_OIDC_ADMIN_GROUP=admins

Opengist 官方说明可以用环境变量配置 OIDC,其中包括 provider name、client key、secret、discovery URL、group claim 和 admin group。(Authelia)

修改你的 Opengist Quadlet:

sudo nano /etc/containers/systemd/opengist.container

推荐变成:

[Unit]
Description=Opengist container
After=network-online.target
Wants=network-online.target

[Container]
ContainerName=opengist
Image=ghcr.io/thomiceli/opengist:1
AutoUpdate=registry
EnvironmentFile=/etc/opengist.env
PublishPort=127.0.0.1:6157:6157
PublishPort=2222:2222
Volume=/opt/opengist:/opengist

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

重启:

sudo systemctl daemon-reload
sudo systemctl restart opengist.service
sudo journalctl -u opengist.service -f

Caddy:

gist.example.com {
reverse_proxy 127.0.0.1:6157
}

这里不要再套 Authelia forward_auth,因为 Opengist 自己会走 OIDC 登录。否则 OAuth 回调、Git HTTP 操作可能会变复杂。

Opengist 后台里建议打开:

Disable signup
Disable login form

Opengist 管理面板支持禁用注册、要求登录、禁用登录表单;禁用登录表单后用户只会看到 OAuth providers。(Opengist)

Headscale

Headscale 官方容器文档说明配置目录挂载到 /etc/headscale,数据目录挂载到 /var/lib/headscale,容器命令是 serve;它也说明容器镜像可用 docker.io/headscale/headscale:<VERSION>ghcr.io/juanfont/headscale:<VERSION>。(Headscale)

创建目录:

sudo mkdir -p /opt/headscale/config /opt/headscale/lib

配置:

sudo nano /opt/headscale/config/config.yaml

最小可用配置:

server_url: https://hs.example.com
listen_addr: 127.0.0.1:8070
metrics_listen_addr: 127.0.0.1:9080
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false

noise:
private_key_path: /var/lib/headscale/noise_private.key

prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential

derp:
server:
enabled: true
region_id: 999
region_code: "aliyun"
region_name: "Aliyun DERP"
ipv4: 你的公网IPv4
ipv6: 你的公网IPv6
stun_listen_addr: "[::]:3477"
private_key_path: /var/lib/headscale/derp_server_private.key

urls: []

paths: []
auto_update_enabled: true
update_frequency: 3h

database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true

tls_cert_path: ""
tls_key_path: ""

log:
level: info
format: text

policy:
mode: file
path: ""

dns:
magic_dns: true
base_domain: tail.example.com
override_local_dns: false
nameservers:
global: []
search_domains: []
extra_records: []

unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"

logtail:
enabled: false

randomize_client_port: false

taildrop:
enabled: true

Headscale 的反代模式里,server_url 应该是公网 HTTPS 域名,listen_addr 可以监听容器内 0.0.0.0:8080,TLS 留给 Caddy 处理时 tls_cert_pathtls_key_path 为空。(Headscale)

创建 Quadlet:

sudo nano /etc/containers/systemd/headscale.container

DERP 服务器需要知道真实源 IP, 所以需要设置 Network=host,内容:

[Unit]
Description=Headscale container
After=network-online.target
Wants=network-online.target

[Container]
ContainerName=headscale
Image=docker.io/headscale/headscale:0.28.0
AutoUpdate=registry
Exec=serve
Network=host
Volume=/opt/headscale/config:/etc/headscale:ro
Volume=/opt/headscale/lib:/var/lib/headscale
Tmpfs=/var/run/headscale
HealthCmd=["headscale","health"]
HealthStartPeriod=60s

[Service]
Restart=always

[Install]
WantedBy=multi-user.target

启动:

sudo systemctl daemon-reload
sudo systemctl start headscale.service
sudo journalctl -u headscale.service -f

Caddy:

hs.example.com {
reverse_proxy 127.0.0.1:8081
}

Headscale 反向代理必须支持 WebSocket;不要把 Cloudflare 橙云代理套在 Headscale 前面,Headscale 文档明确说 Cloudflare proxy/tunnel 不支持它需要的 WebSocket POST。(Headscale)


Headscale 加设备

先建用户:

sudo podman exec -it headscale headscale users create richard
sudo podman exec -it headscale headscale users list

创建预授权 key,注意新版本通常用 user ID:

sudo podman exec -it headscale headscale preauthkeys create --user 1 --reusable --expiration 24h

客户端加入:

sudo tailscale up \
--login-server https://hs.example.com \
--authkey hskey-auth-xxxx

Headscale 的预授权 key 流程就是先创建 user,再创建 preauthkey,然后客户端用 tailscale up --login-server ... --authkey ... 注册。(Juan Font)


可选:Headscale 也接 Authelia OIDC

这个可以后面再做。做法是给 Authelia 再加一个 OIDC client:

      - client_id: headscale
client_name: Headscale
client_secret: "$pbkdf2-sha512$这里换成Headscale client secret hash"
authorization_policy: one_factor
redirect_uris:
- https://hs.example.com/oidc/callback
scopes:
- openid
- email
- profile
grant_types:
- authorization_code

然后在 Headscale config.yaml 里加:

oidc:
issuer: https://auth.example.com
client_id: headscale
client_secret: "这里填 Headscale OIDC 明文 secret"
expiry: 180d
scope: ["openid", "profile", "email"]
email_verified_required: false

Headscale OIDC 基本配置需要 issuer URL、client ID、client secret 和 redirect URI,redirect URI 通常是 https://headscale.example.com/oidc/callback。 (Headscale)


最后检查顺序

sudo systemctl restart authelia
sudo systemctl restart opengist
sudo systemctl restart headscale
sudo systemctl reload caddy

sudo podman ps
sudo journalctl -u authelia -n 100
sudo journalctl -u opengist -n 100
sudo journalctl -u headscale -n 100

建议你先按这个顺序跑通:

Authelia 登录页
→ PairDrop forward_auth
→ Opengist OIDC
→ Headscale 基础 preauthkey
→ Headscale OIDC

这样排错最清楚。