王博士 Dr. WANG

NATFRP 域名穿透 + SSL 部署 SOP 模板(v1.0)

域名切换部署 SOP:<你的域名>(NATFRP <节点所在城市> + Let’s Encrypt 证书)

版本:v1.0(2026-09-03 实测完成)
目的:把一套自建系统的公网入口从 <旧域名>(Cloudflare 隧道)切换到 <你的域名>(NATFRP <节点所在城市>线路 + HTTPS + 受信任证书)。
本文档基于本次实际执行与验证完整记录,可作为同类 <你的主域名> 子域名复用的步骤复用文档。


0. 结果摘要

对外域名 https://<你的域名>
穿透服务 NATFRP(SakuraFrp)
节点 <节点名称> = (域名 <natfrp节点域名><节点IP>
隧道 myapp,id <隧道ID>,类型 https,<你的域名>127.0.0.1:8081
证书 Let’s Encrypt(YE2,ECDSA ec-256),有效期 2026-09-03 ~ 2026-12-02,浏览器信任
证书续期 acme.sh dns_ali 全自动(阿里云自动增删 TXT)

最终验证:curl https://<你的域名>/ 返回 HTTP 200,前端资源 / 登录 API 均正常。


1. 架构与原理(务必先读)

1.1 TLS 在哪里终结:本地 frpc

NATFRP 的「自动 HTTPS」由本地 frpc 进程实现,TLS 在你的服务器上终结,证书不会上传到 NATFRP 服务器:

用户浏览器
   │  TCP 穿透(密文,边缘节点不碰 TLS)
   ▼
NATFRP 边缘节点(<你的域名> → CNAME <natfrp节点域名>)
   │  内网隧道(TLS 密文)
   ▼
本地 frpc(192.168.1.120)
   │  从工作目录加载证书:/etc/natfrp/FrpcWorkingDirectory/<域名>.crt|.key
   │  解密后纯 HTTP/HTTPS 反代回源
   ▼
本地服务(自建前端 nginx,127.0.0.1:8081)

1.2 证书加载规则(关键)

隧道开启 auto_https 后,frpc 加载证书的优先级:

auto_https 行为
留空(默认) 禁用
auto 节点域名生成自签证书;若配置了子域绑定(自定义域名),则从服务端自动加载该域名证书
逗号分隔的域名列表 加载工作目录 <域名>.crt / <域名>.key;不存在则生成自签兜底

⚠️ 若证书文件缺失/格式错,frpc 会静默生成自签证书CN=SakuraFrp Automatic TLS),浏览器报”不受信任”。看到自签证书 = 正式证书没加载上

本次隧道配置用了 auto_https = auto + force_https = 301:自动 HTTPS 开启、http 自动跳转 https。

1.3 为什么用 acme.sh 的 dns_ali 插件签发证书

  • HTTP-01 验证在穿透场景不可靠(80 端口/路径不可控),DNS-01 更稳
  • <你的主域名> 的 DNS 在阿里云,dns_ali 插件用阿里云 AccessKey 自动增删 _acme-challenge TXT 记录,全程无需人工改 DNS,且续期时也无需人工介入。
  • 相比 certbot manual DNS-01(每次续期 TXT 值会变、需人工改 DNS),acme.sh dns_ali 是全链路无人值守方案。

2. 前置条件 Checklist

  • [ ] NATFRP(SakuraFrp)账号,拿到访问密钥(Access Key,形如 sg2m...
  • [ ] 目标域名(<你的域名>)所在的阿里云账号 AccessKeyId / AccessKeySecret(有 Alidns 操作权限)
  • [ ] 内网服务器(本次 192.168.1.120)可 SSH,有 sudo,网络可出公网
  • [ ] 本地服务已在某端口监听(本次自建前端 nginx 在 127.0.0.1:8081
  • [ ] 服务器装有 acme.sh、openssl、dig、python3(带 aliyun-python-sdk-alidns,本机已装)

3. 步骤总览

Step 1  创建 NATFRP 隧道(<节点所在城市> <nodeID>,HTTPS,<你的域名> → 127.0.0.1:8081)
Step 2  阿里云 DNS 添加 CNAME:<你的域名> → <natfrp节点域名>
Step 3  acme.sh dns_ali 签发 Let's Encrypt 证书
Step 4  部署证书到 frpc 工作目录 /etc/natfrp/FrpcWorkingDirectory
Step 5  新隧道加入 daemon 自动启动并重启 natfrp;配置自动续期闭环
Step 6  端到端验证

4. Step 1:创建 NATFRP 隧道

4.1 方案选择:用官方 API(推荐,可脚本化)

NATFRP 有官方 API,https://api.natfrp.com/docs/(OpenAPI,swagger)。认证方式:

Authorization: Bearer <访问密钥>
基础 URL:https://api.natfrp.com/v4

先确认访问密钥有效,并列出当前隧道:

AK="<你的NATFRP访问密钥>"
curl -s --max-time 15 "https://api.natfrp.com/v4/tunnels" \
  -H "Authorization: Bearer $AK" | python3 -m json.tool

获取节点列表,找到<节点所在城市>节点(nodes 是 dict,key 为节点 id):

curl -s --max-time 20 "https://api.natfrp.com/v4/nodes" \
  -H "Authorization: Bearer $AK" -o /tmp/nodes.json
python3 -c "
import json
d=json.load(open('/tmp/nodes.json'))
for nid,n in d.items():
    if '<节点所在城市>' in n.get('name',''): print(nid, n.get('name'), n.get('host'))
"

本次结果:<节点名称> = (host <natfrp节点域名>),与既有 <某服务子域名> / <某服务子域名> 隧道同节点,支持 HTTPS 建站。

4.2 创建 HTTPS 隧道

POST /v4/tunnels,请求体字段见 OpenAPI(name/type/node 必填;remote 为绑定域名;local_ip/local_port 为回源地址;extra 为自定义配置):

AK="<你的NATFRP访问密钥>"
curl -s --max-time 20 -X POST "https://api.natfrp.com/v4/tunnels" \
  -H "Authorization: Bearer $AK" -H "Content-Type: application/json" \
  -d '{
    "name":"myapp",
    "type":"https",
    "node":<nodeID>,
    "remote":"<你的域名>",
    "local_ip":"127.0.0.1",
    "local_port":8081,
    "note":"内容系统(<节点所在城市>)",
    "extra":"auto_https = auto\nforce_https = 301\n"
  }'

响应(HTTP 201):

{"id": <隧道ID>, "name": "myapp", "remote": "<你的域名>"}

说明:类型 https 隧道必须与协议一致;也可建 http 隧道(80)。HTTPS 隧道配 force_https=301 后,用户访问 http 会自动跳 https,无需再建 http 隧道。

内地节点备案提醒:用内地(<节点所在城市>等)节点建 HTTP(S) 隧道时,域名需有 ICP 备案。本系统之所有可复用此流程,正是因 <你的主域名> 已备案。

4.3 面板替代方案(不用 API 时)

也可在 NATFRP 面板 手动创建:隧道类型选「HTTPS」,节点选<节点所在城市>,绑定域名填 <你的域名>,本地端口填 8081,开启「自动 HTTPS」。参数与 API 等价。


5. Step 2:配置阿里云 DNS

5.1 方式 A:阿里云 API(SDK,推荐)

本机已装 aliyun-python-sdk-alidns。先列出当前记录确认账号可访问:

python3 - <<'EOF'
from aliyunsdkcore.client import AcsClient
from aliyunsdkalidns.request.v20150109.DescribeDomainRecordsRequest import DescribeDomainRecordsRequest
client = AcsClient('<AccessKeyId>', '<AccessKeySecret>', 'cn-hangzhou')
req = DescribeDomainRecordsRequest(); req.set_DomainName('<你的主域名>')
import json
d = json.loads(client.do_action_with_exception(req))
for r in d['DomainRecords']['Record']:
    print(r.get('RR'), r.get('Type'), r.get('Value'), 'recordId='+r.get('RecordId'))
EOF

添加 CNAME 记录(content<natfrp节点域名>,TTL 600):

python3 - <<'EOF'
from aliyunsdkcore.client import AcsClient
from aliyunsdkalidns.request.v20150109.AddDomainRecordRequest import AddDomainRecordRequest
client = AcsClient('<AccessKeyId>', '<AccessKeySecret>', 'cn-hangzhou')
req = AddDomainRecordRequest()
req.set_DomainName('<你的主域名>')
req.set_RR('content'); req.set_Type('CNAME'); req.set_Value('<natfrp节点域名>'); req.set_TTL(600)
import json; print(json.loads(client.do_action_with_exception(req)))
EOF

验证生效:

dig +short <你的域名>
# 期望输出:
# <natfrp节点域名>.
# <节点IP>

5.2 方式 B:阿里云控制台

DNS 控制台 → 解析设置 → 添加记录:记录类型 CNAME、主机记录 content、记录值 <natfrp节点域名>、TTL 600。

<你的主域名> 的 NS 是阿里云(dns31/32.hichina.com)。CNAME 目标指向节点域名 <natfrp节点域名>(不要用 IP,便于自动 HTTPS 识别与节点迁移)。


6. Step 3:签发 Let’s Encrypt 证书(acme.sh dns_ali 全自动)

6.1 确认 acme.sh 及 dns_ali 插件存在

ls ~/.acme.sh/acme.sh ~/.acme.sh/dnsapi/dns_ali.sh
~/.acme.sh/acme.sh --version   # 本机 v3.1.5

6.2 签发(环境变量一次性传入阿里云凭证;签发后会写进配置)

export HOME=/home/ubuntu
cd ~/.acme.sh
export Ali_Key="<AccessKeyId>"
export Ali_Secret="<AccessKeySecret>"
./acme.sh --issue --dns dns_ali -d <你的域名> \
  --keylength ec-256 --server letsencrypt --force

dns_ali 会自动调用阿里云 API 添加 _acme-challenge.<你的域名> TXT 记录并在校验证后删除,全程无需人工

成功后在:

~/.acme.sh/<你的域名>_ecc/
├── <你的域名>.cer      # cert
├── <你的域名>.key      # private key
├── ca.cer                     # intermediate
└── fullchain.cer              # 完整链(部署用这个)

验证证书信息:

openssl x509 -in ~/.acme.sh/<你的域名>_ecc/fullchain.cer -noout -issuer -subject -dates
# issuer=C = US, O = Let's Encrypt, CN = YE2

7. Step 4:部署证书到 frpc 工作目录

必须用 fullchain.cer(含中间链)作为 .crt,不能用 cert.cer,否则浏览器报证书链不完整。

DEST="/etc/natfrp/FrpcWorkingDirectory"
SRC="/home/ubuntu/.acme.sh/<你的域名>_ecc"

sudo cp "$SRC/fullchain.cer" "$DEST/<你的域名>.crt"
sudo cp "$SRC/<你的域名>.key" "$DEST/<你的域名>.key"
sudo chown natfrp:natfrp "$DEST/<你的域名>.crt" "$DEST/<你的域名>.key"
sudo chmod 644 "$DEST/<你的域名>.crt"
sudo chmod 600 "$DEST/<你的域名>.key"

校验公私钥匹配(注意用 sudo 读 600 权限的 key,普通用户读会报 Permission denied,属正常):

CERTPUB=$(sudo openssl x509 -in "$DEST/<你的域名>.crt" -noout -pubkey | openssl md5)
KEYPUB=$(sudo bash -c "openssl ec -in '$DEST/<你的域名>.key' -pubout 2>/dev/null" | openssl md5)
[ "$CERTPUB" = "$KEYPUB" ] && echo "✓ 公私钥匹配" || echo "✗ 不匹配"

8. Step 5:新隧道加入 daemon 自动启动 + 配置自动续期

8.1 让 natfrp daemon 自动启动新隧道

natfrp-service/etc/natfrp/config.json)里的 auto_start_tunnels 控制哪些隧道随服务启动。把新隧道 id 加进去并重启:

sudo cp /etc/natfrp/config.json /etc/natfrp/config.json.bak.content
sudo python3 - <<'EOF'
import json
p='/etc/natfrp/config.json'; d=json.load(open(p))
ast=list(d.get('auto_start_tunnels',[]))
if <隧道ID> not in ast: ast.append(<隧道ID>); d['auto_start_tunnels']=ast
json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)
print(d['auto_start_tunnels'])
EOF
sudo python3 -c "import json; json.load(open('/etc/natfrp/config.json')); print('JSON OK')"
sudo systemctl restart natfrp.service

重启后查看日志确认新隧道加载了正式证书(而非自签):

sudo grep -i content /etc/natfrp/Logs/natfrp-service.<日期>.log
# 期望看到:
# 已为 <你的域名> 加载证书 [CN = <你的域名>, 2026-09-03 - 2026-12-02]
# HTTPS 隧道启动成功, 绑定到域名 [<你的域名>]
# 隧道启动成功

同时会看到类似 dx.<natfrp节点域名> 的自签提示,那个是节点域名的自签兜底,不影响自定义域名 <你的域名> 使用正式证书。

8.2 配置证书自动续期 + 自动部署

acme.sh 已通过 dns_ali 持久化阿里云凭证(~/.acme.sh/account.confSAVED_Ali_Key/SAVED_Ali_Secret),续期时会自动加载重签,无需再传。还需配置「续期后自动复制证书到 frpc 工作目录并重启 natfrp」。

创建部署脚本(用 sudo 写 natfrp 目录,ubuntu 免密 sudo):

sudo tee /usr/local/bin/natfrp-content-deploy.sh >/dev/null <<'EOF'
#!/bin/bash
DEST="/etc/natfrp/FrpcWorkingDirectory"
CRT_DIR="$HOME/.acme.sh/<你的域名>_ecc"
if [ -f "$CRT_DIR/fullchain.cer" ] && [ -f "$CRT_DIR/<你的域名>.key" ]; then
  sudo cp -f "$CRT_DIR/fullchain.cer" "$DEST/<你的域名>.crt"
  sudo cp -f "$CRT_DIR/<你的域名>.key" "$DEST/<你的域名>.key"
  sudo chown natfrp:natfrp "$DEST/<你的域名>.crt" "$DEST/<你的域名>.key"
  sudo chmod 644 "$DEST/<你的域名>.crt"
  sudo chmod 600 "$DEST/<你的域名>.key"
  sudo systemctl restart natfrp.service
  echo "[deploy] <你的域名> cert updated & natfrp restarted"
fi
EOF
sudo chmod +x /usr/local/bin/natfrp-content-deploy.sh

把脚本注册为域名的续期 hook

CONF=~/.acme.sh/<你的域名>_ecc/<你的域名>.conf
python3 - "$CONF" <<'EOF'
import sys, re
p=sys.argv[1]; s=open(p).read()
hook="/usr/local/bin/natfrp-content-deploy.sh"
s = re.sub(r"Le_RenewHook=.*", f"Le_RenewHook='{hook}'", s) if 'Le_RenewHook=' in s \
    else s.rstrip('\n') + f"\nLe_RenewHook='{hook}'\n"
open(p,'w').write(s)
EOF
grep Le_RenewHook "$CONF"

确认 cron 已存在(acme.sh 首次运行会装):

crontab -l | grep acme
# 58 5,11,17,23 * * * "/home/ubuntu/.acme.sh"/acme.sh --cron --home "/home/ubuntu/.acme.sh" > /dev/null

证书有效期 90 天;acme.sh 按 Let’s Encrypt ARI 建议时间自动续期(本次 Next renewal = 2026-11-03)。cron 每天跑 4 次检查。


9. Step 6:端到端验证清单

# 1) 本机回源服务正常
curl -s -o /dev/null -w "local 8081 -> %{http_code}\n" http://127.0.0.1:8081/

# 2) 公网 HTTPS + 校验证书(不用 -k,必须能过证书校验)
curl -s -o /dev/null -w "https://<你的域名> -> %{http_code}\n" --max-time 12 https://<你的域名>/

# 3) 证书信息
echo | openssl s_client -connect <你的域名>:443 -servername <你的域名> 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

# 4) 前端资源 + 登录 API(验证是真实应用而非默认页)
curl -s https://<你的域名>/ | grep -o '<title>[^<]*</title>'
curl -s -o /dev/null -w "JS asset -> %{http_code}\n" https://<你的域名>/assets/index-*.js
curl -s -o /dev/null -w "login API (bad creds) -> %{http_code}\n" -X POST https://<你的域名>/api/auth/login -H 'Content-Type: application/json' -d '{"username":"x","password":"y"}'

# 5) 隧道在线状态
curl -s "https://api.natfrp.com/v4/tunnels" -H "Authorization: Bearer $AK" \
  | python3 -c "import sys,json;[print(f\"{t['name']} online={t['online']}\") for t in json.load(sys.stdin) if t['id']==<隧道ID>]"

# 6) natfrp 服务存活 + 证书续期闭环
systemctl is-active natfrp.service
grep Le_RenewHook ~/.acme.sh/<你的域名>_ecc/<你的域名>.conf

本次全部通过:

检查 结果
本机 8081 200
https://<你的域名> 200
证书 issuer=YE2,CN=<你的域名>,2026-12-02 前有效
页面标题 「<系统名称>」
前端 JS / 登录 API 200 / 401
隧道 API myapp online=True
natfrp active

10. 故障排查

现象 排查
证书显示 CN=SakuraFrp Automatic TLS(自签) 正式证书没加载上:① 检查 /etc/natfrp/FrpcWorkingDirectory/<你的域名>.{crt,key} 存在且格式正确(PEM)② 替换后是否 systemctl restart natfrp.service ③ 看日志是否有自签提示
浏览器报证书链不完整 .crt 必须用 fullchain.cer(含中间链),不能用 cert.cer
域名访问不了 / SakuraFrp 错误页 ① DNS 是否生效:dig <你的域名> +short ② 隧道是否启动、节点是否在线 ③ 内地节点是否备案
502 / 504 / 连接拒绝 frpc 到本地回源不通:ss -tlnp | grep 8081ps aux | grep frpc;回源服务是否在监听
openssl 读 key 报 Permission denied 正常,key 是 600 权限且属 natfrp;用 sudo 读
natfrp 面板建隧道提示需备案 内地(<节点所在城市>)节点的 HTTP(S) 隧道必须 ICP 备案域名;或改用 TCP 隧道(非内地节点)
续期后又是自签 Le_RenewHook 未注册或脚本无执行权限;检查 cron 是否在跑

11. 续期说明(重要)

环节 自动化程度
certbot/acme.sh 触发续期(到期前约 30 天,ARI) ✅ 自动(cron 每天 4 次检查)
DNS-01 验证(阿里云增删 TXT) ✅ 自动(acme.sh dns_ali,凭证在 account.conf,TXT 每次不同但自动处理)
部署证书到 frpc 工作目录 + 重启 natfrp ✅ 自动(Le_RenewHook/usr/local/bin/natfrp-content-deploy.sh

全链路无人值守,但建议在 2026-11-01 前后抽查一次续期是否成功:

~/.acme.sh/acme.sh --list
# 期望:Renew 时间往前推进,证书 notAfter 更新为新的 +90 天

如需手动续期:

export HOME=/home/ubuntu
~/.acme.sh/acme.sh --renew -d <你的域名> --ecc

12. 变更记录

日期 内容
2026-09-03 v1.0:完成 <你的域名> 全流程部署——NATFRP <节点所在城市> HTTPS 隧道创建(id <隧道ID>,→127.0.0.1:8081)、阿里云 CNAME 解析、acme.sh dns_ali 签发 ECDSA LE 证书并部署到 frpc 工作目录、配置自动续期 hook;端到端验证通过(HTTP 200 + 证书信任)。

13. 安全提示

  • 文中的 NATFRP 访问密钥、阿里云 AccessKey 均为敏感凭证,请勿提交到代码仓库/公开文档;生产环境建议用变量注入而非硬编码。
  • 阿里云 AccessKey 建议按需轮换,并通过 RAM 限制到 Alidns 最小权限范围。