mTLS überall: So konfigurieren Sie TLS für APISIX

Nicolas Fränkel

Nicolas Fränkel

July 31, 2023

Ecosystem

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
  1. Das Repository der Charts hinzufügen
  2. Die Objekte in einem dedizierten Namespace installieren
  3. 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
  1. Zertifizierungsstelle, die clusterweit Zertifikate generiert
  2. Einen Namespace für unsere Demo erstellen
  3. Namespaced Root-Zertifikat, das den clusterweiten Issuer verwendet. Wird nur verwendet, um einen namespaced Issuer zu erstellen
  4. 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:

Apache APISIX-Architektur

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
  1. Zertifikat für etcd
  2. Kubernetes Secret-Name, siehe unten
  3. Verwendungszwecke für dieses Zertifikat
  4. Kubernetes Service-Name, siehe unten
  5. Verweis auf den zuvor erstellten namespaced Issuer
  6. Zertifikat für Apache APISIX als Client von etcd
  7. 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
  1. Zuvor erstelltes Zertifikat
  2. 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 selbst
  • tls.key: Der private Schlüssel
  • ca.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
  1. Die vertrauenswürdige CA setzen
  2. Das Zertifikat setzen
  3. Den privaten Schlüssel setzen
  4. Erfordern, dass Clients ihr Zertifikat vorlegen, wodurch gegenseitige Authentifizierung sichergestellt wird
  5. 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
  1. Apache APISIX bietet keine Konfiguration über Umgebungsvariablen. Wir müssen einen ConfigMap verwenden, der die reguläre config.yaml-Datei widerspiegelt
  2. Client-Authentifizierung für etcd konfigurieren
  3. Server-Authentifizierung für die Admin-API konfigurieren
  4. Regulärer HTTPS-Port
  5. Admin-HTTPS-Port
  6. Zertifikate für die Server-Authentifizierung
  7. Zertifikate für die Client-Authentifizierung
  8. 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'
  1. --resolve vermeidet die Verschmutzung der /etc/hosts-Datei. curl übersetzt admin in localhost, aber die Anfrage wird an admin innerhalb des Kubernetes-Clusters gesendet, wodurch der korrekte Service verwendet wird
  2. 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
  1. NGINX erlaubt keine Konfiguration über Umgebungsvariablen; wir müssen den ConfigMap-Ansatz verwenden
  2. Das Schlüssel-Zertifikat-Paar verwenden, das über das Certificate erstellt wurde
  3. 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
          }
        }
     }"
  1. Client-Authentifizierung für die Admin-API, wie oben
  2. HTTPS für den Upstream verwenden
  3. 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:

Tags: