mTLS en todas partes: Cómo configurar TLS para APISIX
July 31, 2023
TLS en pocas palabras
TLS ofrece varias capacidades:
- Autenticación del servidor: el cliente tiene la certeza de que el servidor con el que intercambia datos es el correcto. Evita enviar datos, que podrían ser confidenciales, a un actor incorrecto.
- Autenticación opcional del cliente: de manera inversa, el servidor solo permite clientes cuya identidad pueda ser verificada.
- Confidencialidad: ningún tercero puede leer los datos intercambiados entre el cliente y el servidor.
- Integridad: ningún tercero puede manipular los datos.
TLS funciona a través de certificados. Un certificado es similar a una identificación, que prueba la identidad del titular del certificado. Al igual que una identificación, es necesario confiar en quién lo emitió. La confianza se establece a través de una cadena: si confío en Alice, quien confía en Bob, quien a su vez confía en Charlie, quien emitió el certificado, entonces confío en este último. En este escenario, Alice se conoce como la autoridad certificadora raíz.
La autenticación de TLS se basa en criptografía de clave pública. Alice genera un par de claves pública/privada y publica la clave pública. Si alguien cifra datos con la clave pública, solo la clave privada que generó la clave pública puede descifrarlos. El otro uso es que alguien cifre datos con la clave privada y que cualquiera con la clave pública pueda descifrarlos, demostrando así su identidad.
Finalmente, el TLS mutuo, también conocido como mTLS, es la configuración de TLS bidireccional: autenticación del servidor hacia el cliente, como es habitual, pero también al revés, autenticación del cliente hacia el servidor.
Ahora que tenemos suficiente comprensión de los conceptos, es hora de poner manos a la obra.
Generación de certificados con cert-manager
Algunas autoridades certificadoras raíz están instaladas en los navegadores por defecto. Así es como podemos navegar por sitios HTTPS de manera segura, confiando en que https://apache.org es el sitio que dicen ser. La infraestructura no tiene certificados preinstalados, por lo que debemos comenzar desde cero.
Necesitamos al menos un certificado raíz. A su vez, este generará todos los demás certificados. Aunque es posible hacer todo manualmente, confiaré en cert-manager en Kubernetes. Como su nombre lo indica, cert-manager es una solución para gestionar certificados.
Instalarlo con Helm es sencillo:
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
- Agregar el repositorio de charts
- Instalar los objetos en un espacio de nombres dedicado
- No monitorear, en el alcance de este post
Podemos asegurarnos de que todo funciona como se espera revisando los pods:
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 puede firmar certificados desde múltiples fuentes: HashiCorp Vault, Let's Encrypt, etc. Para mantener las cosas simples:
- Generaremos nuestro propio certificado raíz, es decir,
Self-Signed
. - No manejaremos la rotación de certificados.
Comencemos con lo siguiente:
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
- Autoridad certificadora que genera certificados en todo el clúster.
- Crear un espacio de nombres para nuestra demo.
- Certificado raíz en el espacio de nombres utilizando el emisor en todo el clúster. Solo se usa para crear un emisor en el espacio de nombres.
- Emisor en el espacio de nombres. Se usa para crear todos los demás certificados en este post.
Después de aplicar el manifiesto anterior, deberíamos poder ver el único certificado que creamos:
kubectl get certificate -n tls
NAME READY SECRET AGE
selfsigned-ca True root-secret 7s
La infraestructura de certificados está lista; ahora veamos Apache APISIX.
Vista rápida de una arquitectura de muestra de Apache APISIX
Apache APISIX es una puerta de enlace de API. Por defecto, almacena su configuración en etcd, un almacén de clave-valor distribuido, el mismo que usa Kubernetes. Tenga en cuenta que en escenarios del mundo real, deberíamos configurar un clúster de etcd para mejorar la resiliencia de la solución. Para este post, nos limitaremos a una única instancia de etcd. Apache APISIX ofrece una API de administración a través de endpoints HTTP. Finalmente, la puerta de enlace reenvía las llamadas del cliente a un upstream. Aquí hay una vista general de la arquitectura y los certificados requeridos:
Comencemos con los bloques fundamentales: etcd y Apache APISIX. Necesitamos dos certificados: uno para etcd, en el rol de servidor, y otro para Apache APISIX, como cliente de etcd.
Configuraremos los certificados desde nuestro emisor en el espacio de nombres:
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
- Certificado para etcd.
- Nombre del
Secret
de Kubernetes, ver más abajo. - Usos para este certificado.
- Nombre del
Service
de Kubernetes, ver más abajo. - Referencia al emisor en el espacio de nombres creado anteriormente.
- Certificado para Apache APISIX como cliente de etcd.
- Atributo obligatorio para clientes.
Después de aplicar el manifiesto anterior, podemos listar los certificados en el espacio de nombres 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
- Certificado creado previamente.
- Certificados recién creados firmados por
selfsigned-ca
.
Certificados de cert-manager
Hasta ahora, hemos creado objetos Certificate
, pero no hemos explicado qué son. De hecho, son simples CRDs de Kubernetes proporcionados por cert-manager. Bajo el capó, cert-manager crea un Secret
de Kubernetes a partir de un Certificate
. Gestiona todo el ciclo de vida, por lo que eliminar un Certificate
elimina el Secret
asociado. El atributo secretName
en el manifiesto anterior establece el nombre del 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
Echemos un vistazo a un Secret
, por ejemplo, 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
Un Secret
creado por un Certificate
proporciona tres atributos:
tls.crt
: El certificado en sí.tls.key
: La clave privada.ca.crt
: El certificado de firma en la cadena de certificados, es decir,root-secret/tls.crt
.
Kubernetes codifica el contenido de Secret
en base 64. Para obtener cualquiera de los anteriores en texto plano, se debe decodificar, por ejemplo:
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-----
Configuración de mTLS entre etcd y APISIX
Con los certificados disponibles, ahora podemos configurar el TLS mutuo entre etcd y APISIX. Comencemos con 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
- Establecer la CA de confianza.
- Establecer el certificado.
- Establecer la clave privada.
- Requerir que los clientes pasen su certificado, asegurando así la autenticación mutua.
- Montar el
Secret
generado previamente en el contenedor para su acceso.
Ahora es el turno de 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 no ofrece configuración a través de variables de entorno. Necesitamos usar un
ConfigMap
que refleje el archivoconfig.yaml
regular. - Configurar la autenticación cliente para etcd.
- Configurar la autenticación servidor para la API de administración.
- Puerto HTTPS regular.
- Puerto HTTPS de administración.
- Certificados para la autenticación del servidor.
- Certificados para la autenticación del cliente.
- Se usan dos conjuntos de certificados, uno para la autenticación del servidor para la API de administración y HTTPS regular, y otro para la autenticación del cliente para etcd.
En este punto, podemos aplicar los manifiestos anteriores y ver los dos pods comunicándose. Al conectarse, Apache APISIX envía su certificado apisix-client
a través de HTTPS. Debido a que una autoridad firma el certificado en el que etcd confía, permite la conexión.
He omitido la definición del Service
por brevedad, pero puede revisarlos en el repositorio de GitHub asociado.
NAME READY STATUS RESTARTS AGE
apisix 1/1 Running 0 179m
etcd 1/1 Running 0 179m
Acceso del cliente
Ahora que hemos configurado la infraestructura básica, deberíamos probar el acceso con un cliente. Usaremos nuestro fiel curl
, pero cualquier cliente que permita configurar certificados debería funcionar, por ejemplo, httpie.
El primer paso es crear un par de certificado-clave dedicado para el cliente:
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
requiere una ruta al archivo del certificado en lugar del contenido. Podemos sortear esta limitación a través de la magia de zsh: la sintaxis =( ... )
permite la creación de un archivo temporal. Si está usando otro shell, necesitará encontrar la sintaxis equivalente o descargar los archivos manualmente.
Consultemos la API de administración para todas las rutas existentes. Este simple comando permite verificar que Apache APISIX está conectado a etcd y puede leer su configuración desde allí.
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
evita contaminar el archivo/etc/hosts
.curl
traduciráadmin
alocalhost
, pero la consulta se enviará aadmin
dentro del clúster de Kubernetes, utilizando así elService
correcto.- Obtener los datos requeridos dentro del
Secret
, decodificarlos y usarlos como un archivo temporal.
Si todo funciona, y debería hacerlo, el resultado debería ser el siguiente:
{"total":0,"list":[]}
No hay rutas disponibles hasta ahora porque aún no hemos creado ninguna.
TLS con upstreams
Por último, pero no menos importante, debemos configurar TLS para los upstreams. En lo siguiente, usaré una instancia simple de nginx que responde con contenido estático. Úselo como una ilustración para upstreams más complejos.
El primer paso, como siempre, es generar un Certificate
dedicado para el upstream. Omitiré cómo hacerlo, ya que ya hemos creado algunos. Lo llamaré upstream-server
y su Secret
, sin imaginación, upstream-secret
. Ahora podemos usar este último para asegurar 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 no permite la configuración a través de variables de entorno; necesitamos usar el enfoque de
ConfigMap
. - Usar el par clave-certificado creado a través del
Certificate
. - Algún contenido estático sin importancia en el alcance de este post.
El siguiente paso es crear la ruta con la ayuda de la API de administración. Preparamos todo en el paso anterior; ahora podemos usar la 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
}
}
}"
- Autenticación del cliente para la API de administración, como arriba.
- Usar HTTPS para el upstream.
- Configurar el par clave-certificado para la ruta. Apache APISIX almacena los datos en etcd y los usará cuando llame a la ruta. Alternativamente, puede mantener el par como un objeto dedicado y usar la referencia recién creada (al igual que para los upstreams). Depende de cuántas rutas necesite el certificado. Para más información, consulte el endpoint SSL.
Finalmente, podemos verificar que funciona como se espera:
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)
Y lo hace:
{ "hello": "world" }
Conclusión
En este post, he descrito una arquitectura funcional de Apache APISIX e implementado TLS mutuo entre todos los componentes: etcd y APISIX, cliente y APISIX, y finalmente, cliente y upstream. Espero que esto le ayude a lograr lo mismo.
Contáctenos si tiene alguna pregunta o consulta sobre APISIX y la gestión de API.
Para ir más allá: