🌐

Kubernetesアプリを独自ドメインでTailnet内だけに公開する(SSL対応)

に公開

自宅鯖をお持ちの皆さんこんにちは!
サーバーで動かしているサービス、自宅の外からも接続したいですよね
ただ、サービスをインターネットに公開することには一定のセキュリティリスクが伴い、自分だけがアクセスするのであればインターネットの全員が到達できる場所にある必要はありません。

ここではVPNを通したクライアントだけが接続できるように、Tailscaleを使って公開することを考えます。
*.ts.net は使えますが、ホスト名の前に文字列がつくので覚えにくいです(tailscale: Generate a fun tailnet name
せっかく独自ドメインを持っているなら、それを使ってわかりやすい名前にしたいですよね

やりたいこと

  • xxx.example.com で外出先からでもサービスが閲覧できるようにする
  • tailscaleを有効化したクライアントのみがアクセス可能

前提

  • Kubernetesを運用しており、IngressやServiceなどについての知識がある程度ある
  • DNSを自由に設定できるドメインを持っている(今回はCloudflareの前提で説明します)

構成図

はじめに、本解説の構成図を示します

Cloudflare API Token の取得

external-dns (Ingressの設定を読み取ってDNS設定) と cert-manager (DNS-01 チャレンジを行い証明書を取得) のためにCloudflare API Tokenを取得しておきます
https://developers.cloudflare.com/fundamentals/api/get-started/create-token/
これを見て取得しましょう

今回の用途であれば、

  • ゾーン / DNS / 読み取り
  • ゾーン / DNS / 編集

の権限があれば問題ないです。ゾーンリソースについては設定したいドメインを指定しましょう。

トークンが取得できたらメモをしておいて、後で使います

前提ミドルウェアのデプロイ

以下4つのミドルウェアを使用します

  • Tailscale Operator
  • cert-manager
  • ExternalDNS
  • ingress-nginx

Tailscale Operator

https://tailscale.com/kb/1236/kubernetes-operator
k8sとtailscaleをいい感じに統合してくれます。特筆すべき設定はないです。
loadBalancerClass: tailscale で正常に 100.x.x.x がバインドされたServiceが生成されればよいです

cert-manager

https://github.com/cert-manager/cert-manager
helmでインストールする場合、values.yamlは以下のようにしましょう

values.yaml
crds:
  enabled: true

DNS-01チャレンジを通す

みんな大好きLet's Encryptで証明書を取得しますが、tailnet内はインターネットからはアクセスできません。なのでドメイン単位で証明書を取得できるDNS-01チャレンジを使用します。
また、namespaceの関係上ここでは ClusterIssuer の作成のみ行い、 Certificate は nginx-ingress namespaceの方で作成します

cert-manager namespaceに cloudflare-api-token-secret の名前でSecretを作っておきます
先ほどメモっておいたCloudflare API Tokenを使います

$ token=xxxxx
$ kubectl create secret generic cloudflare-api-token-secret -n cert-manager --from-literal=api-token=$token

ClusterIssuer を作成します
ingress-nginxと同じnamespaceを使用するのであれば Issuerでも構いません

example-com-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: example-com-issuer
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: example-com-issuer-account-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token-secret
              key: api-token

ExternalDNS

https://kubernetes-sigs.github.io/external-dns/latest/docs/tutorials/cloudflare/#creating-cloudflare-credentials

external-dns namespaceに cloudflare-api-token-secret の名前でSecretを作っておきます
先ほどメモっておいたCloudflare API Tokenを使います(namespaceが異なる場合、ここでもSecretを新規に作成する必要があります)

$ token=xxxxx
$ kubectl create secret generic cloudflare-api-token-secret -n external-dns --from-literal=api-token=$token

helmでデプロイする場合、valuesは以下のようにしてください

values.yaml
provider: cloudflare
policy: sync
env:
  - name: CF_API_TOKEN
    valueFrom:
      secretKeyRef:
        name: cloudflare-api-token-secret
        key: api-token

ingress-nginx

https://github.com/kubernetes/ingress-nginx
Ingress Controllerである Ingress NGINX Controller をクラスタに展開します

helmでデプロイする場合、valuesは以下のようにしてください

values.yaml
controller:
  service:
    loadBalancerClass: tailscale
  extraArgs:
    default-ssl-certificate: "ingress-nginx/wildcard-example-com-tls"

service の loadBalancerClass: tailscale を設定することで、ingress-nginx podの前段にtailscaleのIPがついたServiceができます

また、Certificateを作成しておきます

certificate-example-com.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example-com
  namespace: ingress-nginx
spec:
  secretName: wildcard-example-com-tls
  issuerRef:
    name: example-com-issuer
    kind: ClusterIssuer
  dnsNames:
    - "*.example.com"

これで、 *.example.com のワイルドカード証明書が取得できました。

サービスをデプロイする

ここまで来たら、準備は完了です
以下のマニフェストをデプロイすることでtailnet内のみからアクセスできるhttpsサービスができます

test_service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample
  template:
    metadata:
      labels:
        app: sample
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
              protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  namespace: default
spec:
  selector:
    app: sample
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
  namespace: default
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - nginx.example.com
  rules:
    - host: nginx.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: nginx-service
                port:
                  number: 80
nslookup (例示のため実際のIPとは異なる)
$ nslookup nginx.example.com
Server:         10.255.255.254
Address:        10.255.255.254#53

Non-authoritative answer:
Name:   nginx.example.com
Address: 100.123.456.789

Address がtailnetのIPとなっていることがわかります。


このように、tailscaleを接続した状態でnginxのwelcome画面が出たら成功です
念の為tailscaleを切断した状態でアクセスできないことも確認しておきましょう

大丈夫そうですね

IngressのTLS設定の補足

上記のマニフェスト、主にIngressの補足説明を少しだけします

nginx-ingressではtls.hostsにsecret設定をしない場合デフォルトのワイルドカード証明書が利用されます(参考:公式ドキュメント
そのため、ingress-nginxで指定した default-ssl-certificate の設定が使われます

Discussion