在数字化时代,拥有一个属于自己的家庭网络存储(NAS)和私有云服务变得越来越流行。飞牛OS作为一款专注于NAS管理的操作系统,结合Docker容器技术,为我们搭建各类服务提供了坚实的基础。然而,当我们希望从外部安全地访问这些服务时,HTTPS加密就成了必不可少的一环。传统上,为多个服务配置和维护HTTPS证书往往是件头疼的事,特别是当你的网络运营商限制了80/443端口时。
本文将深入探讨如何利用 飞牛OS 的强大Docker能力、Traefik 这款现代化反向代理,并结合 ohttps.com 的自动化证书服务,实现 *.your-doamin.com 泛域名证书的全自动HTTPS访问,同时巧妙解决内网访问的痛点。
1. 认识你的工具:飞牛OS与Traefik
在深入技术细节之前,我们先来了解一下本文的主角们:
-
飞牛OS (FN OS):一款专为NAS设计、基于Linux内核的操作系统。它提供友好的管理界面,并支持Docker容器技术,让用户能轻松部署和管理各种应用服务,如影音媒体中心、个人云盘、智能家居系统等。它的核心在于将复杂的Linux操作容器化、图形化,让普通用户也能享受到私有服务器的乐趣。同时基于X86架构的支持使得它的安装潜力相当的大,不论是一两百的小主机还是你大学时代闲置的笔记本电脑。都可以轻松安装。分分钟变身专业级NAS系统。
-
Traefik (音同 "Traffic"):一个现代化的 HTTP 反向代理和负载均衡器。Traefik 的强大之处在于它能与 Docker 等容器编排工具无缝集成,动态发现你部署的服务,并根据容器的标签(Labels)自动配置路由规则。最吸引人的是,它内置了对 Let's Encrypt 等证书机构的 ACME 协议支持,可以自动申请和续期 HTTPS 证书。对于本文场景,即使80/443端口受限(国内运行商对于个人宽带默认屏蔽),我们依然能巧妙地利用它的特性。
FN OS硬件要求虽然不高。但如果你对数据安全、完整性、可靠性要求较高。还是建议使用 企业级硬盘和专业电源以及配合UPS使用 并且请至少配置 RAID 1模式
2. 为什么需要HTTPS?以及传统痛点
当你搭建了 navidrome.your-doamin.com 或 php.your-doamin.com 这样的私有服务,并希望通过域名从家中或外部访问时,HTTPS 加密是保护数据安全的基石。它能防止数据在传输过程中被窃听或篡改。
然而,传统的 HTTPS 配置存在诸多痛点:
- 证书申请与续期繁琐:手动申请和更新 Let's Encrypt 证书,每90天重复一次,容易遗忘。
- 多服务证书管理复杂:每个子域名可能都需要单独的证书,管理成本高。泛域名证书可以解决这个问题,但申请需要特殊的 DNS 验证。
- 80/443 端口限制:部分家庭宽带运营商会封禁这两个端口,导致无法直接通过 HTTP-01 或 TLS-ALPN-01 挑战来验证域名所有权,也就无法申请到证书。
- 内网访问回环问题:即使外网能正常访问 HTTPS 服务,内网设备通过公网域名访问时,往往会遇到“NAT 回流”(NAT Hairpinning)问题,导致无法访问或访问异常。
我们的方案将完美解决以上所有痛点。
先来看下我的网络链路图: 这份图展示了你的服务请求如何从用户的设备出发,经过DNS解析、公网、路由器、NAS,最终到达Docker容器中的服务。特别强调了内网访问的优化路径。
graph TD
subgraph "外部网络访问 (External Access)"
A["用户设备 (外网)"] -->|"1. DNS查询: navidrome.your-domain"| B("公共DNS服务器")
B -->|"2. 返回公网IP"| C("你的公网IP")
C -->|"3. HTTPS请求: 公网IP:4443"| D("光猫 - 桥接模式")
D -->|"4. 转发流量"| E("openWrt软路由")
E -->|"5. 端口转发 (4443 -> NAS内网IP:4443)"| F("NAS服务器 - 飞牛OS")
F -->|"6. Traefik 接收 (宿主机4443)"| G("Traefik 容器")
G -->|"7. 代理到 Docker 服务 (内网)"| H("Docker 服务容器, 如 Navidrome")
end
subgraph "内部网络访问 (Internal Access)"
I["用户设备 (内网)"] -->|"1. DNS查询: navidrome.your-domain"| J("openWrt软路由 - 内网DNS/DNSMasq")
J -->|"2. DNS重定向: 解析到 Traefik 容器内网IP"| K("Traefik 容器内网IP, 如 172.18.0.2")
K -->|"3. HTTPS请求 (内网直接访问)"| G
end
G --> H
H -->|"响应"| G
G -->|"响应"| F
F -->|"响应"| E
E -->|"响应"| D
D -->|"响应"| C
C -->|"响应"| B
B -->|"响应"| A
G -->|"响应"| K
K -->|"响应"| J
J -->|"响应"| I
style A fill:#f9f,stroke:#333,stroke-width:2px
style I fill:#f9f,stroke:#333,stroke-width:2px
style G fill:#bbf,stroke:#333,stroke-width:2px
style H fill:#bfb,stroke:#333,stroke-width:2px
style E fill:#fcf,stroke:#333,stroke-width:2px
style F fill:#ddf,stroke:#333,stroke-width:2px
3. 自动化方案核心设计:智取HTTPS
由于 80/443 端口受限,Traefik 无法直接通过 ACME 协议向 Let's Encrypt 申请泛域名证书。这时,我们将引入 ohttps.com 作为第三方服务,利用其强大的 DNS-01 挑战能力和 Webhook 推送机制。
graph TD
subgraph "ohttps 证书管理 (ohttps Certificate Management)"
A["ohttps.com 平台"] -->|"1. 证书申请/续期请求"| B("Let's Encrypt CA")
B -->|"2. DNS-01 挑战 (通过阿里云 DNS API)"| C("阿里云 DNS")
C -->|"3. 验证域名所有权"| B
B -->|"4. 颁发/续期 *.your-domain 证书"| A
end
subgraph "证书自动化部署 (Automated Certificate Deployment)"
A -->|"5. Webhook POST 请求 (包含证书/私钥, timestamp, sign)"| D("NAS服务器 - ImmortalWrt 路由")
D -->|"6. 端口转发 (外部4443 -> NAS内网IP:4443)"| E("NAS服务器 - 飞牛OS")
E -->|"7. Traefik 代理 (webhook.your-domain:4443 -> webhook-listener:5567)"| F("Webhook Listener 容器")
F -->|"8. 验证签名 (md5: timestamp + token)"| G{"签名有效?"}
G -- "是" --> H("保存证书文件: fullchain.cer, your-domain.key")
H -->|"9. 写入共享目录"| I("NAS宿主机: /opt/automation/traefik/data/certs/")
I -->|"10. Docker Volume 共享"| J("Traefik 容器")
J -->|"11. File Provider 检测文件变化"| K("Traefik 自动加载新证书")
K -->|"12. 为所有 *.your-domain 服务提供HTTPS"| L("Docker 服务容器")
G -- "否" --> M("返回 403 Forbidden")
F -->|"13. 返回 200 OK"| A
end
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#fcf,stroke:#333,stroke-width:2px
style C fill:#ddf,stroke:#333,stroke-width:2px
style D fill:#fcf,stroke:#333,stroke-width:2px
style E fill:#ddf,stroke:#333,stroke-width:2px
style F fill:#bbf,stroke:#333,stroke-width:2px
style J fill:#bbf,stroke:#333,stroke-width:2px
style L fill:#bfb,stroke:#333,stroke-width:2px
style I fill:#eee,stroke:#333,stroke-width:2px
style G fill:#ffc,stroke:#333,stroke-width:2px
style M fill:#fcc,stroke:#333,stroke-width:2px
整个自动化流程如下:
- ohttps.com 负责证书生命周期:
- 你在 ohttps.com 配置阿里云 DNS API 凭证。
- ohttps.com 通过阿里云 DNS-01 挑战,自动为你的 *.your-doamin.com 泛域名申请和续期证书。
- 一旦证书申请或续期成功,ohttps.com 会立即向你指定的 Webhook URL 发送一个包含新证书内容的 POST 请求。
- Webhook Listener 接收并保存证书:
- 一个轻量级的 Node.js 应用(运行在 Docker 容器中)作为 Webhook Listener,监听来自 ohttps.com 的 Webhook 请求。
- 它验证请求的签名(确保安全性),解析请求体中的证书和私钥内容。
- 将这些证书文件(fullchain.cer 和 your-doamin.com.key)保存到 NAS 宿主机上的一个共享目录中。
- Traefik 动态加载证书:
- Traefik 的 File Provider 功能会持续监控宿主机上指定证书目录的变动。
- 当 Webhook Listener 更新了证书文件后,Traefik 会自动检测到文件变化,并无需重启地加载新的泛域名证书。
- Traefik 代理服务:
- Traefik 使用这个泛域名证书,为 Docker 网络中所有配置了相应 Host 规则的容器(如 Navidrome、PhotoPrism,以及 Webhook Listener 自身)提供 HTTPS 代理访问。
- ImmortalWrt 解决内网痛点:
- 在路由器(ImmortalWrt 系统)上配置 DNS 重定向,将所有 *.your-doamin.com 的域名查询在内网中直接解析到 Traefik 容器的内网 IP 地址。这样,内网设备访问域名时,流量直接在局域网内部转发,避免了公网回环问题。
4. 部署实施步骤
请按照以下详细步骤在你的飞牛OS NAS 上部署这套自动化系统。
4.1. 前期准备:获取Traefik内网IP
在你的 NAS 服务器上,执行以下命令获取 Traefik 容器在 web Docker 网络中的 IP 地址。这是解决内网访问问题的关键。
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' traefik
当然正常来说。如果是启动在你的FN OS中的话,那就是你系统的 IP
例如,你会得到一个类似 172.18.0.2 的 IP 地址。请务必记住这个 IP。
4.2. 目录结构规划
在 NAS 服务器上选择一个合适的位置(例如 /opt/automation/),创建以下项目目录结构:
/opt/automation/
├── webhook_listener/
│ ├── app.js
│ ├── package.json
│ └── Dockerfile
└── traefik/
├── traefik.yaml # Traefik 静态配置
├── docker-compose.yaml # Docker Compose 主配置文件 (在此目录下运行)
├── data/ # Traefik 持久化数据(含证书)
│ └── certs/ # 用于存储 ohttps 推送的证书
│ ├── fullchain.cer
│ └── your-doamin.com.key
└── dynamic_configs/ # Traefik 动态配置(含证书加载规则)
└── tls.yaml
4.3. Webhook Listener (Node.js) 服务的创建
- 创建 package.json:
// /opt/automation/webhook_listener/package.json
{
"name": "ohttps-webhook-listener",
"version": "1.0.0",
"description": "A Node.js webhook listener for ohttps.com certificate updates.",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.19.2",
"body-parser": "^1.20.2"
}
}
- 创建 app.js:
// /opt/automation/webhook_listener/app.js
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const app = express();
// --- 配置项 ---
const WEBHOOK_TOKEN = process.env.WEBHOOK_TOKEN || 'your_secure_ohttps_callback_token';
// 证书保存路径,这是容器内部路径,与 Docker volume 映射对应
const CERT_FILE_PATH = process.env.CERT_FILE_PATH || '/certs/fullchain.cer';
const KEY_FILE_PATH = process.env.KEY_FILE_PATH || '/certs/your-doamin.com.key';
// 使用 JSON body parser
app.use(bodyParser.json());
app.post('/webhook', async (req, res) => {
console.log(`[${new Date().toISOString()}] Received webhook request from ${req.ip}`);
// 1. 获取请求参数
const { timestamp, payload, sign } = req.body;
if (!timestamp || !payload || !sign) {
console.error(`[${new Date().toISOString()}] Missing required fields in webhook payload.`);
return res.status(400).json({ success: false, message: 'Missing required fields (timestamp, payload, sign).' });
}
// 2. 验证签名 (安全性关键)
// 签名规则:`${timestamp}:${回调令牌}`的32位小写md5值
const rawSignatureString = `${timestamp}:${WEBHOOK_TOKEN}`;
const expectedSign = crypto.createHash('md5').update(rawSignatureString).digest('hex');
if (sign !== expectedSign) {
console.error(`[${new Date().toISOString()}] Invalid signature: Expected ${expectedSign}, Got ${sign}`);
return res.status(403).json({ success: false, message: 'Forbidden: Invalid signature.' });
}
console.log(`[${new Date().toISOString()}] Webhook signature verified successfully.`);
// 3. 解析证书内容
const { certificateCertKey, certificateFullchainCerts } = payload;
if (!certificateCertKey || !certificateFullchainCerts) {
console.error(`[${new Date().toISOString()}] Missing certificate key or fullchain certs in payload.payload.`);
return res.status(400).json({ success: false, message: 'Missing certificate key or fullchain certs.' });
}
// 4. 保存证书文件
try {
// 确保证书存储目录存在
fs.mkdirSync(path.dirname(CERT_FILE_PATH), { recursive: true });
// 保存证书和私钥
await fs.promises.writeFile(CERT_FILE_PATH, certificateFullchainCerts, { encoding: 'utf-8', mode: 0o644 }); // 证书读写权限
await fs.promises.writeFile(KEY_FILE_PATH, certificateCertKey, { encoding: 'utf-8', mode: 0o600 }); // 私钥严格权限
console.log(`[${new Date().toISOString()}] Successfully updated certificate files: ${CERT_FILE_PATH}, ${KEY_FILE_PATH}`);
// Traefik 的 File Provider (watch: true) 会自动检测到文件更新并加载新证书,无需额外操作。
res.status(200).json({ success: true });
} catch (error) {
console.error(`[${new Date().toISOString()}] Error saving certificate files: ${error.message}`);
res.status(500).json({ success: false, message: `Internal Server Error: Failed to save certificate files: ${error.message}` });
}
});
const PORT = process.env.PORT || 5567; // 容器内部监听端口 5567
app.listen(PORT, '0.0.0.0', () => {
console.log(`[${new Date().toISOString()}] Webhook listener running on port ${PORT}`);
});
- 创建 Dockerfile:
# /opt/automation/webhook_listener/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY app.js ./
# 暴露端口,供 Docker Compose 映射
EXPOSE 5567
CMD ["node", "app.js"]
4.4. Traefik 配置文件的创建
- 创建 traefik.yaml:
# /opt/automation/traefik/traefik.yaml (Traefik 静态配置)
---
log:
level: INFO
global:
sendAnonymousUsage: false
entryPoints:
web:
address: :80
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: :443
providers:
docker:
exposedByDefault: false
watch: true
network: web
file:
directory: "/etc/traefik/dynamic_configs" # 关键! 动态加载的配置
watch: true
api:
insecure: true
dashboard: true
# certificatesResolvers 部分必须完全注释掉或删除 这里不能使用
# certificatesResolvers:
# myresolver:
# acme:
# email: your-email@example.com
# storage: /data/acme.json
# dnsChallenge:
# provider: alidns
# delayBeforeCheck: 600
- 创建 tls.yaml:
# /opt/automation/traefik/dynamic_configs/tls.yaml (Traefik 动态配置:证书定义)
tls:
certificates:
- certFile: "/data/certs/fullchain.cer"
keyFile: "/data/certs/namehu.top.key"
# 这部分非常关键,它告诉 Traefik 这个证书是为哪些域名服务的
stores:
- default # 使用默认的证书存储,通常是这个
重要提示:请确保 tls.yaml 的文件编码为 UTF-8。使用
file -i /opt/automation/traefik/dynamic_configs/tls.yaml命令进行检查,如果不是 UTF-8,请使用 iconv 命令进行转换。
4.5. Docker Compose 主配置文件
# /opt/automation/traefik/docker-compose.yaml
version: '3.8'
services:
traefik:
image: traefik:v3.3
container_name: traefik
restart: always
networks:
- web
ports:
- "880:80" # 宿主机 880 端口映射到容器 80 (HTTP)
- "4443:443" # 宿主机 4443 端口映射到容器 443 (HTTPS)
- "8080:8080" # Traefik Dashboard 端口
volumes:
- "./traefik.yaml:/etc/traefik/traefik.yaml:ro"
- "./data:/data" # 宿主机 /opt/automation/traefik/data -> 容器 /data
- "./dynamic_configs:/etc/traefik/dynamic_configs:ro"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
command:
- "--configFile=/etc/traefik/traefik.yaml"
- "--providers.file.directory=/etc/traefik/dynamic_configs"
- "--providers.file.watch=true"
webhook-listener:
build:
context: ../webhook_listener # (从 /opt/automation/traefik/ 向上溯一层到 /opt/automation/webhook_listener/)
dockerfile: Dockerfile
container_name: webhook-listener
restart: always
environment:
# ⚠️ 替换为你在 ohttps 配置的"回调令牌"!
- WEBHOOK_TOKEN=your_secure_ohttps_callback_token
- CERT_FILE_PATH=/app/certs_mount/fullchain.cer
- KEY_FILE_PATH=/app/certs_mount/your-doamin.com.key
volumes:
# 宿主机 ./data/certs 目录映射到 webhook-listener 容器的 /app/certs_mount
- ./data/certs:/app/certs_mount
# 不再直接暴露端口,完全通过 Traefik 代理
networks:
- web # 与 Traefik 在同一个 Docker 网络中
labels: # 修复:为 webhook-listener 添加 Traefik 标签以实现代理
- "traefik.enable=true"
- "traefik.http.routers.webhook.rule=Host(`webhook.your-doamin.com`) && Path(`/webhook`)"
- "traefik.http.routers.webhook.entrypoints=websecure"
- "traefik.http.routers.webhook.tls=true"
- "traefik.http.services.webhook.loadbalancer.server.port=5567" # 修复:容器内部监听端口改为 5567
# 你的其他应用服务,例如 Navidrome 或者你可以创建一个独立的 docker-compose 文件来管理。
# Traefik 能够根据labels正确的发现你的服务
navidrome:
image: deluan/navidrome:latest
container_name: navidrome
restart: always
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.navidrome.rule=Host(`navidrome.your-doamin.com`)"
- "traefik.http.routers.navidrome.entrypoints=websecure"
- "traefik.http.routers.navidrome.tls=true"
- "traefik.http.services.navidrome.loadbalancer.server.port=4533"
# ... 其他 Navidrome 配置 ...
networks:
web:
external: true
4.6. 创建空证书目录(如果不存在)
确保宿主机 /opt/automation/traefik/data/certs/ 目录存在,Docker 挂载时会自动创建,但手动创建更稳妥:
mkdir -p /opt/automation/traefik/data/certs
5. openWrt 路由器网络配置
5.1. 路由器端口转发
你的网络运营商关闭了 80 和 443 端口,我们通过 Traefik 映射了外部的 880 (HTTP) 和 4443 (HTTPS) 端口。在 ImmortalWrt 路由器上,请确保以下端口转发规则已正确配置:
- 将路由器外部的 880/TCP 流量转发到 NAS 服务器的 NAS内网IP:880。
- 将路由器外部的 4443/TCP 流量转发到 NAS 服务器的 NAS内网IP:4443。
5.2. 内网 DNS 配置 (解决回环问题)
如果你不需要可以跳过
为了确保内网设备能够通过域名正确访问你的服务,我们需要在 ImmortalWrt 上配置 DNS 重定向,将 *.your-doamin.com 域名在内网中直接解析到 Traefik 容器的内网 IP。
-
登录 ImmortalWrt 路由器管理界面。
-
导航到 网络 (Network) -> DHCP/DNS。
-
滚动到 自定义选项 (Custom Options) 框。
-
添加以下行:
address=/.your-doamin.com/172.18.0.2- 将 172.18.0.2 替换为你在步骤 4.1 中获取的 Traefik 容器的实际内网 IP 地址。
- address=/.your-doamin.com/ 这条规则会让 DNSMasq 将所有以 .your-doamin.com 结尾的域名(包括 your-doamin.com 本身和 *.your-doamin.com 下的子域名)都解析到指定的内网 IP。
-
保存并应用更改。
-
刷新内网设备 DNS 缓存 (重启设备或在 Windows 上运行
ipconfig /flushdns)。
6. 部署与测试
6.1. 构建和启动所有服务
进入你的 docker-compose.yaml 文件所在的目录(即 /opt/automation/traefik/),执行:
docker compose build # 构建 webhook-listener 镜像
docker compose up -d # 启动所有服务
如果你是使用的 飞牛可视化界面。 则在配置好 compose 文件后。 点击 构建 即可
6.2. 验证 Webhook Listener 运行
- 检查日志:
docker logs webhook-listener。确认监听器是否已在容器内部端口 5567 启动。 - 访问 Webhook Listener 地址: 尝试在外网或内网(如果 DNS 规则已生效)访问 webhook.your-doamin.com:4443/webhook。由于它… POST 请求,你应该会收到 405 Method Not Allowed 或其他非 200 的响应,但关键在于请求能到达 Traefik 并被代理到 webhook-listener。
6.3. 首次证书部署
- 在 ohttps.com 平台手动触发证书部署: 找到你申请的 *.your-doamin.com 证书,点击"部署"或"立即部署"按钮,选择你配置的 Webhook 节点。
- 观察 Webhook Listener 日志:
- 执行
docker logs -f webhook-listener。 - 你将能看到类似 Received webhook request. 和 Successfully updated certificate files... 的日志输出。
- 执行
- 检查证书文件: 确认宿主机 /opt/automation/traefik/data/certs/ 目录下 fullchain.cer 和 your-doamin.com.key 文件内容是否已更新,并且权限分别为 644 (证书) 和 600 (私钥)。
- 检查 Traefik Dashboard: 访问 http://你的NAS-IP:8080 (如果你有在路由器上转发该端口),进入 TLS Certificates 部分。你应该能看到 *.your-doamin.com 的泛域名证书已正确加载。
6.4. 测试域名访问
- 从外网访问: 尝试访问 navidrome.your-doamin.com:4443 确保可以正常访问。
- 从内网访问: 在你内网的设备上(确保 DNS 缓存已刷新),尝试访问 navidrome.your-doamin.com:4443 现在应该也能正常访问了。
7. 维护与故障排除
- 日志是你的朋友:遇到问题时,首先检查
docker logs traefik和docker logs webhook-listener的输出。它们能提供宝贵的错误信息。 - 文件权限:确保所有挂载的卷和文件都有正确的读写权限。特别是私钥文件 your-doamin.com.key 权限应严格设为 600。
- 网络连接:检查 Docker 内部网络 (web) 是否正常,容器之间能否互相通信。确认 NAS 服务器能正常访问公网。
- Webhook Secret/Token 错误:如果 webhook-listener 总是报签名错误,请仔细检查 ohttps 上的"回调令牌"和 docker-compose.yaml 中 WEBHOOK_TOKEN 环境变量的值是否完全一致。
- YAML 格式错误:如果 Traefik 报告 field not found 或其他 YAML 解析错误,务必检查 tls.yaml 文件的编码 (必须是 UTF-8) 和缩进 (必须是空格,不能是 Tab 键)。
希望这份指南应该能帮助你顺利实现 Traefik 泛域名证书的自动化部署。祝你使用愉快!