mTLS Everywhere: APISIX の TLS 設定方法

Nicolas Fränkel

Nicolas Fränkel

July 31, 2023

Ecosystem

TLSの概要

TLS は以下の機能を提供します:

  • サーバー認証:クライアントがデータを交換するサーバーが正しいものであることを確認します。これにより、機密データを誤った相手に送信することを防ぎます。
  • オプションのクライアント認証:逆に、サーバーは身元が確認できるクライアントのみを許可します。
  • 機密性:クライアントとサーバー間で交換されるデータを第三者が読むことができません。
  • 完全性:第三者がデータを改ざんすることができません。

TLSは証明書を介して動作します。証明書はIDに似ており、証明書の保持者の身元を証明します。IDと同様に、誰がそれを発行したかを信頼する必要があります。信頼はチェーンを通じて確立されます。もし私がAliceを信頼し、AliceがBobを信頼し、BobがCharlieを信頼し、Charlieが証明書を発行した場合、私はその証明書を信頼します。このシナリオでは、Aliceはルート証明機関として知られています。

TLS認証は公開鍵暗号方式に基づいています。Aliceは公開鍵と秘密鍵のペアを生成し、公開鍵を公開します。公開鍵でデータを暗号化すると、その公開鍵を生成した秘密鍵のみがデータを復号化できます。もう一つの使い方は、秘密鍵でデータを暗号化し、公開鍵を持つ誰もがそれを復号化することで、身元を証明することです。

最後に、相互TLS(mTLS)は双方向のTLSの設定です。通常のサーバー認証に加えて、クライアントもサーバーに対して認証を行います。

これで、概念を理解したので、実際に手を動かしてみましょう。

cert-managerを使用した証明書の生成

いくつかのルート証明機関(CA)がブラウザにデフォルトでインストールされています。これにより、https://apache.org が正しいサイトであると信頼して、安全にHTTPSウェブサイトを閲覧できます。インフラストラクチャには事前にインストールされた証明書がないため、ゼロから始める必要があります。

少なくとも1つのルート証明書が必要です。それによって、他のすべての証明書が生成されます。手動で行うことも可能ですが、ここではKubernetesのcert-managerを使用します。その名の通り、cert-managerは証明書を管理するためのソリューションです。

Helmを使用してインストールするのは簡単です:

helm repo add jetstack https://charts.jetstack.io #1 helm install \ cert-manager jetstack/cert-manager \ --namespace cert-manager \ #2 --create-namespace \ #2 --version v1.11.0 \ --set installCRDs=true \ --set prometheus.enabled=false #3
  1. チャートのリポジトリを追加
  2. 専用の名前空間にオブジェクトをインストール
  3. この投稿の範囲では監視しない

以下のコマンドで、すべてが期待通りに動作していることを確認できます:

kubectl get pods -n cert-manager
cert-manager-cainjector-7f694c4c58-fc9bk 1/1 Running 2 (2d1h ago) 7d cert-manager-cc4b776cf-8p2t8 1/1 Running 1 (2d1h ago) 7d cert-manager-webhook-7cd8c769bb-494tl 1/1 Running 1 (2d1h ago) 7d

cert-managerは、HashiCorp Vault、Let's Encryptなど、複数のソースから証明書を署名できます。シンプルにするために:

  • 専用のルート証明書を生成します。つまり、Self-Signedです。
  • 証明書のローテーションは扱いません。

以下の内容から始めます:

apiVersion: cert-manager.io/v1 kind: ClusterIssuer #1 metadata: name: selfsigned-issuer spec: selfSigned: {} --- apiVersion: v1 kind: Namespace metadata: name: tls #2 --- apiVersion: cert-manager.io/v1 kind: Certificate #3 metadata: name: selfsigned-ca namespace: tls spec: isCA: true commonName: selfsigned-ca secretName: root-secret issuerRef: name: selfsigned-issuer kind: ClusterIssuer group: cert-manager.io --- apiVersion: cert-manager.io/v1 kind: Issuer #4 metadata: name: ca-issuer namespace: tls spec: ca: secretName: root-secret
  1. クラスター全体で証明書を生成する証明機関
  2. デモ用の名前空間を作成
  3. クラスター全体のIssuerを使用して、名前空間内のルート証明書を作成。名前空間内のIssuerを作成するためだけに使用
  4. 名前空間内のIssuer。この投稿内の他のすべての証明書を作成するために使用

上記のマニフェストを適用した後、作成した単一の証明書を確認できます:

kubectl get certificate -n tls
NAME READY SECRET AGE selfsigned-ca True root-secret 7s

証明書のインフラストラクチャが準備できたので、Apache APISIXを見てみましょう。

サンプルApache APISIXアーキテクチャの概要

Apache APISIX はAPIゲートウェイです。デフォルトでは、その設定をetcdに保存します。etcdは分散型のキーバリューストアで、Kubernetesでも使用されています。実際のシナリオでは、etcdクラスタリングを設定してソリューションの耐障害性を向上させるべきです。この投稿では、単一のetcdインスタンスに限定します。Apache APISIXは、HTTPエンドポイントを介して管理APIを提供します。最後に、ゲートウェイはクライアントからの呼び出しをアップストリームに転送します。以下はアーキテクチャと必要な証明書の概要です:

Apache APISIX architecture

まず、基盤となるetcdとApache APISIXから始めます。2つの証明書が必要です。1つはetcd用(サーバー役)、もう1つはApache APISIX用(etcdクライアント役)です。

名前空間内のIssuerから証明書を設定しましょう:

apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: etcd-server #1 namespace: tls spec: secretName: etcd-secret #2 isCA: false usages: - client auth #3 - server auth #3 dnsNames: - etcd #4 issuerRef: name: ca-issuer #5 kind: Issuer --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: apisix-client #6 namespace: tls spec: secretName: apisix-client-secret isCA: false usages: - client auth emailAddresses: - apisix@apache.org #7 issuerRef: name: ca-issuer #5 kind: Issuer
  1. etcd用の証明書
  2. Kubernetes Secret 名、以下を参照
  3. この証明書の使用目的
  4. Kubernetes Service 名、以下を参照
  5. 以前に作成した名前空間内のIssuerを参照
  6. etcdのクライアントとしてのApache APISIX用の証明書
  7. クライアントの必須属性

上記のマニフェストを適用した後、tls名前空間内の証明書をリストできます:

kubectl get certificates -n tls
NAME READY SECRET AGE selfsigned-ca True root-secret 8m59s //1 apisix-client True apisix-client-secret 8m22s //2 etcd-server True etcd-secret 8m54s //2
  1. 以前に作成した証明書
  2. selfsigned-caによって署名された新しく作成された証明書

cert-managerの証明書

これまでにCertificateオブジェクトを作成しましたが、それが何であるかを説明していませんでした。実際、これらはcert-managerが提供する単純なKubernetesのCRDです。内部では、cert-managerはCertificateからKubernetesのSecretを作成します。ライフサイクル全体を管理するため、Certificateを削除すると、関連するSecretも削除されます。上記のマニフェストのsecretName属性は、Secretの名前を設定します。

kubectl get secrets -n tls
NAME TYPE DATA AGE apisix-client-secret kubernetes.io/tls 3 35m etcd-secret kubernetes.io/tls 3 35m root-secret kubernetes.io/tls 3 35m

Secretを見てみましょう。例えば、apisix-client-secret

kubectl describe apisix-client-secret -n tls
Name: apisix-client-secret Namespace: tls Labels: controller.cert-manager.io/fao=true Annotations: cert-manager.io/alt-names: cert-manager.io/certificate-name: apisix-client cert-manager.io/common-name: cert-manager.io/ip-sans: cert-manager.io/issuer-group: cert-manager.io/issuer-kind: Issuer cert-manager.io/issuer-name: ca-issuer cert-manager.io/uri-sans: Type: kubernetes.io/tls Data ==== ca.crt: 1099 bytes tls.crt: 1115 bytes tls.key: 1679 bytes

Certificateによって作成されたSecretは、3つの属性を提供します:

  • tls.crt:証明書自体
  • tls.key:秘密鍵
  • ca.crt:証明書チェーン内の署名証明書、つまりroot-secret/tls.crt

KubernetesはSecretの内容をbase64でエンコードします。上記のいずれかをプレーンテキストで取得するには、デコードする必要があります。例えば:

kubectl get secret etcd-secret -n tls -o jsonpath='{ .data.tls\.crt }' | base64
-----BEGIN CERTIFICATE----- MIIDBjCCAe6gAwIBAgIQM3JUR8+R0vuUndjGK/aOgzANBgkqhkiG9w0BAQsFADAY MRYwFAYDVQQDEw1zZWxmc2lnbmVkLWNhMB4XDTIzMDMxNjEwMTYyN1oXDTIzMDYx NDEwMTYyN1owADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMQpMj/0 giDVOjOosSRRKUwTzl1Wo2R9YYAeteOW3fuMiAd+XaBGmRO/+GWZQN1tyRQ3pITM ezBgogYAUUNcuqN/UAsgH/JM58niMjZdjRKn4+it94Nj1e24jFL4ts2snCn7FfKJ 3zRtY9tyS7Agw3tCwtXV68Xpmf3CsfhPmn3rGdWHXyYctzAZhqYfEswN3hxpJZxR YVeb55WgDoPo5npZo3+yYiMtoOimIprcmZ2Ye8Wai9S4QKDafUWlvU5GQ65VVLzH PEdOMwbWcwiLqwUv889TiKiC5cyAD6wJOuPRF0KKxxFnG+lHlg9J2S1i5sC3pqoc i0pEQ+atOOyLMMECAwEAAaNkMGIwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUF BwMBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAU2ZaAdEficKUWPFRjdsKSEX/l gbMwEgYDVR0RAQH/BAgwBoIEZXRjZDANBgkqhkiG9w0BAQsFAAOCAQEABcNvYTm8 ZJe3jUq6f872dpNVulb2UvloTpWxQ8jwXgcrhekSKU6pZ4p9IPwfauHLjceMFJLp t2eDi5fSQ1upeqXOofeyKSYjjyA/aVf1zMI8ReCCQtQuAVYyJWBlNLc3XMMecbcp JLGtd/OAZnKDeYYkUX7cJ2wN6Wl/wGLM2lxsqDhEHEZwvGL0DmsdHw7hzSjdVmxs 0Qgkh4jVbNUKdBok5U9Ivr3P1xDPaD/FqGFyM0ssVOCHxtPxhOUA/m3DSr6klfEF McOfudZE958bChOrJgVrUnY3inR0J335bGQ1luEp5tYwPgyD9dG4MQEDD3oLwp+l +NtTUqz8WVlMxQ== -----END CERTIFICATE-----

etcdとAPISIX間のmTLSの設定

証明書が利用可能になったので、etcdとAPISIX間の相互TLSを設定できます。まず、etcdから始めます:

apiVersion: v1 kind: Pod metadata: name: etcd namespace: tls labels: role: config spec: containers: - name: etcd image: bitnami/etcd:3.5.7 ports: - containerPort: 2379 env: - name: ETCD_TRUSTED_CA_FILE #1 value: /etc/ssl/private/ca.crt - name: ETCD_CERT_FILE #2 value: /etc/ssl/private/tls.crt - name: ETCD_KEY_FILE #3 value: /etc/ssl/private/tls.key - name: ETCD_ROOT_PASSWORD value: whatever - name: ETCD_CLIENT_CERT_AUTH #4 value: "true" - name: ETCD_LISTEN_CLIENT_URLS value: https://0.0.0.0:2379 volumeMounts: - name: ssl mountPath: /etc/ssl/private #5 volumes: - name: ssl secret: secretName: etcd-secret #5
  1. 信頼されたCAを設定
  2. 証明書を設定
  3. 秘密鍵を設定
  4. クライアントに証明書の提示を要求し、相互認証を確保
  5. 以前に生成されたシークレットをコンテナにマウントしてアクセス可能にする

次に、Apache APISIXの設定です:

apiVersion: v1 kind: ConfigMap #1 metadata: name: apisix-config namespace: tls data: config.yaml: >- apisix: ssl: ssl_trusted_certificate: /etc/ssl/certs/ca.crt #2 deployment: etcd: host: - https://etcd:2379 tls: cert: /etc/ssl/certs/tls.crt #2 key: /etc/ssl/certs/tls.key #2 admin: allow_admin: - 0.0.0.0/0 https_admin: true #3 admin_api_mtls: admin_ssl_cert: /etc/ssl/private/tls.crt #3 admin_ssl_cert_key: /etc/ssl/private/tls.key #3 admin_ssl_ca_cert: /etc/ssl/private/ca.crt #3 --- apiVersion: v1 kind: Pod metadata: name: apisix namespace: tls labels: role: gateway spec: containers: - name: apisix image: apache/apisix:3.2.0-debian ports: - containerPort: 9443 #4 - containerPort: 9180 #5 volumeMounts: - name: config #1 mountPath: /usr/local/apisix/conf/config.yaml subPath: config.yaml - name: ssl #6 mountPath: /etc/ssl/private - name: etcd-client #7 mountPath: /etc/ssl/certs volumes: - name: config configMap: name: apisix-config - name: ssl #6,8 secret: secretName: apisix-server-secret - name: etcd-client #7,8 secret: secretName: apisix-client-secret
  1. Apache APISIXは環境変数による設定を提供しないため、通常のconfig.yamlファイルを反映するConfigMapを使用する必要があります
  2. etcdの_クライアント_認証を設定
  3. Admin APIの_サーバー_認証を設定
  4. 通常のHTTPSポート
  5. Admin HTTPSポート
  6. サーバー認証用の証明書
  7. クライアント認証用の証明書
  8. 2つの証明書セットを使用します。1つはAdmin APIと通常のHTTPS用のサーバー認証、もう1つはetcd用のクライアント認証です。

この時点で、上記のマニフェストを適用し、2つのポッドが通信していることを確認できます。接続時に、Apache APISIXはHTTPSを介してapisix-client証明書を送信します。etcdが信頼する機関によって署名された証明書であるため、接続を許可します。

簡潔にするためにServiceの定義は省略しましたが、関連するGitHubリポジトリで確認できます。

NAME READY STATUS RESTARTS AGE apisix 1/1 Running 0 179m etcd 1/1 Running 0 179m

クライアントアクセス

基本的なインフラストラクチャを設定したので、クライアントでアクセスをテストする必要があります。ここでは、信頼できるcurlを使用しますが、証明書を設定できるクライアントであれば何でも動作します(例:httpie)。

最初のステップは、クライアント用の専用の証明書とキーのペアを作成することです:

apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: curl-client namespace: tls spec: secretName: curl-secret isCA: false usages: - client auth emailAddresses: - curl@localhost.dev issuerRef: name: ca-issuer kind: Issuer

curlは証明書ファイルのパスを必要としますが、内容ではなく。zshの魔法を使うことで、この制限を回避できます。=( ... )構文を使用すると、一時ファイルを作成できます。他のシェルを使用している場合は、同等の構文を見つけるか、手動でファイルをダウンロードする必要があります。

既存のすべてのルートを取得するためにAdmin APIにクエリを送信します。このシンプルなコマンドにより、Apache APISIXがetcdに接続し、そこから設定を読み取ることができることを確認できます。

curl --resolve 'admin:32180:127.0.0.1' https://admin:32180/apisix/admin/routes \ #1 --cert =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.tls\.crt }' | base64 -d) \ #2 --key =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.tls\.key }' | base64 -d) \ #2 --cacert =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.ca\.crt }' | base64 -d) \ #2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
  1. --resolveを使用して/etc/hostsファイルを汚染しないようにします。curladminlocalhostに変換しますが、クエリはKubernetesクラスタ内のadminに送信されるため、正しいServiceが使用されます
  2. Secret内の必要なデータを取得し、デコードして一時ファイルとして使用

すべてが正しく動作している場合、結果は以下のようになります:

{"total":0,"list":[]}

まだルートを作成していないため、利用可能なルートはありません。

アップストリームとのTLS

最後に、アップストリームとのTLSを設定する必要があります。以下では、静的コンテンツを返すシンプルなnginxインスタンスを使用します。より複雑なアップストリームの例として使用してください。

最初のステップは、いつものように、アップストリーム用の専用のCertificateを生成することです。すでにいくつか作成したので、その方法は省略します。これをupstream-serverと呼び、そのSecretupstream-secretとします。これを使用してNGINXを保護できます:

apiVersion: v1 kind: ConfigMap #1 metadata: name: nginx-config namespace: tls data: nginx.conf: >- events { worker_connections 1024; } http { server { listen 443 ssl; server_name upstream; ssl_certificate /etc/ssl/private/tls.crt; #2 ssl_certificate_key /etc/ssl/private/tls.key; #2 root /www/data; location / { index index.json; } } } --- apiVersion: v1 kind: Pod metadata: name: upstream namespace: tls labels: role: upstream spec: containers: - name: upstream image: nginx:1.23-alpine ports: - containerPort: 443 volumeMounts: - name: config mountPath: /etc/nginx/nginx.conf #1 subPath: nginx.conf - name: content mountPath: /www/data/index.json #3 subPath: index.json - name: ssl #2 mountPath: /etc/ssl/private volumes: - name: config configMap: name: nginx-config - name: ssl #2 secret: secretName: upstream-secret - name: content #3 configMap: name: nginx-content
  1. NGINXは環境変数による設定を許可しないため、ConfigMapアプローチを使用する必要があります
  2. Certificateによって作成されたキーと証明書のペアを使用
  3. この投稿の範囲では重要ではない静的コンテンツ

次のステップは、Admin APIを使用してルートを作成することです。前のステップですべてを準備したので、APIを使用できます:

curl --resolve 'admin:32180:127.0.0.1' https://admin:32180/apisix/admin/routes/1 \ --cert =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.tls\.crt }' | base64 -d) \ #1 --key =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.tls\.key }' | base64 -d) \ #1 --cacert =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.ca\.crt }' | base64 -d) \ #1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d "{ \"uri\": \"/\", \"upstream\": { \"scheme\": \"https\", #2 \"nodes\": { \"upstream:443\": 1 }, \"tls\": { \"client_cert\": \"$(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.tls\.crt }' | base64 -d)\", #3 \"client_key\": \"$(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.tls\.key }' | base64 -d)\" #3 } } }"
  1. Admin APIのクライアント認証、上記と同様
  2. アップストリームにHTTPSを使用
  3. ルートのキーと証明書のペアを設定。Apache APISIXはデータをetcdに保存し、ルートを呼び出す際にそれらを使用します。または、専用のオブジェクトとしてペアを保持し、新しく作成された参照を使用することもできます(アップストリームと同様)。証明書が必要なルートの数に依存します。詳細については、SSLエンドポイントを確認してください

最後に、期待通りに動作することを確認できます:

curl --resolve 'upstream:32443:127.0.0.1' https://upstream:32443/ \ --cert =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.tls\.crt }' | base64 -d) \ --key =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.tls\.key }' | base64 -d) \ --cacert =(kubectl get secret curl-secret -n tls -o jsonpath='{ .data.ca\.crt }' | base64 -d)

そして、動作します:

{ "hello": "world" }

結論

この投稿では、動作するApache APISIXアーキテクチャを説明し、すべてのコンポーネント間で相互TLSを実装しました:etcdとAPISIX、クライアントとAPISIX、そして最後にクライアントとアップストリームです。これがあなたの助けになることを願っています。

APISIXやAPI管理に関する質問や問い合わせがある場合は、お問い合わせください

さらに進むために:

Tags: