mTLS Everywhere: APISIX の TLS 設定方法
July 31, 2023
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
- チャートのリポジトリを追加
- 専用の名前空間にオブジェクトをインストール
- この投稿の範囲では監視しない
以下のコマンドで、すべてが期待通りに動作していることを確認できます:
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
- クラスター全体で証明書を生成する証明機関
- デモ用の名前空間を作成
- クラスター全体のIssuerを使用して、名前空間内のルート証明書を作成。名前空間内のIssuerを作成するためだけに使用
- 名前空間内の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を提供します。最後に、ゲートウェイはクライアントからの呼び出しをアップストリームに転送します。以下はアーキテクチャと必要な証明書の概要です:
まず、基盤となる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
- etcd用の証明書
- Kubernetes
Secret
名、以下を参照 - この証明書の使用目的
- Kubernetes
Service
名、以下を参照 - 以前に作成した名前空間内のIssuerを参照
- etcdのクライアントとしてのApache APISIX用の証明書
- クライアントの必須属性
上記のマニフェストを適用した後、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
- 以前に作成した証明書
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
- 信頼されたCAを設定
- 証明書を設定
- 秘密鍵を設定
- クライアントに証明書の提示を要求し、相互認証を確保
- 以前に生成されたシークレットをコンテナにマウントしてアクセス可能にする
次に、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
- Apache APISIXは環境変数による設定を提供しないため、通常の
config.yaml
ファイルを反映するConfigMap
を使用する必要があります - etcdの_クライアント_認証を設定
- Admin APIの_サーバー_認証を設定
- 通常のHTTPSポート
- Admin HTTPSポート
- サーバー認証用の証明書
- クライアント認証用の証明書
- 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'
--resolve
を使用して/etc/hosts
ファイルを汚染しないようにします。curl
はadmin
をlocalhost
に変換しますが、クエリはKubernetesクラスタ内のadmin
に送信されるため、正しいService
が使用されますSecret
内の必要なデータを取得し、デコードして一時ファイルとして使用
すべてが正しく動作している場合、結果は以下のようになります:
{"total":0,"list":[]}
まだルートを作成していないため、利用可能なルートはありません。
アップストリームとのTLS
最後に、アップストリームとのTLSを設定する必要があります。以下では、静的コンテンツを返すシンプルなnginxインスタンスを使用します。より複雑なアップストリームの例として使用してください。
最初のステップは、いつものように、アップストリーム用の専用のCertificate
を生成することです。すでにいくつか作成したので、その方法は省略します。これをupstream-server
と呼び、そのSecret
をupstream-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
- NGINXは環境変数による設定を許可しないため、
ConfigMap
アプローチを使用する必要があります Certificate
によって作成されたキーと証明書のペアを使用- この投稿の範囲では重要ではない静的コンテンツ
次のステップは、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 } } }"
- Admin APIのクライアント認証、上記と同様
- アップストリームにHTTPSを使用
- ルートのキーと証明書のペアを設定。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管理に関する質問や問い合わせがある場合は、お問い合わせください。
さらに進むために: