先回顾一下现在的局面:
用户访问: http://xx.xx.xx.xx:4097
浏览器: ⚠️ 不安全(大红叉)
你心里: 淦,这看起来像个盗版网站
没域名、没 HTTPS、地址带端口号——三样东西凑一起,自己用还好,若是以后要做其他功能出来见人,页面再好看也没人敢用。
这次的目标:
https://xxx.bnvstudio.com直接访问,不带端口号- 浏览器地址栏给我一把 绿色的小锁
- 不能随便什么子域名都能访问到我的服务
一、先搞清楚流量长什么样
有了 frp 隧道之后,整条链路是这样的:
用户 → https://xxx.bnvstudio.com
↓ DNS 解析到 ECS 公网 IP
↓ ECS:443(nginx 监听 HTTPS)
↓ nginx 反代到 127.0.0.1:4097(frps 在监听的端口)
↓ frp 隧道
↓ Mac Mini:4096(opencode 服务)
要搞定这件事,三步走:
DNS 解析 → SSL 证书 → nginx 反代
一个不能少。
二、DNS 解析——等了五分钟以为自己配错了
第一步,去你的域名注册商(阿里云/腾讯云/DNSpod 之类的),加一条 A 记录:
| 记录类型 | 主机记录 | 记录值 |
|---|---|---|
| A | opencode | 你的 ECS 公网 IP |
然后等。
$ dig xxx.bnvstudio.com
; <<>> DiG 9.x <<>>
;; 过了五秒...
;; 过了十秒...
;; 怎么还没生效???
每次配 DNS 都是这个心理活动:"是不是我配错了?"
实际上没配错,TTL 还没过期而已。去泡杯茶回来看就好了。
确认生效:
dig xxx.bnvstudio.com +short
# 输出: 你的ECS公网IP ← 可以了
三、SSL 证书——为什么选了 acme.sh
现在 xxx.bnvstudio.com 通过nginx反代能解析到 ECS ,但访问还是 HTTP。需要一张 SSL 证书。
3.1 选项对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 阿里云免费证书 | 点几下就拿到 | 一年一换,不支持泛域名,要手动部署 |
| 商业证书 | 贵,看起来专业 | 贵 |
| acme.sh + Let's Encrypt | 免费,自动续期,一行命令 | 需要稍微动动手 |
我选 acme.sh。理由很简单:这个工具是用 shell 写的,连 npm install 都不用。
3.2 DNS-01 vs HTTP-01
Let's Encrypt 验证域名所有权有两种方式:
- HTTP-01:在 80 端口放一个文件,CA 来访问验证
- 问题:我的 80 端口本来就没开,还得专门去配
- DNS-01:在 DNS 记录里加一个 TXT 记录,CA 去查
- 好处:不需要开任何端口,配一次以后全部自动
我选 DNS-01。不动现有服务,是运维的第一原则。
3.3 阿里云 RAM 子用户
DNS-01 需要你有一个 DNS API 的 AccessKey,让 acme.sh 可以自动创建和删除 TXT 记录。
去阿里云 RAM 访问控制创建一个子用户:
https://ram.console.aliyun.com/users
授权策略选择 AliyunDNSFullAccess,不要多给。
然后拿到:
AccessKey ID: LTAI5t...
AccessKey Secret: xxxxxxxxxxxxxxxxxxxxxxxxxx
心理活动:"我只是想申请个证书,为什么要经过『访问控制』?"
照做就是了。
3.4 安装 acme.sh
curl https://get.acme.sh | sh -s email="admin@你的域名.com"
装完之后,acme.sh 会自动做两件事:
- 把
~/.acme.sh/acme.sh加到 PATH - 装一个 cron job 每天检查证书是否需要续期
没有任何依赖,没有 node_modules,一个 shell 脚本全搞定。
3.5 申请证书
export Ali_Key="你的AccessKey ID"
export Ali_Secret="你的AccessKey Secret"
~/.acme.sh/acme.sh --issue \
--dns dns_ali \
-d bnvstudio.com \
-d "*.bnvstudio.com"
解释一下:
| 参数 | 含义 |
|---|---|
--dns dns_ali | 用阿里云 DNS 的 API 做验证 |
-d bnvstudio.com | 主域名也要申请,不然根域名访问会警告 |
-d "*.bnvstudio.com" | 泛域名,所有子域名都能用这张证书 |
acme.sh 背后的流程:
- 调用阿里云 API 创建
_acme-challenge.bnvstudio.com的 TXT 记录 - 等 DNS 生效(默认它会等 120 秒)
- Let's Encrypt 来验证 TXT 记录
- 验证通过后自动删除 TXT 记录
- 证书生成完毕
全程自动化,你不需要手动去 DNS 控制台点任何按钮。
3.6 安装证书到 nginx
~/.acme.sh/acme.sh --install-cert \
-d bnvstudio.com \
--key-file /etc/nginx/ssl/bnvstudio.com/key.pem \
--fullchain-file /etc/nginx/ssl/bnvstudio.com/fullchain.pem \
--reloadcmd "systemctl reload nginx"
证书文件放到了:
/etc/nginx/ssl/bnvstudio.com/
├── fullchain.pem # 证书链
└── key.pem # 私钥
私钥一定要保护好,泄露了别人可以冒充你的域名。
四、nginx 配置——你以为装完证书就完了?
4.1 写 server block
server {
listen 443 ssl http2;
server_name xxx.bnvstudio.com;
ssl_certificate /etc/nginx/ssl/bnvstudio.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/bnvstudio.com/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://127.0.0.1:4097;
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 $scheme;
}
}
然后:
nginx -t
# syntax is ok ← 基本不会出问题
systemctl reload nginx
# 走你
去浏览器访问 https://xxx.bnvstudio.com...
超时。
4.2 第一层防火墙:firewalld
ss -tlnp | grep 443
# 看到 nginx 在监听,没问题
# 从另一台机器测
echo > /dev/tcp/ECS公网IP/443 && echo "通" || echo "不通"
# 不通
ECS 上的 firewalld 默认没放行 443:
firewall-cmd --add-port=443/tcp --permanent
firewall-cmd --reload
再测:
echo > /dev/tcp/ECS公网IP/443 && echo "通" || echo "不通"
# 通 ✅
4.3 第二层防火墙:阿里云安全组
火急火燎去浏览器访问...
还是超时。
对哦,阿里云安全组还没放行。
去阿里云控制台 → 安全组 → 入方向规则 → 添加:
| 方向 | 协议 | 端口 | 授权对象 | 说明 |
|---|---|---|---|---|
| 入方向 | TCP | 443 | 0.0.0.0/0 | HTTPS |
保存之后再访问:
HTTPS 能打开了!小绿锁! ✅
经验教训:ECS 有两层防火墙——firewalld 和安全组。你永远会忘记其中一层。
五、SELinux 补刀
HTTPS 能访问了,点一下页面...
502 Bad Gateway
查看 nginx 错误日志:
tail -f /var/log/nginx/error.log
# 2025/xx/xx ... permission denied while connecting to upstream
SELinux 拦了 nginx 的出站连接。nginx 在 127.0.0.1:4097 反代到 frps,但 SELinux 说:"nginx 只能提供静态文件,不允许连后端服务。"
解决方案:
setsebool -P httpd_can_network_connect 1
解释:
setsebool设置 SELinux 的布尔值httpd_can_network_connect允许 nginx 发起网络出站连接-P永久生效,重启不丢失
SELinux 每次都会在你觉得"终于搞定了"的时候出现,补上一刀。
感想:你每次都想过把它关了,但每次都忍住了。不是因为你会配 SELinux,而是因为关了之后万一出安全问题你不好交代。
六、Basic Auth——给服务加个门卫
泛域名证书办下来的好处是 ``*.bnvstudio.com` 都能用 HTTPS 访问了。
但坏处也是:``*.bnvstudio.com` 都能用 HTTPS 访问了。
这意味着如果有人扫到你的子域名,可以直接访问你的服务。虽然不是什么重要数据,但总归不舒服。
加一层 HTTP Basic Auth:
yum install -y httpd-tools # 如果还没装
mkdir -p /etc/nginx/auth
htpasswd -bcs /etc/nginx/auth/.htpasswd admin $(openssl rand -base64 12)
在 nginx server block 里加上:
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/auth/.htpasswd;
nginx -t && systemctl reload nginx 之后,再访问就会弹一个登录框:
⚠️ 此网站需要登录
用户名: admin
密码: (随机生成的那一串)
至少挡住扫端口的脚本小子。真·安全性不要靠 Basic Auth,它只是不想让路人随便进。
七、默认 server 陷阱——"怎么哪个子域名都能访问?"
配完之后心血来潮试了一下:
https://aabbcc.bnvstudio.com
竟然也能访问,而且内容和 xxx.bnvstudio.com 一模一样。
nginx 的工作原理是这样的:当请求的 server_name 没匹配到任何配置块时,它会把请求交给监听同一个端口的第一个 server block(或者说默认 server)来处理。
当前配置里只有一个 server block 监听 443,它就成了默认 server。所以不管什么子域名,只要 DNS 能解析到 ECS IP,都会落到同一个后端。
修复:加一个拒绝所有未知域名的默认 server。
把之前的 server block 改成:
# 默认 server:拒绝一切未匹配的域名
server {
listen 443 ssl http2 default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/bnvstudio.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/bnvstudio.com/key.pem;
return 444; # 直接断开连接,不给任何响应
}
# 真正的服务
server {
listen 443 ssl http2; # 注意:没有 default_server 了
server_name xxx.bnvstudio.com;
ssl_certificate /etc/nginx/ssl/bnvstudio.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/bnvstudio.com/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/auth/.htpasswd;
location / {
proxy_pass http://127.0.0.1:4097;
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 $scheme;
}
}
return 444 是 nginx 专属的状态码,直接关闭连接,不给任何响应数据。比起 return 403 或 return 404,444 更狠——连"这个服务器存在"的消息都不给你。
nginx -t && systemctl reload nginx
再试:
curl -I https://xxx.bnvstudio.com
# HTTP/2 401 ← 正常,Basic Auth 弹了登录框
curl -I https://aabbcc.bnvstudio.com
# curl: (52) Empty reply from server ← 444 断开连接
舒服了。
八、最终链路
从浏览器输入 URL 到页面渲染,整条路长这样:
┌─ 用户 ──────────────────────────────────┐
│ https://xxx.bnvstudio.com │
└────────────────┬─────────────────────────┘
▼ DNS 解析
xxx.bnvstudio.com → ECS 公网 IP
▼
┌─ ECS ───────────────────────────────────┐
│ ① nginx:443 │
│ SSL 证书验证 ✅ │
│ server_name 匹配 ✅ │
│ Basic Auth 验证 ✅ │
│ → 反代到 127.0.0.1:4097 │
│ ② frps:4097 │
│ → 通过 frp 隧道转发 │
└────────────────┬─────────────────────────┘
▼ frp 隧道
┌─ Mac Mini 2012 ─────────────────────────┐
│ frpc → 本机:4096 │
│ opencode 服务响应请求 │
└──────────────────────────────────────────┘
三层安全防护:
| 层 | 防护 | 作用 |
|---|---|---|
| 1 | SSL/TLS | 传输加密,小绿锁 |
| 2 | Basic Auth | 挡住路人访问 |
| 3 | 默认 server 444 | 未知域名直接断开 |
九、改完收工
# 防火墙
firewall-cmd --add-port=443/tcp --permanent
firewall-cmd --reload
# SELinux 放行 nginx 出站
setsebool -P httpd_can_network_connect 1
# nginx 验证
nginx -t && systemctl reload nginx
# 验证访问
curl -I https://xxx.bnvstudio.com
访问 https://xxx.bnvstudio.com,浏览器地址栏出现小绿锁的那一刻,前面所有踩的坑都值了。
续期的事不用管——acme.sh 早就帮你装好了 cron,每天检查一次,到期前自动续。从今天起往后一年,证书的事情都不是你的事了。
一年后才是。
附录:涉及的文件和路径
ECS 侧:
/etc/nginx/ssl/bnvstudio.com/fullchain.pem # 证书链
/etc/nginx/ssl/bnvstudio.com/key.pem # 私钥
/etc/nginx/auth/.htpasswd # Basic Auth 密码文件
/etc/nginx/conf.d/(你的server block配置文件)
自动续期:
~/.acme.sh/acme.sh --cron # 每天检查,acme.sh 自动配置
日志:
/var/log/nginx/access.log
/var/log/nginx/error.log