跳到主要内容

Angie

概览​

Angie 是 nginx 的分支,拥有更丰富的模块集、内置 ACME 客户端, 并支持 HTTP/3。用于 XLADA 面板时,它的用法与 nginx 相同:终止 TLS 并把请求代理到面板和订阅页面的 loopback 端口。

面板和订阅页面必须收到 X-Forwarded-For 和 X-Forwarded-Proto: https 请求头。它们的代码中有 middleware,如果 没有这些请求头就会断开连接:没有它们,面板无法在代理后面启动。同时, 客户端真实 IP 是 HWID 限制、管理员操作审计和 登录防暴力破解所必需的。

示例中的域名是 panel.example.com 和 sub.example.com,上游是 127.0.0.1:3000(面板)和 127.0.0.1:3010(订阅页面)—— 这是 xpanel 和 xpanel-subpage 容器在宿主机 loopback 上的端口。

注意

需要已注册的域名。在域名指向你的服务器之前,无法签发 Let's Encrypt 证书。

DNS​

检查服务器 IP 地址(最好是静态的),并在 DNS 服务商或注册商处创建记录:

类型名称值
Apanel服务器的 IPv4 地址
Asub服务器的 IPv4 地址
AAAApanel服务器的 IPv6 地址
AAAAsub服务器的 IPv6 地址

在 Cloudflare 中,记录创建在 DNS → Records → Create record 部分。

  • 灰云 —— 流量直接通过,Angie 能看到客户端真实 IP; 如果 Cloudflare 不可用或造成干扰,就使用这种模式。
  • 橙云 —— 流量经过 Cloudflare 网络:服务器 IP 被隐藏,攻击防护生效。在此模式下 Angie 看到的是 Cloudflare 地址, 因此要获得正确的 IP,请启用 realip 模块并信任 CF-Connecting-IP 请求头。

TLS 证书​

创建用于证书和 ACME 验证的目录:

sudo mkdir -p /etc/angie/ssl /var/www/acme

方案 1:certbot​

sudo apt-get update && sudo apt-get install -y certbot
sudo certbot certonly --webroot -w /var/www/acme \
-d panel.example.com -d sub.example.com \
--email admin@example.com --agree-tos --no-eff-email

sudo ln -sf /etc/letsencrypt/live/panel.example.com/fullchain.pem /etc/angie/ssl/fullchain.pem
sudo ln -sf /etc/letsencrypt/live/panel.example.com/privkey.pem /etc/angie/ssl/privkey.key

方案 2:acme.sh​

curl https://get.acme.sh | sh -s email=admin@example.com

acme.sh --issue --webroot /var/www/acme \
-d panel.example.com -d sub.example.com \
--key-file /etc/angie/ssl/privkey.key \
--fullchain-file /etc/angie/ssl/fullchain.pem \
--reloadcmd "docker exec angie angie -s reload"

两个客户端都会自动续期证书:certbot 通过自己的 systemd 定时器, acme.sh 通过每 60 天一次的 cron 任务。端口 80 必须开放,并能响应 我们将在下面的配置中描述的 ACME 验证路径。

Angie 也有自己的 ACME 客户端:无需 certbot 和 acme.sh 即可签发证书。这种情况的完整配置见 “内置 ACME 方案”。

angie.conf 完整配置​

sudo mkdir -p /opt/angie && cd /opt/angie
sudo nano angie.conf
/opt/angie/angie.conf
# ─── Upstream:xpanel 和 xpanel-subpage 容器 ─────────────────────────
upstream xpanel_panel {
server 127.0.0.1:3000;
keepalive 32;
}

upstream xpanel_subpage {
server 127.0.0.1:3010;
keepalive 16;
}

# WebSocket:只在 Upgrade 时填入 Connection 请求头
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

# ─── 为所有虚拟主机启用 Gzip ─────────────────────────────────────────
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
application/atom+xml
application/geo+json
application/javascript
application/json
application/ld+json
application/manifest+json
application/rdf+xml
application/rss+xml
application/xhtml+xml
application/xml
application/wasm
font/eot
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;

# ─── HTTP:ACME 验证并重定向到 HTTPS ─────────────────────────────────
server {
listen 80;
listen [::]:80;
server_name panel.example.com sub.example.com;

location /.well-known/acme-challenge/ {
root /var/www/acme;
}

location / {
return 301 https://$host$request_uri;
}
}

# ─── XLADA 面板 ──────────────────────────────────────────────────────
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name panel.example.com;

# TLS(Mozilla Intermediate)
ssl_certificate /etc/angie/ssl/fullchain.pem;
ssl_certificate_key /etc/angie/ssl/privkey.key;
ssl_trusted_certificate /etc/angie/ssl/fullchain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 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;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
resolver_timeout 2s;

# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;

# 请求体大小 —— 为通过界面上传文件留出余量
client_max_body_size 25m;

# 界面静态资源:文件名中的哈希允许长期缓存
location ^~ /assets/ {
proxy_http_version 1.1;
proxy_pass http://xpanel_panel;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";

expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}

location / {
proxy_http_version 1.1;
proxy_pass http://xpanel_panel;

# 没有这两个请求头,面板会断开连接
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;

# WebSocket —— 到节点的 SSH 终端
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;
proxy_buffering off; # 流式传输和 SSE
proxy_cache off;
}
}

# ─── 订阅页面 ────────────────────────────────────────────────────────
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name sub.example.com;

ssl_certificate /etc/angie/ssl/fullchain.pem;
ssl_certificate_key /etc/angie/ssl/privkey.key;
ssl_trusted_certificate /etc/angie/ssl/fullchain.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 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;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=60s;
resolver_timeout 2s;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;

client_max_body_size 10m;

# 订阅页面的构建产物静态资源
location ^~ /assets/ {
proxy_http_version 1.1;
proxy_pass http://xpanel_subpage;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";

expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}

location / {
proxy_http_version 1.1;
proxy_pass http://xpanel_subpage;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffering off;
}
}

# ─── 拦截按 IP 和未知域名的 TLS 握手 ─────────────────────────────────
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;

ssl_reject_handshake on;
}

在宿主机上安装时,把文件复制到 /etc/angie/http.d/xlada.conf。 以 Docker 运行时,它按下文所示整体挂载到容器中。

我们不使用 reuseport 指令:它与同一地址和端口上的多个 虚拟主机不兼容 —— Angie 会以 duplicate listen options 错误退出。

内置 ACME 方案​

如果不需要外部客户端,Angie 可以自己签发证书。下面的配置是 自给自足的:无需事先签发任何东西,Angie 会在 首次启动时获取证书并自动续期。域名所有权验证 通过 443 端口上的 TLS-ALPN 进行,因此 80 端口只做重定向。ACME 状态存储在 /var/lib/angie/acme/ —— 该目录必须在容器 重启后保留。

/opt/angie/angie.conf
# ─── Upstream:xpanel 和 xpanel-subpage 容器 ─────────────────────────
upstream xpanel_panel {
server 127.0.0.1:3000;
keepalive 32;
}

upstream xpanel_subpage {
server 127.0.0.1:3010;
keepalive 16;
}

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

# ─── 内置 ACME ───────────────────────────────────────────────────────
resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 valid=60s;
resolver_timeout 2s;
acme_client acme_le https://acme-v02.api.letsencrypt.org/directory;

# ─── 为所有虚拟主机启用 Gzip ─────────────────────────────────────────
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
application/atom+xml
application/geo+json
application/javascript
application/json
application/ld+json
application/manifest+json
application/rdf+xml
application/rss+xml
application/xhtml+xml
application/xml
application/wasm
font/eot
font/otf
font/ttf
image/svg+xml
text/css
text/javascript
text/plain
text/xml;

# ─── HTTP:重定向到 HTTPS ────────────────────────────────────────────
server {
listen 80;
listen [::]:80;
server_name panel.example.com sub.example.com;

location / {
return 301 https://$host$request_uri;
}
}

# ─── XLADA 面板 ──────────────────────────────────────────────────────
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name panel.example.com;

acme acme_le;
ssl_certificate $acme_cert_acme_le;
ssl_certificate_key $acme_cert_key_acme_le;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 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;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:ACME_TLS:10m;
ssl_session_tickets off;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;

client_max_body_size 25m;

location ^~ /assets/ {
proxy_http_version 1.1;
proxy_pass http://xpanel_panel;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";

expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}

location / {
proxy_http_version 1.1;
proxy_pass http://xpanel_panel;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 86400s;
proxy_read_timeout 86400s;
proxy_buffering off;
proxy_cache off;
}
}

# ─── 订阅页面 ────────────────────────────────────────────────────────
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name sub.example.com;

acme acme_le;
ssl_certificate $acme_cert_acme_le;
ssl_certificate_key $acme_cert_key_acme_le;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 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;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:ACME_TLS:10m;
ssl_session_tickets off;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;

client_max_body_size 10m;

location ^~ /assets/ {
proxy_http_version 1.1;
proxy_pass http://xpanel_subpage;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Connection "";

expires 365d;
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}

location / {
proxy_http_version 1.1;
proxy_pass http://xpanel_subpage;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;

proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_buffering off;
}
}

# ─── 拦截按 IP 和未知域名的 TLS 握手 ─────────────────────────────────
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;

ssl_reject_handshake on;
}

检查配置并重载 Angie:

sudo angie -t && sudo angie -s reload
docker compose exec angie angie -t && docker compose exec angie angie -s reload

在 Docker 中运行​

/opt/angie/docker-compose.yml
services:
angie:
image: docker.angie.software/angie:1.9.0
container_name: angie
restart: unless-stopped
# host 模式,让 Angie 看到 127.0.0.1:3000 和 127.0.0.1:3010
network_mode: host
environment:
- TZ=UTC
volumes:
- ./angie.conf:/etc/angie/http.d/default.conf:ro
- /etc/angie/ssl:/etc/angie/ssl:ro # Let's Encrypt 证书
- /var/www/acme:/var/www/acme:ro # ACME 验证目录
- angie-ssl-data:/var/lib/angie/acme/ # 内置 ACME 的状态

volumes:
angie-ssl-data:
name: angie-ssl-data
cd /opt/angie
docker compose up -d
docker compose logs -f -t

选择 host 模式是有意为之:容器看到的 loopback 端口 127.0.0.1:3000 和 127.0.0.1:3010 与宿主机相同,因此 angie.conf 中的上游无需改写为容器名。同时证书和 ACME 验证目录仍保留在宿主机上。

检查与重载​

# 检查语法
sudo angie -t
docker compose exec angie angie -t

# 完整导出最终配置
sudo angie -T

# 平滑重载,不中断连接
sudo angie -s reload
docker compose exec angie angie -s reload

日志中该看什么​

journalctl -u angie -f # 安装在宿主机
docker compose logs -f -t angie
  • [emerg] —— 配置错误,angie -t 会指出行号;
  • connect() failed (111: Connection refused) —— 面板或订阅页面没有 在预期端口上运行;
  • upstream timed out —— 为耗时操作增大 proxy_read_timeout;
  • SSL_do_handshake() failed —— 普通扫描器,无需处理。
curl -I https://panel.example.com
curl -I https://sub.example.com

两个响应都应以 HTTP/2 200 开头。如果面板审计中看到 Cloudflare 地址,请通过 realip 模块启用对 CF-Connecting-IP 的信任。

接下来​