wireguard

我的架构:云服务器A,绑定域名www.test.com,并且安装了wireguard,wireguard ip : AWIP,本地虚拟机B,Ubuntu26.04,wirdguard ip:BWIP,现在我想在B中安装ghost博客,然后通过A的域名访问

你的链路本质是:B 上跑 Ghost,A 做七层反向代理并终止 TLS。域名解析到 A 的公网 IP,A 通过 WireGuard 把请求转给 BWIP:2368,B 不直接暴露公网。这是最省事、最容易排查的做法。

浏览器
  │  https://www.test.com  (443)
  ▼
云服务器 A   nginx + Let's Encrypt 证书
  │  proxy_pass http://BWIP:2368   (走 wg0 隧道,明文)
  ▼
虚拟机 B   Ghost (Node,监听 2368)

三个必须守住的前提:

  1. Ghost 的 url 必须写死 https://www.test.com,和公开访问地址完全一致,否则后台会重定向循环。
  2. TLS 只在 A 上做,B 不申请证书,A→B 走隧道内网。
  3. A 反代必须带 X-Forwarded-Proto: https,否则 Ghost 认为自己在 http 后面,会不停 301。

第 1 步:打通 WireGuard 并放行端口

两端 wg0 的 AllowedIPs 要互相对得上(A 端放 BWIP/32,B 端放 AWIP/32 或整个 10.x.0.0/24),并且 B 在 NAT 后要加 PersistentKeepalive = 25。云厂商安全组放行 51820/udp、80/tcp、443/tcp。

验证(B 上):

wg show                      # 确认 latest handshake 是几秒前
ping -c 3 AWIP

A 上:

ping -c 3 BWIP

ping 不通就别往下走,先修隧道。


第 2 步:在 B 上安装 Ghost(去掉 nginx 和 SSL)

B 是 Ubuntu 26.04,在 Ghost 官方支持范围内。装 MySQL 8、Node 22、Ghost-CLI:

# MySQL(注意 root 要改成密码认证,Ghost 不支持 socket 认证)
sudo apt-get update && sudo apt-get install -y mysql-server
sudo mysql
#   ALTER USER 'root'@'localhost' IDENTIFIED WITH 'caching_sha2_password' BY '<密码>';
#   FLUSH PRIVILEGES;  exit

# Node 22
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
  | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
  | sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update && sudo apt-get install -y nodejs

# Ghost-CLI
sudo npm install ghost-cli@latest -g

建目录并安装:

sudo mkdir -p /var/www/ghostblog
sudo chown $USER:$USER /var/www/ghostblog
sudo chmod 775 /var/www/ghostblog
cd /var/www/ghostblog
ghost install

问答按这个填:

问题 填什么
Blog URL https://www.test.com(必须带 https)
MySQL hostname 回车(localhost)
MySQL 用户/密码 root + 刚设的密码
数据库名 ghostblog
建独立 MySQL 用户 Yes
Set up NGINX? No(A 上已经有 nginx 了)
Set up SSL? No(证书在 A 上)
Set up systemd? Yes
Start Ghost? Yes

然后改监听地址,让 A 能连进来。编辑 /var/www/ghostblog/config.production.json:

{
  "url": "https://www.test.com",
  "server": {
    "host": "0.0.0.0",
    "port": 2368
  }
}
ghost restart
为什么用 0.0.0.0 而不是 BWIP:如果用 BWIP,B 重启时 wg0 还没起来,Ghost bind 会失败并进入重启循环。用 0.0.0.0 + 防火墙限制来源更稳。

B 上只放行 A 的隧道 IP:

sudo ufw allow from AWIP to any port 2368 proto tcp
sudo ufw reload

第 3 步:A 上配置 nginx 反代(先 80,后 443)

A 是公网入口,要做四件事:占住 80/443 → 反代到隧道 → 上证书 → 验证。下面按执行顺序给全,命令都写在注释里,你直接照抄;BWIP 换成你实际的隧道内网 IP(如 10.0.0.2)。

说明:下面的路径用 /etc/nginx/conf.d/,因为 Ubuntu/Debian 和 RHEL 系(Anolis/CentOS/Alibaba Cloud Linux)都认这个目录,能省掉 sites-available 的差异。包管理命令两种给全,按 cat /etc/os-release 的结果选一种。

阶段 0:动手前先取证

在 A 上依次跑,每条都看输出,别跳:

# 1) A 是什么系统、有没有 SELinux
cat /etc/os-release | head -3
getenforce 2>/dev/null || echo "no selinux"

# 2) 域名是否已经解析到 A 的公网 IP(两边要一致)
dig +short www.test.com
curl -4 -s ifconfig.me; echo

# 3) WireGuard 隧道通不通
wg show
ping -c 3 BWIP

# 4) B 上的 Ghost 是否活着(关键前置)
curl -I --max-time 5 http://BWIP:2368

# 5) 80/443 有没有被别的程序占了
sudo ss -lntp | grep -E ':(80|443)\b'

判读标准:

  • 第 2 条:dig 返回的 IP 必须等于 curl ifconfig.me 的结果。不等就先改 DNS,证书签发会卡在这。
  • 第 3 条:wg show 里 latest handshake 要是几十秒内;ping 丢包 0。
  • 第 4 条:返回 301 且 Location 指向 https://www.test.com 是最理想的结果——说明 Ghost 活着、url 也配对。返回 Connection refused 就先回 B 处理,A 这边做什么都没用。
  • 第 5 条:如果 80/443 已被 宝塔/apache/caddy/docker 容器 占用,先决定是停掉还是复用,别硬装。
  • getenforce 如果返回 Enforcing,记住,阶段 4 大概率要在 SELinux 上补一脚。

阶段 1:装 nginx 并放行端口

# ---- Debian / Ubuntu ----
sudo apt-get update
sudo apt-get install -y nginx
sudo systemctl enable --now nginx

# ---- RHEL / Anolis / CentOS / Rocky ----
sudo dnf install -y nginx
sudo systemctl enable --now nginx

验证:

nginx -v
curl -I http://127.0.0.1        # 期望 HTTP/1.1 200,欢迎页

防火墙(按你实际启用的那个做,两个都没开就跳过,但云厂商安全组必须单独开):

# ufw
sudo ufw allow 'Nginx Full'
sudo ufw status verbose

# firewalld
sudo firewall-cmd --permanent --add-service=http --add-service=https
sudo firewall-cmd --reload
sudo firewall-cmd --list-services

云控制台安全组入方向放行:80/tcp、443/tcp,源 0.0.0.0/0。这一步漏了,后面 certbot 一定失败,而且现象是"卡住等超时",很容易误判成 DNS 问题。


阶段 2:先配 80 反代,跑通再上证书

这一步刻意不碰 443,让排查只有"反代通不通"一个变量。

2.1 全局 upgrade 映射

新建 /etc/nginx/conf.d/00-upgrade-map.conf:

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

00- 前缀是让它在按字母序加载时排在站点配置前面。这个 map 必须放在 http 块内(conf.d 里的文件天然就在 http 块内,所以直接放就行)。

2.2 站点配置

新建 /etc/nginx/conf.d/www.test.com.conf:

server {
    listen      80;
    server_name www.test.com;

    access_log /var/log/nginx/www.test.com.access.log;
    error_log  /var/log/nginx/www.test.com.error.log warn;

    client_max_body_size 50m;          # 传主题/大图,不够会 413

    location / {
        proxy_pass http://BWIP:2368;            # ← 换成实际隧道 IP
        proxy_http_version 1.1;

        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;   # 关键,别写成 $scheme
        proxy_set_header X-Forwarded-Host  $host;

        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;

        proxy_buffering off;               # Ghost 后台有 SSE,必须关
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
        proxy_redirect off;
    }
}
X-Forwarded-Proto 这里写死 https 而不是 $scheme,是因为这个 80 的 server 块将来会被 certbot 改成"80 只做跳转",真正的业务请求全走 443。写死可以避免 certbot 改配置后语义漂移导致 301 循环。

2.3 加载并验证

sudo nginx -t
sudo systemctl reload nginx

# 本机带 Host 头打一下
curl -I -H 'Host: www.test.com' http://127.0.0.1

期望 HTTP/1.1 200。如果拿到的不是 200:

# 看请求到底进了哪个 server 块、反代有没有报错
sudo tail -n 20 /var/log/nginx/www.test.com.access.log
sudo tail -n 20 /var/log/nginx/www.test.com.error.log
  • 如果 access log 里根本没有你这次的记录 → 请求被别的 server(比如 sites-enabled/default)抢走了,检查有没有 default_server 冲突。
  • 如果 error log 里是 connect() failed (111: Connection refused) → B 上 Ghost 没起或没监听 0.0.0.0。
  • 如果 error log 里是 connect() failed (13: Permission denied) → 这是 SELinux,跳到下面「阶段 4 的 SELinux 补丁」。

阶段 3:签证书,切到 443

# ---- Debian / Ubuntu ----
sudo apt-get install -y certbot python3-certbot-nginx

# ---- RHEL / Anolis(需要 EPEL)----
sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx

# 签发(HTTP-01 验证走 80 端口)
sudo certbot --nginx -d www.test.com --redirect --agree-tos -m you@example.com

--redirect 会让 certbot 自动把 80 的 server 块改成 301 跳 https,并新增 443 的 server 块。

签完,务必人工核对生成的 443 配置有没有把 proxy 头带过来(certbot 是复制原 server 块,正常情况下 location 会一起过去,但版本差异下偶尔会丢):

sudo nginx -T | sed -n '/server_name www.test.com/,/^}/p'

对照检查 443 的 location 里这四行必须都在:

proxy_set_header Host              $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
proxy_buffering off;

最终 443 块应该长这样(可作参照):

server {
    listen      443 ssl;
    server_name www.test.com;

    ssl_certificate     /etc/letsencrypt/live/www.test.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.test.com/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    access_log /var/log/nginx/www.test.com.access.log;
    error_log  /var/log/nginx/www.test.com.error.log warn;
    client_max_body_size 50m;

    location / {
        proxy_pass http://BWIP:2368;
        proxy_http_version 1.1;
        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_buffering off;
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;
        proxy_redirect off;
    }
}
sudo nginx -t && sudo systemctl reload nginx

续期必须验证一次,否则 90 天后全站证书过期:

sudo certbot renew --dry-run
systemctl list-timers | grep -i certbot

阶段 4:全链路验证 + SELinux 补丁

4.1 验证

# 证书有效期与域名
openssl s_client -connect www.test.com:443 -servername www.test.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -dates

# 首页
curl -I https://www.test.com

# 后台
curl -s -o /dev/null -w '%{http_code}\n' https://www.test.com/ghost

# 80 是否已跳 443
curl -I http://www.test.com

期望:首页 200、/ghost 200(或 302 到登录页)、http:// 返回 301 Location: https://www.test.com。

最后浏览器打开 https://www.test.com/ghost,应该能正常加载登录页。

4.2 SELinux 补丁(RHEL 系必看)

如果 getenforce 是 Enforcing,且 error log 里出现 Permission denied 或者直接 502/403,执行:

# 允许 nginx 主动发起出站连接(反代到 2368 需要)
sudo setsebool -P httpd_can_network_connect 1

# 确认
getsebool httpd_can_network_connect
sudo systemctl restart nginx

这是 RHEL 系反代最经典的坑:nginx 能监听 80/443,但不被允许连出去,现象是 502 而 B 那边完全正常。

4.3 排查对照表

现象 大概率原因 定位命令
502 Bad Gateway Ghost 没起 / 只听 127.0.0.1 / ufw 挡 / SELinux curl http://BWIP:2368、ghost log(B 上)、getenforce
ERR_TOO_MANY_REDIRECTS 443 块缺 X-Forwarded-Proto https,或 B 的 url 写成 http nginx -T 对照
403 Forbidden SELinux 未放行出站 sudo tail /var/log/audit/audit.log
certbot 卡住/超时 安全组没开 80、DNS 未生效、国内未备案 dig +short www.test.com、curl -I http://www.test.com
上传主题 413 client_max_body_size 太小 改大后 reload
后台操作卡、事件不刷新 proxy_buffering 没关 看配置
504 Gateway Timeout 超时太短 proxy_read_timeout

阶段 5:可选加固

按需加,不是必须:

# 放在 443 的 server 块里
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
server_tokens off;   # 放 http 块,隐藏版本号

如果 A 前面还套了 Cloudflare 之类的 CDN,要加 real_ip 模块并 set_real_ip_from 对应回源段、real_ip_header CF-Connecting-IP,否则 Ghost 看到的全是 CDN 的 IP,登录限速会误伤。

不建议现在就去限制 /ghost 的访问来源——家庭宽带 IP 会变,容易把自己锁在外面。真要做,等站点稳定后用"仅允许特定 IP + 保留一个后门路径"的方式。


最小执行顺序(TL;DR)

  1. 阶段 0 五条检查全绿(尤其 curl http://BWIP:2368 通)
  2. 装 nginx + 安全组开 80/443
  3. 写 00-upgrade-map.conf 和 www.test.com.conf(80 反代)→ nginx -t → reload
  4. curl -H 'Host: www.test.com' http://127.0.0.1 拿到 200
  5. certbot --nginx -d www.test.com --redirect
  6. 核对 443 块四个 proxy 头 → reload
  7. certbot renew --dry-run + 浏览器 /ghost
蜀ICP备2026026554号-1 · 川公网安备51010802033276号 公安备案 川公网安备51010802033276号