NAS 自架之旅:告別自簽憑證,使用 Traefik 架設完美內網

5 min read

在內網架設服務時,常見的做法是使用類似 https://192.168.1.100 的方式。不過,當服務數量多時,不僅網址難記,自簽憑證還會帶來煩人的警告提示。這次我們就來看看如何利用 Traefik 為每個服務自動簽發合法憑證,讓內網管理更方便。

需要具備合法網域

如果要取得合法簽章,必須擁有自己的網域。除非你要把自己的憑證裝進你的每台設備中。

Traefik 設定

Traefik 是由 Golang 開發的反向代理工具,不僅能取代 Nginx,還能與 Docker 無縫結合。以下是我的 Traefik 的設定檔範例:

api:
  dashboard: true
  insecure: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
          permanent: true
  websecure:
    address: ":443"
    http:
      tls:
        certResolver: letsencrypt
        domains:
          - main: lab.yourdomain.com
            sans:
              - '*.lab.yourdomain.com'

providers:
  file:
    watch: true
    filename: /etc/traefik/provider.yml
  docker:
    watch: true
    endpoint: "unix:///var/run/docker.sock"
    defaultRule: "Host(`{{ lower ( trimPrefix `/` .Name ) }}.lab.yourdomain.com`)"
    exposedByDefault: false

certificatesResolvers:
  letsencrypt:
    acme:
      email: youremail@yourdomain.com
      storage: /etc/traefik/acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "1.0.0.1:53"

serversTransport:
  insecureSkipVerify: true

設定檔中可以看到兩個 provider,分別是 file 和 docker。docker provider 會透過 docker api 去讀取 docker label 屬性,所以我們可以把每個服務的 Traefik 設定直接寫在 docker compose 的設定檔中。而 file provider 則適合用來配置非 Docker 容器的服務,例如 NAS 本身的 Web UI。

整體的邏輯是透過 dns rewrite 將 *.lab.yourdomain.com 指向 Traefik 內網的 ip,然後利用 Traefik 進行反向代理與憑證管理。憑證的部分我比較懶惰是直接簽一張 *.lab.yourdomain.com 的萬用憑證,但也可以為每個服務單獨簽發憑證。

DNS 的部分我是用 Adguard Home 作為內網的 dns server,除了用來 dns rewrite 外,還可以順便擋廣告。但如果不介意隱私的話,也可以直接在公開網域的 dns 直接把 *.lab.yourdomain.com 指向內網的 ip,這樣就不用多一台內網的 dns server。

Adguard DNS Rewrite

docker compose 設定

Traefik

Traefik 的 docker compose 設定檔如下

services:
  traefik:
    image: traefik:v3.2
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "/array/appdata/traefik:/etc/traefik"
    environment:
      - CF_DNS_API_TOKEN=...
    networks:
      - homelab
    labels:
      traefik.enable: true
      traefik.http.services.traefik.loadbalancer.server.port: 8080
      traefik.http.routers.traefik.rule: "Host(`traefik.lab.yourdomain.com`)"
      traefik.http.routers.traefik.entrypoints: websecure
      traefik.http.routers.traefik.service: traefik

networks:
  homelab:
    external: true

因為我的 dns 使用 cloudflare 管理,所以在這邊會需要 Cloudflare 的 API token。Traefik 本身的 Web UI 介面預設開在 8080,這裡透過 docker labels 去單獨設定這個服務,讓我們可以透過 https://traefik.lab.yourdomain.com 反向代理存取 traefik container 的 8080 port。我這裡建立了一個 docker network 叫做 homelab,讓 Traefik 和其他需要反向代理的服務在同一個網段中,而不用在內網搶 port。

Adguard Home

底下提供另一個例子是 Adguard Home 的 docker compose 設定檔

services:
  adguardhome:
    image: adguard/adguardhome:latest
    container_name: adguardhome
    restart: unless-stopped
    ports:
      - 192.168.1.100:53:53/tcp
      - 192.168.1.100:53:53/udp
    volumes:
      - /array/appdata/adguardhome/work:/opt/adguardhome/work
      - /array/appdata/adguardhome/conf:/opt/adguardhome/conf
    networks:
      - homelab
    labels:
      traefik.enable: true
      traefik.http.services.adguardhome.loadbalancer.server.port: 3000
      traefik.http.routers.adguardhome.rule: "Host(`adguard.lab.yourdomain.com`)"
      traefik.http.routers.adguardhome.entrypoints: websecure
      traefik.http.routers.adguardhome.service: adguardhome

networks:
  homelab:
    external: true

我一樣把 Adguard Home 加到同一個 docker network 中,並設定 docker labels,這樣就可以透過 https://adguard.lab.yourdomain.com 反向代理存取 adguardhome container 的 3000 port。

Adguard Home 這裡有個小坑是需要把 ports bind 在內網的 ip 上,而不是 127.0.0.1,不然其他 docker 會沒辦法 resolve。1

總結

過去我們需要透過像 https://192.168.1.100:9005 的方式存取內網服務,不僅會有自簽憑證的煩人警告,網址又難記,不同的服務的 port 還會打架。如今設定好 Traefik 之後,我們就可以直接用 https://nextcloud.lab.yourdomain.com 正常有合法簽章的網域來存取內網服務,十分舒服。

Footnotes

  1. https://stackoverflow.com/questions/64007727/docker-compose-internal-dns-server-127-0-0-11-connection-refused