家中的私有云端:飞牛OS、Traefik与全自动HTTPS泛域名证书的完美融合

1,040 阅读12分钟

在数字化时代,拥有一个属于自己的家庭网络存储(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


整个自动化流程如下:

  1. ohttps.com 负责证书生命周期:
    • 你在 ohttps.com 配置阿里云 DNS API 凭证。
    • ohttps.com 通过阿里云 DNS-01 挑战,自动为你的 *.your-doamin.com 泛域名申请和续期证书。
    • 一旦证书申请或续期成功,ohttps.com 会立即向你指定的 Webhook URL 发送一个包含新证书内容的 POST 请求。
  2. Webhook Listener 接收并保存证书:
    • 一个轻量级的 Node.js 应用(运行在 Docker 容器中)作为 Webhook Listener,监听来自 ohttps.com 的 Webhook 请求。
    • 它验证请求的签名(确保安全性),解析请求体中的证书和私钥内容。
    • 将这些证书文件(fullchain.cer 和 your-doamin.com.key)保存到 NAS 宿主机上的一个共享目录中。
  3. Traefik 动态加载证书:
    • Traefik 的 File Provider 功能会持续监控宿主机上指定证书目录的变动。
    • 当 Webhook Listener 更新了证书文件后,Traefik 会自动检测到文件变化,并无需重启地加载新的泛域名证书。
  4. Traefik 代理服务:
    • Traefik 使用这个泛域名证书,为 Docker 网络中所有配置了相应 Host 规则的容器(如 Navidrome、PhotoPrism,以及 Webhook Listener 自身)提供 HTTPS 代理访问。
  5. 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) 服务的创建

  1. 创建 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"
  }
}
  1. 创建 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}`);
});
  1. 创建 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 配置文件的创建

  1. 创建 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
  1. 创建 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。

  1. 登录 ImmortalWrt 路由器管理界面。

  2. 导航到 网络 (Network) -> DHCP/DNS

  3. 滚动到 自定义选项 (Custom Options) 框。

  4. 添加以下行:

    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。
  5. 保存并应用更改。

  6. 刷新内网设备 DNS 缓存 (重启设备或在 Windows 上运行 ipconfig /flushdns)。

6. 部署与测试

6.1. 构建和启动所有服务

进入你的 docker-compose.yaml 文件所在的目录(即 /opt/automation/traefik/),执行:

docker compose build # 构建 webhook-listener 镜像
docker compose up -d # 启动所有服务

如果你是使用的 飞牛可视化界面。 则在配置好 compose 文件后。 点击 构建 即可

image.png

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. 首次证书部署

  1. 在 ohttps.com 平台手动触发证书部署: 找到你申请的 *.your-doamin.com 证书,点击"部署"或"立即部署"按钮,选择你配置的 Webhook 节点。
  2. 观察 Webhook Listener 日志:
    • 执行 docker logs -f webhook-listener
    • 你将能看到类似 Received webhook request. 和 Successfully updated certificate files... 的日志输出。
  3. 检查证书文件: 确认宿主机 /opt/automation/traefik/data/certs/ 目录下 fullchain.cer 和 your-doamin.com.key 文件内容是否已更新,并且权限分别为 644 (证书) 和 600 (私钥)。
  4. 检查 Traefik Dashboard: 访问 http://你的NAS-IP:8080 (如果你有在路由器上转发该端口),进入 TLS Certificates 部分。你应该能看到 *.your-doamin.com 的泛域名证书已正确加载。

image.png

6.4. 测试域名访问

  1. 从外网访问: 尝试访问 navidrome.your-doamin.com:4443 确保可以正常访问。
  2. 从内网访问: 在你内网的设备上(确保 DNS 缓存已刷新),尝试访问 navidrome.your-doamin.com:4443 现在应该也能正常访问了。

7. 维护与故障排除

  • 日志是你的朋友:遇到问题时,首先检查 docker logs traefikdocker 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 泛域名证书的自动化部署。祝你使用愉快!