mTLS Everywhere: APISIX の TLS 設定方法

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管理に関する質問や問い合わせがある場合は、お問い合わせください

さらに進むために:

Share article link