Traefik on k8s で let's encrypt のワイルドカード TLS 証明書を自動発行する
Traefik (proxy) を k8s Ingress controller として使うと、ワイルドカード証明書の自動発行が簡単そうなのでやってみた。 Traefik 公式ドキュメント が、わかりにくい、というか、設定のための情報が散逸してたり、そもそもプロダクトとして必ずしも k8s を前提としているわけではないのでじゃあ k8s 向けにはどうすんだ、みたいなのが難しかったので、やったことのメモ書き。
状況設定としては
- LAN 内に 192.168.0.100 を IP としてもつサーバがあり、そこで k0s をシングルノードで動かしている
example.com
という domain を保持している- LAN 内の別の PC から、サーバに対して
https://nantoka.wildcard.example.com/
でアクセスしたい
みたいな感じ。
インストール
公式 Docker image を使って自力で manifest 書いてもなんとか動くとは思うんだけど、 CRD (Custom Resource Definitions) とかあるしけっこう骨だと思われるので、素直に公式の Helm chart を使ってインストールする。
基本的には公式ドキュメントの k8s 向けインストール手順に従えばいい。
公式レポジトリを
$ helm repo add traefik https://helm.traefik.io/traefik
追加し
$ helm inspect values traefik/traefik > values.yaml
のようにして設定ファイルを作成しておく。
設定を指定しつインストール
$ helm install -f values.yaml traefik traefik/traefik
インストール後に設定ファイル values.yaml
を更新してそれを反映するときは
$ helm upgrade -f values.yaml traefik traefik/traefik
とする。
service.externalIPs
を指定して外部からのアクセスをうけつける
これで Ingress Controller としては立ち上がったので通常の Ingress を作成するとクラスタ外部からアクセスできるようになる、はず、だが、 公式 Helm chart でインストールするとデフォルトでは Traefik の Service が外部に露出していない。
さきほど作成した values.yaml
の service.externalIPs
に露出する IP を指定する。
service: externalIPs: - 192.168.0.100
これでクラスタ外から Ingress / Service 経由で Pod 等にアクセスできるようになる。
Traefik の dashboard にアクセスする
https://doc.traefik.io/traefik/getting-started/install-traefik/#exposing-the-traefik-dashboard
いったんはこれの port-forward で dashboard にアクセスする。
Helm chart でインストールすると dashboard 用の Traefik IngressRoute がインストールされてはいるが、
entrypoint が traefik になっており、これは Helm chart でセットアップされる port としては expose: false
になっているためそのままでは外部からアクセスできない。
このへんは TLS まわりのセットアップがおわってから整備していくことにする。
DNS の設定
LAN 内の別の PC から、サーバに対して
https://nantoka.wildcard.example.com/
でアクセスしたい
なので、 DNS サーバで *.wildcard.example.com
の A レコードを (今回の例だと) 192.168.0.100
に設定する。
Persistent Volume の設定
Traefik は Let's Encrypt (および ACME protocol サポートしている TLS 証明書発行者) の証明書の自動発行・自動更新に対応していると冒頭に書いたが、 そのためには Traefik サーバ側で発行された証明書を管理するために永続ストレージが必要となる。
k8s クラスタで PersistentVolumeClaim (PVC) に対応した PersistentVolume があるのであれば、 values.yaml
に
persistence: enabled: true
のように書くと、 PersistentVolume が /data
というパス (実際には values.yaml
の persistence.path
で指定されている) にマウントされる。
( traefik-helm-chart/values.yaml at 5d97a2e30076302950c31fc9a98f267bdd624fe8 · traefik/traefik-helm-chart · GitHub 参照)
hostPath Volume を利用する場合
自分の環境の場合、シングルノードでもありまだちゃんとした PersistentVolume はセットアップしていなかったので、いったん hostPath な Volume を利用することにした。
この場合、以下のような values.yaml
を書くことになる。
deployment: additionalVolumes: - name: acmeStore hostPath: path: /volumes/acmeStore type: Directory additionalVolumeMounts: - name: acmeStore mountPath: /acmeStore
deployment.additionalVolumes[].hostPath.path
は適宜 k8s ノード側のパスを指定すること (この例だと /volumes/acmeStore
を用意した)。
公式の Helm chart だと /data
ディレクトリをマウント先としているため、そことかぶらないようにした。
(ちなみにこのへんの設定は、結局 traefik-helm-chart/_podtemplate.tpl at master · traefik/traefik-helm-chart · GitHub などのファイルを読み解いた。このへんも公式 Helm chart のわかりづらいところだと思う)
また、 公式 Helm chart だと、プロセスが uid=65532, gid=65532 で動くので、そのアカウントにとって writable なディレクトリにしておく必要がある。
$ sudo chown 65532:65532 /volumes/acmeStore
証明書ストアの場所を Traefik に設定する
証明書ストアの場所だが https://doc.traefik.io/traefik/https/acme/#storage に書いてあるように、 Traefik の設定としては、たとえば以下のように書く必要がある。
certificatesResolvers: myresolver: acme: storage: /acmeStore/acme.json
が、これはあくまで Traefik の static configuration に書いておく必要があるのであって、これをそのまま values.yaml
に書くのではない (わかりづらい)。
残念ながら現在の公式 Helm chart では certficicatesResolvers
の便利な書き方がサポートされているわけではないので、以下のように書く必要がある。
additionalArguments: - "--certificatesResolvers.le.acme.storage=/acmeStore/acme.json"
certificatesResolver の名前としては、公式ドキュメントに倣って le
(Let's Encrypt の略かな?) としたが、なんでも構わない。
ACME Challenge の設定
Let's Encrypt でワイルドカード証明書を取得するためには、 DNS-01
Challenge を利用する必要がある。
https://doc.traefik.io/traefik/https/acme/#dnschallenge
かんたんにいうと、 対象となるドメインの TXT レコードを設定し Let's Encrypt 側にそれを確認してもらう Challenge である。
なので HTTP-01
Challenge や TLS-ALPN-01
Challenge と異なり、今回のように対象ドメインの解決先が private IP でも利用できる (副産物であるが)。
Challenge の設定をするためには、 Traefik の設定として下記のように設定する。
certificatesResolvers: myresolver: acme: storage: /acmeStore/acme.json # 設定済 email: [email protected] dnsChallenge: provider: ***provider***
さきほど説明したように、これはあくまで Traefik の設定なので、 Helm chart でインストールしている場合は以下のように additionalArguments
で設定する必要がある。
additionalArguments: - "--certificatesResolvers.le.acme.storage=/acmeStore/acme.json" # 設定済 - "[email protected]" - "--certificatesResolvers.le.acme.dnsChallenge:provider=***provider***"
さて、上記の例で ***provider***
と書いてあるところは、 DNS プロバイダを指定する。
どのような DNS プロバイダがサポートされているかは、以下に記載されている (ライブラリとして LEGO を利用しているようだ)。
https://doc.traefik.io/traefik/https/acme/#providers
今回自分は LuaDNS を利用したが、もちろん Route 53 や Google Cloud DNS (Google Domains の DNS ではないことに注意 *1 ) もサポートされている、だけではなくさくらのクラウドや IIJ もサポートされているようだ。 また、 仮にサポートされていないとしても外部プログラムを用いて TXT レコードを設定することができれば利用できそうである。
LuaDNS の場合、
additionalArguments: - "--certificatesResolvers.le.acme.dnsChallenge:provider=luadns"
のように書く。
また、 LuaDNS 用の設定として環境変数を指定する必要がある。
環境変数は values.yml
に以下のように記述する。
env: - name: LUADNS_API_USERNAME value: "***username***" # 実質 LuaDNS でのアカウントの e-mail アドレス - name: LUADNS_API_TOKEN value: "***API token***"
アプリケーションの公開
ということで、ようやくアプリケーションを LAN 内に公開できるようになった。
なんとなく定番っぽい whoami イメージを立ち上げることにする。
Deployment と Service については、特筆するべきことはないと思う。
Deployment
apiVersion: apps/v1 kind: Deployment metadata: name: whoami-deployment spec: selector: matchLabels: app: whoami replicas: 1 template: metadata: labels: app: whoami spec: containers: - name: whoami image: jwilder/whoami ports: - containerPort: 8000
Service
apiVersion: v1 kind: Service metadata: name: whoami-service labels: app: whoami spec: selector: app: whoami ports: - protocol: TCP port: 80 targetPort: 8000
Ingress
TLS 証明書自動発行のためには、 通常 Ingress として利用される networking.k8s.io/v1
の Ingress ではなく
Traefik の CustomResource である traefik.containo.us/v1alpha1
の IngressRoute を利用する必要がある。
apiVersion: traefik.containo.us/v1alpha1 kind: IngressRoute metadata: name: whoami-ingress spec: entryPoints: - websecure routes: - match: Host(`whoami.wildcard.example.com`) kind: Rule services: - name: whoami-service port: 80 tls: certResolver: le domains: - main: "wildcard.example.com" sans: - "*.wildcard.example.com"
spec.tls.certResolver
のところで、上記で設定した certificatesResolvers である le
を指定している。
また、 domains のところで指定したとおり、
TLS 証明書が Let's encrypt から発行される。
ここは main として *.wildcard.example.com
を指定しても (ドキュメントによれば) うまくいくと思うが、
ドキュメントのサンプル設定にしたがってこのようにした。
この IngressRoute resource により、クライアントから https://whoami.wildcard.example.com/
にアクセスすると
- まだ TLS 証明書を発行していなければ発行する
- すでに発行されているが古ければ再発行する
- さもなければすでに発行されている証明書を利用する
といった動作となる。
Traefik dashboard を LAN からアクセスできるようにする
これで任意の *.wildcard.example.com
に対して TLS アクセスができるようになった。
さきほどまで port-forward で利用していた Traefik dashboard も IngressRoute で公開してみる。
apiVersion: traefik.containo.us/v1alpha1 kind: IngressRoute metadata: name: dashboard-traefik spec: entryPoints: - websecure routes: - match: Host(`traefik.wildcard.example.com`) && (PathPrefix(`/dashboard`) || PathPrefix(`/api`)) kind: Rule services: - name: api@internal kind: TraefikService tls: certResolver: le domains: - main: "wildcard.example.com" sans: - "*.wildcard.example.com"
このリソースを作成することで、 https://traefik.wildcard.example.com/dashboard/
にアクセスすると dashboard にアクセスできるようになる。
すこしだけ IngressRoute の TLS 設定を簡略化する
毎回毎回証明書の main と SANs を指定していくのはダルいし、もしかすると別のリソースで間違った SANs を指定してしまうかもしれない。
entryPoints の static configuration として TLS 設定をしておくと、各 IngressRoute のほうの設定が少し楽になる。
values.yml
の ports:
に entryPoint の設定があるので、
ports: websecure: port: 8443 expose: true exposedPort: 443 protocol: TCP tls: enabled: false options: "" certResolver: "" domains: []
となっているところを
ports: websecure: tls: enabled: false options: "" certResolver: le domains: - main: "wildcard.example.com" sans: - "*.wildcard.example.com"
のようにする。
ports.websecure.tls.enabled
は false のままでよいと思う。
もしかすると k8s クラスタで利用する TLS 証明書が単一の場合はここを true にしておくことで、
IngressRoute 側で一切指定をしなくても TLS 証明書つきアクセスを提供できるのかもしれない。
https://doc.traefik.io/traefik/user-guides/crd-acme/#traefik-routers
を参照したところ spec.tls.certResolver
の設定だけやってますね。
時間ができたらやってみる。
これで IngressRoute のほうは
spec: tls: certResolver: le domains: - main: "wildcard.example.com"
のように指定するだけで、ワイルドカード証明書を利用できるようになった。
まあ一行減っただけだし、そもそも main としてワイルドカードドメインを指定していれば減るわけでもないので、ふつうはここまでやる必要はないのかもしれない。