mTLS überall: So konfigurieren Sie TLS für APISIX
July 31, 2023
TLS in Kürze
TLS bietet mehrere Funktionen:
- Server-Authentifizierung: Der Client ist sicher, dass der Server, mit dem er Daten austauscht, der richtige ist. Es verhindert, dass Daten, die vertraulich sein könnten, an den falschen Akteur gesendet werden.
- Optionale Client-Authentifizierung: Umgekehrt erlaubt der Server nur Clients, deren Identität überprüft werden kann.
- Vertraulichkeit: Kein Dritter kann die zwischen Client und Server ausgetauschten Daten lesen.
- Integrität: Kein Dritter kann die Daten manipulieren.
TLS funktioniert über Zertifikate. Ein Zertifikat ähnelt einem Ausweis, der die Identität des Zertifikatsinhabers beweist. Genau wie bei einem Ausweis muss man dem Aussteller vertrauen. Das Vertrauen wird über eine Kette hergestellt: Wenn ich Alice vertraue, die Bob vertraut, der wiederum Charlie vertraut, der das Zertifikat ausgestellt hat, dann vertraue ich letzterem. In diesem Szenario wird Alice als Root-Zertifizierungsstelle bezeichnet.
Die TLS-Authentifizierung basiert auf Public-Key-Kryptografie. Alice generiert ein Schlüsselpaar aus öffentlichem und privatem Schlüssel und veröffentlicht den öffentlichen Schlüssel. Wenn man Daten mit dem öffentlichen Schlüssel verschlüsselt, kann nur der private Schlüssel, der den öffentlichen Schlüssel erzeugt hat, sie entschlüsseln. Die andere Verwendung besteht darin, dass man Daten mit dem privaten Schlüssel verschlüsselt und jeder mit dem öffentlichen Schlüssel sie entschlüsseln kann, wodurch die Identität bewiesen wird.
Schließlich ist Mutual TLS, auch bekannt als mTLS, die Konfiguration von bidirektionalem TLS: Server-Authentifizierung gegenüber dem Client, wie üblich, aber auch umgekehrt, Client-Authentifizierung gegenüber dem Server.
Wir haben jetzt genug Verständnis der Konzepte, um praktisch zu arbeiten.
Zertifikate mit cert-manager generieren
Einige Root-Zertifizierungsstellen sind standardmäßig in Browsern installiert. So können wir HTTPS-Websites sicher durchsuchen und darauf vertrauen, dass https://apache.org die Website ist, die sie vorgibt zu sein. Die Infrastruktur hat keine vorinstallierten Zertifikate, daher müssen wir von Grund auf beginnen.
Wir benötigen mindestens ein Root-Zertifikat. Dieses wird wiederum alle anderen Zertifikate generieren. Während es möglich ist, alles manuell zu erledigen, werde ich mich auf cert-manager in Kubernetes verlassen. Wie der Name schon sagt, ist cert-manager eine Lösung zur Verwaltung von Zertifikaten.
Die Installation mit Helm ist einfach:
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
- Das Repository der Charts hinzufügen
- Die Objekte in einem dedizierten Namespace installieren
- Kein Monitoring, im Rahmen dieses Beitrags
Wir können sicherstellen, dass alles wie erwartet funktioniert, indem wir uns die Pods ansehen:
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 kann Zertifikate aus verschiedenen Quellen signieren: HashiCorp Vault, Let's Encrypt usw. Um die Dinge einfach zu halten:
- Wir werden unser eigenes Root-Zertifikat generieren, d.h.
Self-Signed
- Wir werden keine Zertifikatsrotation behandeln
Beginnen wir mit Folgendem:
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
- Zertifizierungsstelle, die clusterweit Zertifikate generiert
- Einen Namespace für unsere Demo erstellen
- Namespaced Root-Zertifikat, das den clusterweiten Issuer verwendet. Wird nur verwendet, um einen namespaced Issuer zu erstellen
- Namespaced Issuer. Wird verwendet, um alle anderen Zertifikate in diesem Beitrag zu erstellen
Nachdem wir das vorherige Manifest angewendet haben, sollten wir das einzelne Zertifikat, das wir erstellt haben, sehen können:
kubectl get certificate -n tls
NAME READY SECRET AGE
selfsigned-ca True root-secret 7s
Die Zertifikatsinfrastruktur ist bereit; schauen wir uns Apache APISIX an.
Kurzer Überblick über eine Beispielarchitektur von Apache APISIX
Apache APISIX ist ein API-Gateway. Standardmäßig speichert es seine Konfiguration in etcd, einem verteilten Schlüssel-Wert-Speicher – demselben, der von Kubernetes verwendet wird. Beachten Sie, dass wir in realen Szenarien ein etcd-Clustering einrichten sollten, um die Resilienz der Lösung zu verbessern. In diesem Beitrag beschränken wir uns auf eine einzelne etcd-Instanz. Apache APISIX bietet eine Admin-API über HTTP-Endpunkte. Schließlich leitet das Gateway Aufrufe vom Client an ein Upstream weiter. Hier ist eine Übersicht über die Architektur und die erforderlichen Zertifikate:
Beginnen wir mit den grundlegenden Bausteinen: etcd und Apache APISIX. Wir benötigen zwei Zertifikate: eines für etcd in der Serverrolle und eines für Apache APISIX als etcd-Client.
Richten wir Zertifikate von unserem namespaced Issuer ein:
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
- Zertifikat für etcd
- Kubernetes
Secret
-Name, siehe unten - Verwendungszwecke für dieses Zertifikat
- Kubernetes
Service
-Name, siehe unten - Verweis auf den zuvor erstellten namespaced Issuer
- Zertifikat für Apache APISIX als Client von etcd
- Obligatorisches Attribut für Clients
Nachdem wir das obige Manifest angewendet haben, können wir die Zertifikate im tls
-Namespace auflisten:
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
- Zuvor erstelltes Zertifikat
- Neu erstellte Zertifikate, die von
selfsigned-ca
signiert wurden
Zertifikate von cert-manager
Bisher haben wir Certificate
-Objekte erstellt, aber wir haben nicht erklärt, was sie sind. Tatsächlich sind sie einfache Kubernetes CRDs, die von cert-manager bereitgestellt werden. Unter der Haube erstellt cert-manager ein Kubernetes Secret
aus einem Certificate
. Es verwaltet den gesamten Lebenszyklus, sodass das Löschen eines Certificate
das gebundene Secret
löscht. Das Attribut secretName
im obigen Manifest legt den Secret
-Namen fest.
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
Schauen wir uns ein Secret
an, z.B. 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
Ein Secret
, das von einem Certificate
erstellt wurde, bietet drei Attribute:
tls.crt
: Das Zertifikat selbsttls.key
: Der private Schlüsselca.crt
: Das signierende Zertifikat in der Zertifikatskette, d.h.root-secret/tls.crt
Kubernetes kodiert den Inhalt von Secret
in Base64. Um einen der oben genannten Inhalte im Klartext zu erhalten, muss man ihn dekodieren, z.B.:
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-----
Konfiguration von mTLS zwischen etcd und APISIX
Mit den verfügbaren Zertifikaten können wir nun Mutual TLS zwischen etcd und APISIX konfigurieren. Beginnen wir mit 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
- Die vertrauenswürdige CA setzen
- Das Zertifikat setzen
- Den privaten Schlüssel setzen
- Erfordern, dass Clients ihr Zertifikat vorlegen, wodurch gegenseitige Authentifizierung sichergestellt wird
- Das zuvor generierte Secret im Container für den Zugriff einbinden
Nun ist Apache APISIX an der Reihe:
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 bietet keine Konfiguration über Umgebungsvariablen. Wir müssen einen
ConfigMap
verwenden, der die reguläreconfig.yaml
-Datei widerspiegelt - Client-Authentifizierung für etcd konfigurieren
- Server-Authentifizierung für die Admin-API konfigurieren
- Regulärer HTTPS-Port
- Admin-HTTPS-Port
- Zertifikate für die Server-Authentifizierung
- Zertifikate für die Client-Authentifizierung
- Es werden zwei Sätze von Zertifikaten verwendet, einer für die Server-Authentifizierung für die Admin-API und reguläres HTTPS, und einer für die Client-Authentifizierung für etcd.
An diesem Punkt können wir die obigen Manifeste anwenden und sehen, wie die beiden Pods kommunizieren. Beim Verbinden sendet Apache APISIX sein apisix-client
-Zertifikat über HTTPS. Da das Zertifikat von einer vertrauenswürdigen Stelle signiert wurde, erlaubt etcd die Verbindung.
Ich habe die Service
-Definition der Kürze halber weggelassen, aber Sie können sie im zugehörigen GitHub-Repo überprüfen.
NAME READY STATUS RESTARTS AGE
apisix 1/1 Running 0 179m
etcd 1/1 Running 0 179m
Client-Zugriff
Nachdem wir die grundlegende Infrastruktur eingerichtet haben, sollten wir den Zugriff mit einem Client testen. Wir werden unseren treuen curl
verwenden, aber jeder Client, der die Konfiguration von Zertifikaten ermöglicht, sollte funktionieren, z.B. httpie.
Der erste Schritt besteht darin, ein dediziertes Zertifikat-Schlüssel-Paar für den Client zu erstellen:
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
benötigt einen Pfad zur Zertifikatsdatei anstelle des Inhalts. Wir können diese Einschränkung durch die Magie von zsh umgehen: Die Syntax =( ... )
ermöglicht die Erstellung einer temporären Datei. Wenn Sie eine andere Shell verwenden, müssen Sie die entsprechende Syntax finden oder die Dateien manuell herunterladen.
Lassen Sie uns die Admin-API nach allen vorhandenen Routen abfragen. Dieser einfache Befehl ermöglicht es zu überprüfen, dass Apache APISIX mit etcd verbunden ist und seine Konfiguration von dort lesen kann.
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
vermeidet die Verschmutzung der/etc/hosts
-Datei.curl
übersetztadmin
inlocalhost
, aber die Anfrage wird anadmin
innerhalb des Kubernetes-Clusters gesendet, wodurch der korrekteService
verwendet wird- Die erforderlichen Daten innerhalb des
Secret
abrufen, dekodieren und als temporäre Datei verwenden
Wenn alles funktioniert, und das sollte es, sollte das Ergebnis folgendes sein:
{"total":0,"list":[]}
Es sind noch keine Routen verfügbar, da wir noch keine erstellt haben.
TLS mit Upstreams
Last but not least sollten wir TLS für Upstreams konfigurieren. Im Folgenden werde ich eine einfache nginx-Instanz verwenden, die mit statischem Inhalt antwortet. Verwenden Sie dies als Beispiel für komplexere Upstreams.
Der erste Schritt, wie immer, besteht darin, ein dediziertes Certificate
für den Upstream zu generieren. Ich werde überspringen, wie das geht, da wir bereits einige erstellt haben. Ich nenne es upstream-server
und sein Secret
, wenig einfallsreich, upstream-secret
. Wir können letzteres nun verwenden, um NGINX zu sichern:
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 erlaubt keine Konfiguration über Umgebungsvariablen; wir müssen den
ConfigMap
-Ansatz verwenden - Das Schlüssel-Zertifikat-Paar verwenden, das über das
Certificate
erstellt wurde - Einige statische Inhalte, die im Rahmen dieses Beitrags unwichtig sind
Der nächste Schritt besteht darin, die Route mit Hilfe der Admin-API zu erstellen. Wir haben im vorherigen Schritt alles vorbereitet; jetzt können wir die API verwenden:
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
}
}
}"
- Client-Authentifizierung für die Admin-API, wie oben
- HTTPS für den Upstream verwenden
- Schlüssel-Zertifikat-Paar für die Route konfigurieren. Apache APISIX speichert die Daten in etcd und wird sie verwenden, wenn Sie die Route aufrufen. Alternativ können Sie das Paar als dediziertes Objekt behalten und die neu erstellte Referenz verwenden (genau wie für Upstreams). Es hängt davon ab, wie viele Routen das Zertifikat benötigt. Weitere Informationen finden Sie im SSL-Endpunkt
Schließlich können wir überprüfen, ob es wie erwartet funktioniert:
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)
Und es funktioniert:
{ "hello": "world" }
Fazit
In diesem Beitrag habe ich eine funktionierende Apache APISIX-Architektur beschrieben und Mutual TLS zwischen allen Komponenten implementiert: etcd und APISIX, Client und APISIX und schließlich Client und Upstream. Ich hoffe, es hilft Ihnen, dasselbe zu erreichen.
Kontaktieren Sie uns, wenn Sie Fragen oder Anfragen zu APISIX und API-Management haben.
Weiterführende Informationen: