Cloudflare Zero TrustでおうちKubernetes上のサービスに外部からアクセスする

今更ながらZero Trustを構築したので,書いておく. 基本的にここにあんまりお金をかけたくないので,Cloudflareの利用料金は限りなくゼロに近い.唯一,どうしてもドメインが必要になってしまったので,ドメイン料金だけは払っている.

以前ImmichをおうちKubernetes上に構築していた.

h3poteto.hatenablog.com

これ,LAN内であればそのままアクセスできる構成にしていたけど,出先等でスマホアプリからも見られるようにしたかった.以前はTailscaleを使って,スマホアプリからVPNを張って見られるようにしていたんだが,これだとLAN内すべてのサービスに無制限にアクセスできちゃうのであまりよろしくない. 自分だけが使うのならまだしも,例えばImmichだけ仲間内に公開したい,というようなことを考えたときに,Tailscaleだとどうしても不安になってしまう.

例えば,longhornの管理画面とかは認証がなかったりするので,とても気になる.

というわけで,流行りのZero Trustを構築してみた.

概要

今現在俺が理解している認証の概要を書く.

まずCloudflare Zero Trustは,LAN内などのPrivate Network上にあるサービスを,認証付きで公開できる(Zero Trustの目的はこれだけではないので,そういう全体的なお話は別の記事を探してくれ).どうやって?というのも別記事を参照してほしいんだが,ここでの認証として大きく2タイプが存在する.

  1. ブラウザ上で動くWebアプリケーションに対してCloudflare Access経由で認証しアクセスを制御する
  2. WARP ClientでVPNを張ってアクセスする

1は基本的にブラウザ上で動くもの向けだ.例えばPrivate Network上のimmichを immich.example.local とかで公開するとする.このとき,example.local ドメインはCloudflareのDNSで管理されている必要がある.そして我々が immich.example.local にブラウザでアクセスすると,*.cloudflareaccess.com にリダイレクトされる.ここでSSOの認証を挟む.認証成功したら,Private Networkのimmichにリダイレクトされる形になる.

2はよくあるVPNと同じように,WARPというcloudflareが用意するクライアントアプリケーションを使う.このクライアントアプリケーションでログインすることで,VPNが貼られてPrivate Network上のサービスにアクセスできるようになる.ただし,ここでもアクセス制御は多少できるようになっている.

Cloudflare Zero Trustの準備

Authenticationの準備

とりあえずCloudflareのダッシュボードに入れる状態にしておく.

Zero Trustのダッシュボードで,Settings -> Authenticationと進む.Login methodsとしてGoogleを追加した.ここはどのような認証方法でも好きなものを選んだら良い.Google WorkspaceとGoogleがあるが,Workspaceはご存知の通りGoogle Workspaceを持っていればそれを使える.つまりWorkspaceユーザのみがログインできるということだ.

Googleの場合,個人のGoogleアカウントでOAuthアプリケーションを作ってそれを利用する.この場合,誰でもこのOAuthアプリにはアクセスできてしまうが,後述するPolicyでメールアドレスの制限をすることで,ログインできるユーザを絞り込むことができる.つまり,Workspaceを持っていて,独自のemail domainを持っているのであれば,Workspaceを選択すれば良い.そうではなく @gmail.com のユーザを,メールアドレス制限でログインできるようにするのがGoogleだ.

今回はGoogleで作成している.だいたいドキュメントどおりにOAuthをgoogle側で作って,ClientIDとClientSecretを設定してやれば問題ない.ログインできるユーザの制御は後述.

Policyの作成

Zero TrustのダッシュボードでAccess -> Policiesから新規にPolicyを作成する.このPolicy,後述するApplicationのアクセス制限と,WARP Clientのログイン制限の両方に使うことができる.両方とも同じPolicyを使ってもいいのだが,わかりにくいので俺は分離した.

まず,WARP Clientログイン用のPolicyを作る.

ここでEmailの制限ができる.前述の通り,googleのOAuthで誰でもOAuth認証はできるのだが,ここで認可されないので自分以外はWARP Clientにログインできないように制限することが可能となる.

次に,Cloudflare Access経由でログインしたときに,同じように認証できるようにPolicyを作っておく.条件はまったく同じ.

WARP Clientの設定

Device enrollment

Settings -> WARP Client -> Device enrollmentの設定をしておく.ここで先程作成したWARP Client用のPolicyを当てる.

Profile

次にProfileも作っておく.だいたいデフォルトでいいのだが,Split Tunnelsだけちょっと設定を入れている. 次のステップで,Tunnelsを作るのだが,WARP Clientで認証したユーザはどの範囲までCloudflare Tunnelsを通過させるかをここで設定することができる.もちろんすべての通信をTunnels経由にすることもできる.のだが,よくよく考えると俺はKubernetes上のサービスにアクセスしたいだけで,普段のインターネット接続をすべてZero Trust経由にしたいわけではない.なので,デフォルトは Exclude IPs and domains になっているところを Include IPs and domains に変更した.こうすることで,特定のIPやドメインのみをZero Trust経由にすることができる.

なお,Kubernetes API Server等,そもそもLAN内の一部のサービスに関しては,Zero Trust経由にしたくないものもある.なので,この Include IPs and domains はかなり狭い範囲に限定している.192.168.0.0/24 とかも指定できるのだが,LANをすべてZero Trust経由にしてしまうと,Kubernetesクラスタに問題が発生し,cloudflaredのPodが死んだときに何もアクセスできなくなってしまう.

Device posture

WARPを認証しているという事実を伝えるためだけのWARPというclient checksを追加した.これで,WARPで接続しているかどうかという条件を,Policy内で利用することができるようになる.

ドメインの登録

ここから先,サービスにホスト名を割り当てるために専用のドメインが必要になる.Zero Trustのダッシュボードから,Cloudflareのダッシュボードに移動し,Domain Registrationからドメインを一つ登録する.ここで買ってもいいし,外部で買ったドメインをここに登録してやってもいい.いずれにしろ,DNSとしてCloudflareのDNSを使う状態にしておく. そうしないと,ホスト名でアクセスしたときにCloudflare accessがリダイレクトを仕込むことができない.

Tunnelsの有効化とclodflaredの起動

Zero Trustのダッシュボードで,Network -> Tunnelsから新たにTunnelを一つ作る.このとき,Public HostnameとPrivate Networkの指定ができる.

Public Hostname

ここでは,このTunnel経由でアクセスしたいホスト名を指定する.ここで,指定するホスト名は必ず前述のドメインで登録したもののサブドメインである必要がある.ちなみにサブサブドメインを一度使ってみたのだが,これはうまく行かない.証明書がサブドメインまでしか対応しないので,追加でACMで証明書を用意する必要がある.が,これには追加課金が必要なのであまりおすすめしない. h3poteto.home みたいなドメインを買ったのであれば, immich.h3poteto.home までにしておくのが良い.

で,ここで接続先のサービスを指定する.まぁKubernetes内にTunnelを作るのであれば,Kubernetes内で解決できるホスト名やIPであれば問題ない.通常ServiceはClusterIP等で登録されているので,immich.h3poteto.home に対しては, http://immich-server.immich.svc.cluster.local:2283 みたいなServiceを設定しておけば良い.

Private Network

ここには,Tunnel経由でアクセスさせたいネットワークを記述する.例えばTunnel経由でLAN内全部アクセス許可したいのであれば, 192.168.0.0/24 を許可してしまってもいいわけだ.

ただ,先程言ったように,LAN内をすべてZero Trust経由アクセスにするのは,それはそれで弊害も大きいので必要な範囲に絞るのをお勧めする.

我が家のクラスタでは,MetalLBにより一部Service type LoadBalancerにIPを付与している.

h3poteto.hatenablog.com

ここにIP指定でアクセスさせたいものがある場合,そのIPを指定している.それ以外は許可していない.

Podのデプロイ

TunnelのOverviewにインストール方法が乗っている.とりあえずDocker版のコマンドをコピーして token を得る.あとは,deploymentとsecretを作って,podをデプロイしておく.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: cloudflared
  labels:
    app: cloudflared
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cloudflared
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: cloudflared
    spec:
      containers:
        - name: cloudflared
          image: cloudflared
          args:
            - tunnel
            - --no-autoupdate
            - run
            - --token
            - "$(TOKEN)"
          resources:
            requests:
              cpu: 200m
              memory: 300Mi
            limits:
              memory: 600Mi
          envFrom:
            - secretRef:
                name: cloudflare-secrets

Accessの設定

ようやくアクセス制限だ.Access -> Applicationsから新しくApplicationを作る.ホスト名指定でアクセスするKubernetes上のアプリケーションであれば,Self-hostedを選べば良い. Public hostnameとして,先程TunnelのPublic Hostnameで指定したものと同じ値を指定する. immich.h3poteto.homeみたいな.で,ここにPolicyとして,Cloudflare Access経由のログインPolicyを当てる.

動作確認

これですべての用意が整った.ブラウザで https://immich.h3poteto.home を開く.

と,このような画面が表示される.google認証で,許可されたemailだった場合のみ,immichのページにアクセスできる.

アプリからAPIに直接アクセスする

デスクトップアプリやスマホアプリでの問題点

ブラウザであれば,上記のようにブラウザがリダイレクトを処理してくれて,セッショントークンをつけてくれるので,問題なくCloudflare access経由でリダイレクトされるようになる.問題はスマホアプリ等だ.だいたいのアプリはWeb APIを叩いてjson等で情報のやり取りをする.となると,いきなりCloudflare accessにリダイレクトされてしまっては困るわけだ.仮にAPI呼び出しがリダイレクトに追従していたとして,Cloudflare accessのログイン画面を突破することはできない.

WARP Clientを通していれば許可する

このような場合の解決策が,WARP Clientだ.先程,WARP Clientの設定で,Device postureを設定しておいた.このDevice postureを通過している場合に,リクエストをすべて受け付けるようにすれば,APIであってもリダイレクトを挟まずにアクセスすることができる.

新しく,Service AuthのPolicyを作成する.

developers.cloudflare.com

Bypassというのもあるが,これはアクセス制御を無効化してしまうので,あんまり推奨されない.WARPで認証している前提であればService Authで十分である.

ここでは,Warpが必須という条件をつけている.

この状態にしておいて,スマホWARP Clientをインストールし,自分のworkspaceにログインしておく.

あとは通常通りimmichのアプリを使うと無事APIを叩けるようになっている.

WARP Clientがない場合はCloudflare accessに回したい

Policyのルールの優先度は,Bypass/Service Auth > Alow/Blockとなる.

dev.classmethod.jp

そのため,Service Authに加えて通常のAllow/Blockでemail制限を書いておけば,WARP Clientがない場合にはCloudflare accessに飛ばされるようになる.

特定のIPを公開する

今まではホスト指定でのアクセスの話をしてきた.HTTPで通信する一般的なWebアプリケーションならこれでもいいのだが,TCPにしろUDPにしろちょっと変わったアプリケーションだと,複数のポートを使ったりするものがある.例えば,アプリケーション自体は myapp.h3poteto.home でアクセスできるのだが,使っているポートが複数あり, myapp.h3poteto.home:30320myapp.h3poteto.home:20234,のような形で,同じドメインで複数ポートを公開し使っている場合もある.こういうアプリケーションの場合,現状Zero TrustのPublic Hostnameでは対応していない.ポートごとに別のドメインを割り当てるのであれば可能なのだが,一つのドメインで複数のポートアクセスをする形では作られていない.

というわけで,こういう場合IPアドレスで指定する方が楽にできる.例えばこのドメイン192.168.0.130 のIPをMetalLBから割り当てられているとしよう.前述の通り,まずTunnelsのPrivate Networkに 192.168.0.130/32 を追加しておく.次に,WARP Clientの Include IPs and domains にも同じCIDRを追加しておく必要がある.

最後に,Access -> Applicationsで新規アプリケーションを追加する際に,Private networkを選択しアクセスを許可したいIPとポートの範囲を決める.このとき自動的にAllow ruleとBlock ruleができる.AllowのIdentityに

このようにEmailを指定しておく. こうすると,WARP Clientでyour-gmail-address で認証したユーザのみが 192.168.0.130 にアクセスできるようになる.

LANでしかアクセスしないサービスは除外する

ここまで構築してきて,いよいよ以前作っていた

h3poteto.hatenablog.com

この仕組みは不要になるかなぁと思っていたのだが,結局こちらも残すことにした.そもそも外部からのアクセスが一切不要なサービスや,先程のように同一ドメインで複数ポートを使うようなサービスも用意しているため,これらをすべてCloudflare経由にする必要がない.

外部からアクセスしてくる通信が,すべてCloudflare Zero Trustで守られる前提であれば,LANでしかアクセスできないサービスはそのまま維持していても特に問題ないのではなかろうか.

もう一歩やりたい

APIアクセスをService Authで実現していたが,ここでEmailによるフィルタリングができると最高だ.Cloudflare accessにリダイレクトせずにEmail制限をかける方法がないかなぁーと探索中.Private Networkであれば,WARPに認証したユーザのEmailでフィルタがかけられるので,同じようなことがService Auth + Self-hostedで実現できるといいのだが…….