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 服务商或注册商处创建记录:
| 类型 | 名称 | 值 |
|---|---|---|
A | panel | 服务器的 IPv4 地址 |
A | sub | 服务器的 IPv4 地址 |
AAAA | panel | 服务器的 IPv6 地址 |
AAAA | sub | 服务器的 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
# ─── 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/ —— 该目录必须在容器
重启后保留。
# ─── 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 中运行
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 的信任。