前回までのあらすじ:
クラウドに出島を作り、5Gルータも入れ、ささやかながらも幸せなOpenVPN生活を送っていた筆者だったが、ドトールWifiの非情な仕様変更がおそいかかる!!
どういうこと?
ドトールのWifiでOpenVPNが通じなくなってしまった。わかったよ。お前らはとにかくVPNが嫌いでブロックしたいというのはよくわかった。
いやいや商用VPN製品ならいけますよって人もいるかもしれないし人がやる分には別にどうこう言う気はないのだけど、自分は以下の理由で商用VPN製品は避けるようにした。
最近のVPN事業者は怪しい(偏見です)
旅行系のYoutubeしてる人でVPN製品の宣伝をしている人はわりといるが、VPN業界の不都合な真実というものがあるらしい。具体的には業者によっては個人情報を他社に売っているらしい。
で、このツイート(のスレッド)を見て、そっかーって思ってしまった。正直エントリポイントでも嫌われて利便性が悪く、まともじゃない商売をしている人もいる怪しい界隈からは離れたほうがいいかもなーと思った。
で、Cloudflare Zero Trust(Tunnel+Access)ですよ
出典: https://developers.cloudflare.com/reference-architecture/diagrams/sase/sase-clientless-access-private-dns/
要するに
- 公開したいLAN内のアプリ、つまり特定のIPアドレス+ポートをCloudflare Tunnel(cloudflared)でcloudflareにトンネリングする(図中⑤)。図中だとCloudflareのドメイン名でアクセスしてるけど自分のドメインのFQDNを指定できる(ただし制限あり。後述)
- 利用者は普通にエンドポイントのアドレスを指定する(図中①)。実際に到達するのはCloudflareのedge。
- Edgeではアプリにルーティングする前に、Cloudflare Accessの機能で様々なアクセス制限をかけることができる(図中③, ④)。いわゆるIdentity Aware Proxy(IAP)だね。例えば以下のようなルールを強制できる
- OAuth認証(ログイン可能な時間枠は設定できない。ログイン後のセッション有効時間は設定できる)
- OTP認証
- GeoIP制限
- ユーザドメイン制限(実際にはユーザ名の末尾で判断)
- いわゆる認証トークン
- で、認証されると実際のアプリにトラヒックがルーティングされる(図中⑤)
- もちろん天下のCloudflareなのでWAFやDDoS対策もバッチリ
しかもCloudflareの恐ろしいのはこのTunnelとAccessが無料で使えるってところ(ただしクレカの登録は必要)。具体的には無料で以下のことができる。
- 無制限のCloudflare Tunnel
- Cloudflare Accessの利用は 50 user まで
- ZTNAを定義するポリシーは無制限に作成適用可能
- 機能制限版とはいえWAF, Rate limitting
- Universal SSL証明書(後述)
- もちろんCDNが暗黙的に組み込まれて有効化
似たようなことはGoogle Cloudでもできるんだけど、オンプレ〜クラウド間の接続が専用線orVPNになってしまいめっちゃ費用がかかる。正直個人でやるにはオーバーキル。さらに上位モデルは目ん玉飛び出るお値段。せめてCloudflare Tunnel相当のものが使えればなあ。
ということでCloudflareですよ!ゼロトラストってニュースでも言ってるくらいだし、「いや・・・オレんとこゼロトラストでやってるんス・・・(メガネクイッ)」とか言えるとかっこいいじゃん!って
実際にやってみるといろいろわかった。世の中そんなに甘くない
ということで実際に使ってるアプリ(自宅で録画した動画を見れる自作アプリ。フロントエンドはVue.js、バックエンドはGo)をCloudflare対応にしてみた。1日程度で基本的な対応作業はできたが、いろいろ制限があることもわかった。
Cloudflare Tunnelは1ポート1接続。複数出したいときは複数実行する
実際のCloudflare Tunnelは以下のように実行する
/usr/bin/cloudflared --no-autoupdate tunnel run --token <接続用トークン>
この接続用トークンはホスト名+ポート番号ごとに1つ発行されるので、Webサーバ(443/tcp)用に1つ、APIサーバ(1324/tcp)用に1つ実行する必要がある。常時動かすには /etc/systemd/system/cloudflared.service をコピーしてトークンを書き換えて実行させる。ChatGPTパイセン曰く /etc/cloudflared/config.yml で複数セッションを定義する方法もあるらしいのだがよくわかんなかったのでスルーした。
接続自体は超あっさりできる。
Cloudflareの利用にはドメイン必須(たぶん)
Cloudflare Tunnelを外部公開するときに必要となる。もともとさくらインターネットのドメインで取っていたドメインのNSレコードをCloudflareに振り替える。
サブドメインだけ移管するということはできない。これはCloudflareの仕様。
Cloudflare Tunnelの公開エンドポイントをサブドメインのFQDNっぽく見せることはできなくもないが無料ではできない。
Cloudflare Tunnelの公開エンドポイントを作ると、Cloudflare DNS上にCNAMEレコードが作成される。普通に移管したドメイン上のホスト(例えば https://app.example.com)として公開すると、Cloudflare DNS上には以下のようにレコードが作られる。
app CNAME <CloudflareのID>
これをサブドメインっぽく公開する(例えば https://app.int.example.com)と、Cloudflare DNS上には以下のようにレコードが作られる。
app.int CNAME <CloudflareのID>
なんだできんじゃんと一見思うのだが(後述)
Cloudflare無料枠のSSL証明書はトップドメイン限定。サブドメインに対応するにはTotal SSL(有料)必須
Cloudflare無料枠で提供されるSSL証明書は *.<管理するドメイン> に有効なものなので、https://app.int.example.com に対応したSSL証明書は提供されない。Cloudflareがクライアントから見た最初のエンドポイントなので、自前でなんとかすることもできない。
解決するには管理ドメイン直下のホスト名で公開するか、Total SSL(Advanced Certificate Manager)が必要。月額 $10.0。
今回は管理ドメイン直下のホスト名で公開した。
OAuth認証はあくまで「認証に成功した」ってこと以上の意味はない
ここ結構クリティカルだなと思ったところ。
Cloudflare Accessのポリシー設定でアクセス時にOAuth認証を必須に設定することができる。私の場合はGoogle認証にしたが、Cloudflareのガイド上だとアクセス対象を External、つまりGoogleアカウントの認証が通るのであれば誰でも通過できてしまう。
うーんこれだとあまり認証の意味ないよなあ。もしかしたら Internal、つまりGoogle Cloudで許可したユーザしか通過させないようにも設定できるのかもしれないけどあまり深堀りしてない。
IAPを通過した情報はCookieでアプリ側に渡される。つまりCookieがある前提で各種コードが書かれている必要がある
当然自宅用アプリでCookieなんて使ってないので対応するようにフロントエンド側とバックエンド側のCORS設定を変更する必要がある。具体的には以下のような感じ
var axios = Axios.create({
baseURL: this.$store.getters.apiroot,
})
axios.get(endPoint, {
withCredentials: true,
params: {}
}).then(function (response) {
JSON.stringify(response.data)
})
fetch(
this.$store.getters.apiroot + endPoint,
{
credentials: "include",
headers: new Headers({})
}
).then((res) => {
})
func GinCORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
allowedOrigins := map[string]bool{
"https://app.example.com": true,
"http://127.0.0.1": true,
}
if allowedOrigins[origin] {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers",
"Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, Accept, Origin, Cache-Control, X-Requested-With")
c.Header("Access-Control-Allow-Methods",
"POST, HEAD, PATCH, OPTIONS, GET, PUT, DELETE")
}
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
r := gin.Default()
r.Use(GinCORSMiddleware())
ちなみにcookieの中身はCloudflare独自だが、役割としてはJWTとそんなに大差ない。要するに認証したっていうセッション情報があることを示しているだけ。これを使ってアプリ側がどうこうするすべはない。
そもそもこういうセッショントークン的なものをcookieに保存するのは今時のセキュリティ的にどうなのっていう。正直麿のお気に召さんわー。
APIへのアクセスの認証は正直微妙
普通のWebページと同様のアクセス制限をAPIに対しても行うのだが、API投げるときにIdP-OAuth認証をいちいちするわけではないので、別の認証手段をポリシーとして定義する必要がある。
一般的にはサービストークンをリクエスト投げる側、つまりWebアプリ側に埋め込む必要がある。
これも実質的にCloudflare対応のコード改修コストがかかるし、いまどきトークンを平文でコードに埋め込むのはヤバくね的な話(Cloudflareにはシークレット保存ストレージサービス Secret Store はあるが今回は試してない)とか、トークンの漏洩とかローテーションとかrevokeとかどうすんだ問題が出てきてしまう。イケてない。
さらに(後述)
APIの呼び出し方にはトークン埋め込みに対応できないパターンもある
何かしらのライブラリ内からAPIを呼び出すような場合はそもそもトークンを埋め込むことすらできない。私の例で具体的に言うと Video.js にビデオのURL(実際にはAPIサーバへのGETアクセス)を与える場合。URLしか渡すことができないので中にトークンを埋め込むこともできないし、後続する .m3u8, .ts, m4s ファイルのGETはURLすらユーザの介入なしに実行されるので、ハッシュキー埋め込みもできない。
対応方法としては、特定のエンドポイントやパターンに対してCloudflare Accessをバイパスするポリシーをセットする。
これも微妙なんだよね。例えば完全に介入できる環境であれば最初のGETのパラメータに何かしらの識別子(例えばトークンのハッシュとか)をセットしてAPIサーバ側で検証することもできるはずだけどCloudflareがマネージドサービスで提供しているものなのでそういう細やかなことには対応できないし。ゼロトラストのはずなのに除外ルールを作るというのも麿のお気に召さんー。
CORS/401地獄
で、諸々くぐり抜けようと何かするたびにCORS Errorに遭遇して右往左往することになる。自前サーバでもそこそこ困惑するのだが相手がCloudflareなので何が原因でrejectされてるのかわからず、この辺のトラブルシュートはかなり手探り感があり正直疲弊する。私の場合はChataGPTパイセンにめっちゃ助けてもらった。正直パイセンがいなかったら挫折してたかもしれん・・・。
ということでやってはみたものの実際にできたものは微妙でした
1日いろいろ苦労してなんとか動かしたけど、正直出来としては微妙だなー・・・ってのがお気持ちです。
微妙だなってのは以下のようなことです。
- オンプレアプリをゼロ改修で出せるわけではない
- 認証認可が微妙
- ここまで苦労してCloudflare Accessで包む必要なくね?
さいごに
とはいえもうVPNの世界からは足を洗いたいので、何かいい方法を思いついたら続編書きます・・・