• 作成:

NixOSの自宅サーバにCloudflare TunnelでHTTPリクエストを通す

背景

最近自宅サーバを完全にリプレースして復活させました。色々と自分用のwebサービスをこの上に動かしたいです。

そのためにはHTTPリクエストをインターネットからこのサーバに通す必要があります。

前回の方法

前回は素直にルータのポート転送を使って、 443番ポートへのアクセスを自宅サーバに転送していました。

しかし厳密にグローバルIPアドレスを持っていたわけではないので、 IPアドレスが変わりませんようにと祈りつつCloudflareのDNS設定にレコードを登録していました。

これはうっかり変わって他のところにアクセスが飛んでしまうとかを考えると良くないですよね。オリジンサーバを含めてフルでSSL保護していたのでアクセスが飛んでも通信内容は読むことは出来なかったとは思いますが。

今回の方法

Cloudflare Tunnel を使うことにしました。

これならポートを明示的に開ける必要もないし、 IPアドレスが変わっても自動的に参照先が変わるので楽が出来ます。

Cloudflareの設定をTerraformで管理

なるべく宣言的に管理したいので、 ncaq/infra.ncaq.net: Infrastructure as Code for ncaq.net としてリポジトリを作って、 Claude Codeに命令して、めぼしいリソースをimportさせて、 Terraformベースの管理に概ね移行しました。

Claude CodeがCloudflareのproviderを設定する時に一つ前のメジャーバージョンを指定してしまったので、後から移行する時に二度手間になってしまいました。これに限らずライブラリの追加とかは自分でやるべきですね。

サーバへのIPアドレスのproxyは以下のような設定になりました。

resource "cloudflare_zero_trust_tunnel_cloudflared" "seminar" {
  account_id    = var.account_id
  name          = "seminar"
  config_src    = "local"
  tunnel_secret = base64encode(random_password.seminar.result)
}

resource "random_password" "seminar" {
  length  = 63
  special = false
}

resource "cloudflare_dns_record" "seminar" {
  zone_id = var.zone_id
  name    = "seminar.ncaq.net"
  type    = "CNAME"
  content = "${cloudflare_zero_trust_tunnel_cloudflared.seminar.id}.cfargotunnel.com"
  proxied = true
  ttl     = 1
}

base64エンコードしているので、渡す時は一貫してbase64エンコードする必要があります。パスワードは32byte以上なら良いらしいですが、どうせ自動生成なので63byte生成しました。

なるべく宣言的管理をするための工夫

Nixではcloudflaredパッケージとして提供されています。

なのでまずは以下のコマンドを実行することで自分のCloudflareアカウントにログイン出来ます。

nix run 'nixpkgs#cloudflared' -- tunnel login

このコマンドが出力するURLをブラウザで開いて認証します。

そうするとアカウントに紐づく証明書が~/.cloudflared/cert.pemに保存されます。

Tunnel permissions · Cloudflare Zero Trust docs に書かれているようにcloudflared tunnelにはAccount certificateTunnel credentialの2つの認証情報が必要です。 Account certificateの方はtunnel loginした~/.cloudflared/cert.pemの方に既に保存されています。 Tunnel credentialcloudflared tunnel create <NAME>で作成出来ますが、その場合Terraform側でIDを参照する方法がファイルをコピーしたり入力したりする必要があるので自動化されているとは言い難いです。

よってtunnel自体は上に書いたようにTerraform側で作成して、 DNSレコードで透過的に参照できるようにして、 outputとして認証情報ファイルを生成するようにしました。

output "tunnel_seminar_credentials" {
  value = jsonencode({
    AccountTag   = var.account_id
    TunnelSecret = base64encode(random_password.seminar.result)
    TunnelID     = cloudflare_zero_trust_tunnel_cloudflared.seminar.id
    Endpoint     = ""
  })
  sensitive   = true
  description = "サーバ側にコピーするための認証情報"
}

これでterraform applyした後に以下のコマンドを実行して認証情報ファイルを入手します。

terraform output -raw tunnel_seminar_credentials|tee tunnel-seminar.json

その後tunnel-seminar.jsonファイルをscpコマンドなどでサーバにコピーします。

これによりある程度の宣言的な管理が出来るようになります。 tunnel loginの部分は手動でやる必要があります。ブラウザを立ち上げてログインする必要があり、 Cloudflareお馴染みのロボット排除もあるので今の知識では自動化は難しいです。サーバをふっとばして端末由来のcert.pemがなくならない限りはやり直す必要はないですし、そこまで大変な手間でも無いので良しとしています。

NixOSのサーバにcloudflaredをインストール

ここまで適切に設定したら、後は以下のように設定できます。

{ username, ... }:
{
  # To initialize, run in server:
  # ```
  # nix run 'nixpkgs#cloudflared' -- tunnel login
  # ```
  # copy credentialsFile from terraform client to server.
  services.cloudflared = {
    enable = true;
    certificateFile = "/home/${username}/.cloudflared/cert.pem";
    tunnels.seminar = {
      default = "http_status:404";
      credentialsFile = "/home/${username}/.cloudflared/tunnel-seminar.json";
      ingress = {
        "seminar.ncaq.net" = "http://localhost:80";
      };
    };
  };
}

セットアップしていない間はcloudflaredのsystemdサービスは失敗しますが、インストール後に起動が失敗しているだけなので挙動は問題ありません。むしろセットアップしきるように促しているとも言えるので都合が良いです。

鍵も含めて、 getsops/sops: Simple and flexible tool for managing secrets などでリポジトリに含めたほうがより楽に宣言的な管理が出来るのかもしれません。ただ今回は考えることが多くなるのが嫌だったのと、 GPGを設定してから使う方が良い気がしたので普通にファイルシステムに展開しておくことにしました。毎日新しいサーバにインストールするような状況になったら話は別ですが。

動作確認

sudo nixos-rebuild switch --flake ".#$(hostname)"

で設定を反映したあとに、

sudo nix run 'nixpkgs#python3' -- -c "import http.server,socketserver;socketserver.TCPServer(('',80),http.server.SimpleHTTPRequestHandler).serve_forever()"

で超簡易的なHTTPサーバを立ち上げて、 https://seminar.ncaq.net/ にアクセスして応答を確認しました。

簡易的なHTTPサーバをシャットダウンしてからアクセスするとちゃんとBad Gatewayでエラーになります。

設定PR

以下のPRで設定を実装しました。