mTLS повсюду: Как настроить TLS для APISIX
July 31, 2023
TLS вкратце
TLS предоставляет несколько возможностей:
- Аутентификация сервера: клиент уверен, что сервер, с которым он обменивается данными, является правильным. Это позволяет избежать отправки данных, которые могут быть конфиденциальными, неправильному участнику.
- Опциональная аутентификация клиента: сервер разрешает доступ только тем клиентам, чья личность может быть подтверждена.
- Конфиденциальность: третьи стороны не могут прочитать данные, передаваемые между клиентом и сервером.
- Целостность: третьи стороны не могут подделать данные.
TLS работает с использованием сертификатов. Сертификат похож на удостоверение личности, подтверждающее личность владельца сертификата. Как и с удостоверением личности, необходимо доверять тому, кто его выдал. Доверие устанавливается через цепочку: если я доверяю Алисе, которая доверяет Бобу, который, в свою очередь, доверяет Чарли, выдавшему сертификат, то я доверяю последнему. В этом сценарии Алиса известна как корневой центр сертификации.
Аутентификация TLS основана на криптографии с открытым ключом. Алиса генерирует пару открытого/закрытого ключей и публикует открытый ключ. Если кто-то зашифрует данные с помощью открытого ключа, только закрытый ключ, который сгенерировал открытый ключ, сможет их расшифровать. Другой способ использования — зашифровать данные с помощью закрытого ключа, и любой, у кого есть открытый ключ, сможет их расшифровать, тем самым подтвердив свою личность.
Наконец, взаимный TLS, также известный как mTLS, — это конфигурация двустороннего TLS: аутентификация сервера для клиента, как обычно, но также и наоборот, аутентификация клиента для сервера.
Теперь у нас достаточно понимания концепций, чтобы приступить к практике.
Генерация сертификатов с помощью cert-manager
Несколько корневых центров сертификации установлены в браузерах по умолчанию. Именно так мы можем безопасно просматривать HTTPS-сайты, доверяя, что https://apache.org — это именно тот сайт, за который он себя выдает. В инфраструктуре нет предустановленных сертификатов, поэтому мы должны начать с нуля.
Нам нужен хотя бы один корневой сертификат. В свою очередь, он будет генерировать все остальные сертификаты. Хотя можно сделать все вручную, я буду использовать cert-manager в Kubernetes. Как следует из названия, 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
- Центр сертификации, который генерирует сертификаты на уровне кластера
- Создание пространства имен для нашего демо
- Пространственный корневой сертификат, использующий кластерный центр сертификации. Используется только для создания пространственного центра сертификации
- Пространственный центр сертификации. Используется для создания всех остальных сертификатов в этом посте
После применения предыдущего манифеста мы должны увидеть созданный сертификат:
kubectl get certificate -n tls
NAME READY SECRET AGE selfsigned-ca True root-secret 7s
Инфраструктура сертификатов готова; давайте посмотрим на Apache APISIX.
Краткий обзор архитектуры Apache APISIX
Apache APISIX — это API-шлюз. По умолчанию он хранит свою конфигурацию в etcd, распределенном хранилище ключей-значений — том же, что используется Kubernetes. Обратите внимание, что в реальных сценариях мы должны настроить кластеризацию etcd для повышения отказоустойчивости решения. В этом посте мы ограничимся одним экземпляром etcd. Apache APISIX предоставляет административный API через HTTP-эндпоинты. Наконец, шлюз перенаправляет вызовы от клиента к вышестоящему серверу. Вот обзор архитектуры и необходимых сертификатов:
Начнем с основных компонентов: etcd и Apache APISIX. Нам нужны два сертификата: один для etcd в роли сервера и один для Apache APISIX как клиента etcd.
Настроим сертификаты из нашего пространственного центра сертификации:
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, см. ниже - Ссылка на ранее созданный пространственный центр сертификации
- Сертификат для Apache APISIX как клиента etcd
- Обязательный атрибут для клиентов
После применения вышеуказанного манифеста мы можем перечислить сертификаты в пространстве имен 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, но не объясняли, что они собой представляют. Действительно, это простые Kubernetes CRD, предоставляемые cert-manager. Под капотом cert-manager создает Kubernetes Secret из Certificate. Он управляет всем жизненным циклом, поэтому удаление 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
Secret, созданный Certificate, предоставляет три атрибута:
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-----
Настройка mTLS между etcd и APISIX
Теперь, когда сертификаты доступны, мы можем настроить взаимный TLS между etcd и APISIX. Начнем с 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 не предоставляет конфигурацию через переменные окружения. Нам нужно использовать
ConfigMap, который отражает обычный файлconfig.yaml - Настройка клиентской аутентификации для etcd
- Настройка серверной аутентификации для Admin API
- Обычный HTTPS-порт
- Admin HTTPS-порт
- Сертификаты для серверной аутентификации
- Сертификаты для клиентской аутентификации
- Используются два набора сертификатов: один для серверной аутентификации для Admin API и обычного HTTPS, и один для клиентской аутентификации для etcd.
На этом этапе мы можем применить вышеуказанные манифесты и увидеть, как два пода общаются. При подключении Apache APISIX отправляет свой сертификат apisix-client через HTTPS. Поскольку сертификат подписан центром, которому доверяет 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, но запрос будет отправлен наadminвнутри кластера Kubernetes, используя правильный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 endpoint
Наконец, мы можем проверить, что все работает как ожидалось:
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.
Для дальнейшего изучения: