banner
kindjeff

kindjeff

jike
twitter_id
misskey

要求:ウェブサイトに1万のドメインをバインドし、自動的にHTTPS証明書を生成する

先解決タイトルの問題#

実際のニーズとタイトルにはいくつかの違いがありますが、短いタイトルで完全なニーズを説明するのは難しいです。まずはこのタイトルに関連する問題を解決しましょう。

もし 1 万の既知のドメイン名があり、それらに一括で証明書を発行したい場合:例えば、すでに存在するドメイン名のリストがあるとします(少しグレーな産業のように聞こえます)。その場合、必要なことは次の通りです:

  1. スクリプトを使って一括で DNS 解析を行う
    • 例えば、すべてを IP w.x.y.z に解析します
  2. w.x.y.z でサービスを立ち上げ、HTTP Challengeを行います
    • cert-managerを起動し、1 万のドメイン名を与えて、証明書の発行を開始させます
  3. ゲートウェイが 443 をリッスンし、cert-manager が生成した公開鍵と秘密鍵を取り付けます
    • ゲートウェイの機能によりますが、もしゲートウェイが各ドメイン名を明示的に記載することを要求する場合、スクリプトで自動的に 1 万のルーティング設定を生成すれば良いです

あまり優雅ではないように聞こえますが、「.jpg が使えないわけではない」;結局のところ、これは一度きりのタスクであり、オフラインで完了できるタスクはオフラインで完了する方が安全です。

実際のニーズ#

実際のニーズは:

  • xlog は crossbell チェーンに基づくライティングプラットフォームです
  • ユーザーは jeff.xlog.app のようなカスタムサブドメインを生成できます
  • さらに、ユーザーは自分の任意のドメインを自分のホームページにバインドできます
    • 例えば x.jeff.wtf
  • ユーザーがバインドし、DNS が xlog を指すと、HTTPS 証明書が自動的に発行されます;アクセス時は全て HTTPS です

cert-manager❎#

まずは古い方法である cert-manager を見てみましょう。

上記の考えに従って、非常に直接的なアイデアは次の通りです:

  1. ユーザーが xlog.app パネルでバインドを完了した時に、バインドしたいドメイン名を cert-manager に送信し、証明書の発行を開始させます
  2. 証明書を取得した後、ゲートウェイの設定を変更し、リロードします(使用するゲートウェイによります)
  3. ユーザーがアクセスする時、ゲートウェイはすでに署名された証明書を持っているため、直接 HTTPS 接続を確立します

ビジネス(xlog)インフラ(cert-manager) に少しだけ結合がある以外は、大きな問題はなさそうです。

どこが変ですか?#

変なところは、ユーザーが DNS 解析を変更するこのステップがどこにあるかです。

上記の提案の間違いは発行のタイミングです:

  • ユーザーがまだ解析を完了していない場合、cert-manager は HTTP-Challenge を通じて全く機能しません
  • cert-manager はどのようにドメイン名がすでに解析されたかを知るのでしょうか?
    • 最も簡単な答えは:もし Host: バインドしたいドメイン名 のリクエストが xlog のアドレスに送信された場合、これをユーザーの初回アクセスと見なし、その時点で解析が完了したと考えます
      • (偽造は簡単ですが、私たちは何も失うことはありません)

したがって、発行のタイミングはユーザーがリクエストをするのを待つしかありません。リクエストが到着した後にこれらのことを行うと:

  • ドメイン名を cert-manager に送信して証明書を発行します
  • 証明書が発行された後、ゲートウェイをリロードするように設定を変更します
    • 高可用性のために、ゲートウェイの数が 1 より多い場合は、すべてのゲートウェイがリロードを完了するのを待ちます

これは、ユーザーの最初の数回(またはそれ以上)のリクエストが必ず失敗することを意味します。小さな最適化を行って証明書発行を定期的に試みても、その時間はあまり制御できません。

なぜゲートウェイに証明書の発行を開始させる必要があるのか?#

上記の補足説明として、次のようなシナリオを想像できます:

  1. ユーザーが xlog パネルで自分のドメインをバインドしました
  2. しかし、彼は DNS 解析を変更していませんでした
  3. ある日、彼は突然思い出し、解析を行いました
  4. 解析が有効になった後、彼がウェブサイトにアクセスすると、正常に HTTPS 接続が確立されるべきです

「定期的に証明書を発行しようとする」ような方法を使用すると、大量のリソースが無駄になります;また、これは明らかに攻撃される可能性のある脆弱性です:私はドメイン名をバインドし続けるだけで、解析を行わなければ、サーバーは無限のリソースを浪費することになります。

したがって、発行をトリガーするタイミングは ゲートウェイがこのドメイン名を Host として初めて受け取ったリクエスト でなければなりません。

Traefik#

補足説明:

自動的に HTTPS 証明書を発行するウェブサーバーを考えると、最初に思い浮かぶのは通常 Caddy ですが、私たちが使用しているゲートウェイは Traefik です。その理由は:

  1. Traefik は Kubernetes Ingress Controller をネイティブに持っており、k8s を自然にサポートします
  2. Traefik-Mesh は k8s クラスター内の Service Mesh を非常に簡単に実現できます
  3. k8s ゲートウェイの調査を始めた日からこの記事を書く日まで、Caddy のIngress Controllerは依然として WIP 状態です。k8s クラスターで使用したい場合は、WIP バージョンを使用するか、自分で Ingress Controller を開発する必要があります

時間の逆転を防ぐための補足説明:記事を書く時、Traefik のバージョンは v2.8.3 です

ちょうど Traefik には自動的に証明書を発行する機能があります。そこで、Traefik がこのニーズに合っているかどうかを調査します。

Traefik の自動証明書発行の設定は次のようになっており、有効にすると:

  1. ユーザーがルーティングルール(IngressRoute)を書くと、Traefik は設定の tls.domain または一致するルールの Host 部分を読み取ります
  2. Traefik は IngressRoute に基づいて自動的に証明書の発行や更新を試みます

上記の設定に従って、私たちが行うべきことは:

  1. ユーザーが xlog でドメインをバインドした後、IngressRoute を作成します
  2. ちょっと待って、これはあまり正しくないようです!このロジックは cert-manager に送るのと同じではありませんか?

それでは別の方法を考えましょう:

  • 特定のドメイン名が解析され、Traefik への初回アクセスがあった際に、自動的に IngressRoute を作成して証明書を発行させる Traefik Middleware を作成します

少し不自然で、回り道が多いように感じますが、機能的には完璧なようです;ただし、いくつかの(おそらく)我慢できる小さな欠点があります:

  1. Middleware を作成し、サービスを実行する必要があります
    • ロジックはおそらく:ドメイン名がバインドされているかどうかを確認 -> IngressRoute を作成
    • ゲートウェイの安全な自動証明書発行を希望する場合、このロジックは避けられないはずです
  2. 少なくとも初回のアクセスは依然として失敗または非 HTTPS です

どこが変ですか?#

変なところは、Traefik の自動証明書発行機能はおもちゃのように見えることです:

  • このドキュメントを逐一読まないと気づかないかもしれませんが、Traefik 2.0 は完全に無状態のサービスとして設定されており、複数の Traefik 間で何も共有されません。したがって、証明書を管理するには、単一のポイント(または企業版を購入する必要があります)でなければなりません。
    • この文は TLS / Let's Encrypt / Kubernetes and Let's Encrypt などの見出しの下にはなく、IngressRoute のドキュメントで紹介されています
    • これが Traefik のレート制限が非常に計算しにくい理由でもあり、クラスター内で現在実行されている Traefik の数を知り、確率的に計算する必要があります

なぜ私たちはおかしな解決策にお金を払う必要があるのでしょうか?

Caddy✅️#

最後に、Caddy です。

Caddy には 2 つの自動証明書発行のロジックがあります:

  1. 一つ目は一般的な、明確なドメイン名のパターンです:
    1. まずドメイン名を解析する必要があります
    2. 次に caddy を起動し、設定ファイルにリッスンするドメイン名を記載します
    3. 起動後、caddy はすぐに証明書を発行し、ユーザーがアクセスするとすぐに使用可能になります
  2. 二つ目は必要に応じたパターンです:
    1. 有効にするだけで、ドメイン名が来るたびに発行します

私たちは二つ目が必要で、その欠点は:

  1. 初回のアクセスが遅くなります(証明書を発行する必要があるため)
  2. セキュリティリスクがあり、攻撃の入り口になりやすい
    • そのため、caddy は本番環境では ask 設定を提供することを要求し、HTTP インターフェースに問い合わせて、発行すべきかどうかの応答を得る必要があります
  3. デフォルトでは、caddy(現在の v2.5.2)は各インスタンスにデータの永続化ディレクトリを設定する必要があり、デフォルトのストレージ設定では単一のポイントでなければなりません
    • 幸いにも、複数の caddy インスタンスが同じストレージを読み書きできるようにするためのサードパーティのストレージプラグインがあります

問題 2 を解決するために、ドメイン名を検証するための簡単な HTTP サービスを作成します(解析が完了しているか、バインドされているかなど);問題 3 を解決するために、caddy を自分でコンパイルする必要があります:

# caddy-tlsredis このプラグインはデータをredisに保存し、複数のcaddyインスタンスが共有できるようにします
# ここでパッケージ化されたイメージを直接使用できます https://github.com/sljeff/caddy-tlsredis-docker
xcaddy build --with github.com/gamalan/caddy-tlsredis

そして私たちの Caddyfile は次のようになります

{
        storage redis {
                # ストレージをredisに変更します。ここは空にして、環境変数で設定を上書きできます
                # 詳細は https://github.com/gamalan/caddy-tlsredis を参照
        }

        on_demand_tls {
                # ここは私たちが作成した検証サービスで、各caddyと一緒にデプロイできます
                ask http://localhost:5000/
        }
}

:80, :443 {
        tls {
                # 自動的に必要に応じて発行
                on_demand
        }

        # ここは実際のアップストリームサービスです
        reverse_proxy 127.0.0.1:3000
}

これで、ドメイン名が来るたびに証明書が発行されるようになります;欠点は初回のアクセスが遅くなることです。

最後の小さな最適化#

私たちのニーズを振り返ると、その中に次の項目があります:

  • ユーザーは jeff.xlog.app のようなカスタムサブドメインを生成できます

これはすべてのユーザーがサブドメインを生成することを意味します;もし私たちが各サブドメインに対して個別に証明書を発行すると、少し無駄が多いようです。

より合理的な方法は、*.xlog.app にワイルドカード証明書を発行することです。しかし、ワイルドカード証明書には DNS-challenge(ドメイン全体の所有権を証明する必要があります)が必要ですので、caddy の設定を更新する必要があります:

# xlog.appのDNSサービスプロバイダーのプラグインも追加してコンパイルし、ワイルドカード証明書を発行・更新できるようにします
xcaddy build --with github.com/gamalan/caddy-tlsredis --with github.com/caddy-dns/cloudflare
# Caddyfileの中間にこの部分を追加し、残りは :80, :443 にルーティングします

xlog.app, *.xlog.app {
        tls {
                dns cloudflare {env.CF_API_TOKEN}
        }

        reverse_proxy 127.0.0.1:3000
}

最後に、大成功です。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。