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