mTLS em Todo Lugar: Como Configurar TLS para o APISIX

Nicolas Fränkel

Nicolas Fränkel

July 31, 2023

Ecosystem

TLS em Poucas Palavras

TLS oferece várias capacidades:

  • Autenticação do servidor: o cliente tem certeza de que o servidor com o qual troca dados é o correto. Isso evita o envio de dados, que podem ser confidenciais, para o ator errado.
  • Autenticação opcional do cliente: por outro lado, o servidor só permite clientes cuja identidade pode ser verificada.
  • Confidencialidade: nenhum terceiro pode ler os dados trocados entre o cliente e o servidor.
  • Integridade: nenhum terceiro pode adulterar os dados.

O TLS funciona por meio de certificados. Um certificado é semelhante a um documento de identidade, provando a identidade do titular do certificado. Assim como um documento de identidade, você precisa confiar em quem o emitiu. A confiança é estabelecida por meio de uma cadeia: se eu confio em Alice, que confia em Bob, que por sua vez confia em Charlie, que emitiu o certificado, então eu confio neste último. Nesse cenário, Alice é conhecida como a autoridade certificadora raiz.

A autenticação TLS é baseada em criptografia de chave pública. Alice gera um par de chaves pública/privada e publica a chave pública. Se alguém criptografar dados com a chave pública, apenas a chave privada que gerou a chave pública pode descriptografá-los. O outro uso é para alguém criptografar dados com a chave privada e todos com a chave pública podem descriptografá-los, provando assim sua identidade.

Finalmente, o TLS mútuo, também conhecido como mTLS, é a configuração do TLS bidirecional: autenticação do servidor para o cliente, como de costume, mas também o contrário, autenticação do cliente para o servidor.

Agora temos entendimento suficiente dos conceitos para colocar a mão na massa.

Gerando Certificados com cert-manager

Algumas autoridades certificadoras raiz são instaladas nos navegadores por padrão. É assim que podemos navegar em sites HTTPS com segurança, confiando que https://apache.org é o site que eles afirmam ser. A infraestrutura não tem certificados pré-instalados, então devemos começar do zero.

Precisamos de pelo menos um certificado raiz. Por sua vez, ele gerará todos os outros certificados. Embora seja possível fazer tudo manualmente, vou confiar no cert-manager no Kubernetes. Como o nome sugere, o cert-manager é uma solução para gerenciar certificados.

Instalá-lo com o Helm é simples:

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. Adiciona o repositório de charts
  2. Instala os objetos em um namespace dedicado
  3. Não monitora, no escopo deste post

Podemos garantir que tudo está funcionando como esperado verificando os 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

O cert-manager pode assinar certificados de várias fontes: HashiCorp Vault, Let's Encrypt, etc. Para manter as coisas simples:

  • Vamos gerar nosso próprio certificado raiz, ou seja, Self-Signed
  • Não vamos lidar com a rotação de certificados

Vamos começar com o seguinte:

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. Autoridade certificadora que gera certificados em todo o cluster
  2. Cria um namespace para nossa demonstração
  3. Certificado raiz no namespace usando o emissor em todo o cluster. Apenas usado para criar um emissor no namespace
  4. Emissor no namespace. Usado para criar todos os outros certificados no post

Após aplicar o manifesto anterior, devemos ser capazes de ver o único certificado que criamos:

kubectl get certificate -n tls
NAME READY SECRET AGE selfsigned-ca True root-secret 7s

A infraestrutura de certificados está pronta; vamos dar uma olhada no Apache APISIX.

Visão Geral Rápida de uma Arquitetura de Exemplo do Apache APISIX

Apache APISIX é um gateway de API. Por padrão, ele armazena sua configuração no etcd, um armazenamento de chave-valor distribuído - o mesmo usado pelo Kubernetes. Observe que, em cenários do mundo real, devemos configurar o clustering do etcd para melhorar a resiliência da solução. Para este post, nos limitaremos a uma única instância do etcd. O Apache APISIX oferece uma API de administração por meio de endpoints HTTP. Finalmente, o gateway encaminha chamadas do cliente para um upstream. Aqui está uma visão geral da arquitetura e dos certificados necessários:

Arquitetura do Apache APISIX

Vamos começar com os blocos fundamentais: etcd e Apache APISIX. Precisamos de dois certificados: um para o etcd, no papel de servidor, e outro para o Apache APISIX, como cliente do etcd.

Vamos configurar os certificados a partir do nosso emissor no namespace:

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. Certificado para o etcd
  2. Nome do Secret do Kubernetes, veja abaixo
  3. Usos para este certificado
  4. Nome do Service do Kubernetes, veja abaixo
  5. Referencia o emissor no namespace criado anteriormente
  6. Certificado para o Apache APISIX como cliente do etcd
  7. Atributo obrigatório para clientes

Após aplicar o manifesto acima, podemos listar os certificados no namespace 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
  1. Certificado criado anteriormente
  2. Certificados recém-criados assinados por selfsigned-ca

Certificados do cert-manager

Até agora, criamos objetos Certificate, mas não explicamos o que eles são. De fato, eles são simples CRDs (Custom Resource Definitions) do Kubernetes fornecidos pelo cert-manager. Nos bastidores, o cert-manager cria um Secret do Kubernetes a partir de um Certificate. Ele gerencia todo o ciclo de vida, então excluir um Certificate exclui o Secret associado. O atributo secretName no manifesto acima define o nome do 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

Vamos dar uma olhada em um Secret, por exemplo, 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

Um Secret criado por um Certificate fornece três atributos:

  • tls.crt: O certificado em si
  • tls.key: A chave privada
  • ca.crt: O certificado de assinatura na cadeia de certificados, ou seja, root-secret/tls.crt

O Kubernetes codifica o conteúdo do Secret em base 64. Para obter qualquer um dos itens acima em texto simples, é necessário decodificá-lo, por exemplo:

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-----

Configurando mTLS Entre etcd e APISIX

Com os certificados disponíveis, podemos agora configurar o TLS mútuo entre etcd e APISIX. Vamos começar com o 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. Define a CA confiável
  2. Define o certificado
  3. Define a chave privada
  4. Exige que os clientes passem seu certificado, garantindo assim a autenticação mútua
  5. Monta o segredo gerado anteriormente no contêiner para acesso

Agora, é a vez do 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
  1. O Apache APISIX não oferece configuração por meio de variáveis de ambiente. Precisamos usar um ConfigMap que espelha o arquivo config.yaml regular
  2. Configura a autenticação cliente para o etcd
  3. Configura a autenticação servidor para a API de administração
  4. Porta HTTPS regular
  5. Porta HTTPS de administração
  6. Certificados para autenticação do servidor
  7. Certificados para autenticação do cliente
  8. Dois conjuntos de certificados são usados, um para autenticação do servidor para a API de administração e HTTPS regular, e outro para autenticação do cliente para o etcd.

Neste ponto, podemos aplicar os manifestos acima e ver os dois pods se comunicando. Ao se conectar, o Apache APISIX envia seu certificado apisix-client via HTTPS. Como uma autoridade assina o certificado que o etcd confia, ele permite a conexão.

Omiti a definição do Service por brevidade, mas você pode verificá-los no repositório GitHub associado.

NAME READY STATUS RESTARTS AGE apisix 1/1 Running 0 179m etcd 1/1 Running 0 179m

Acesso do Cliente

Agora que configuramos a infraestrutura básica, devemos testar o acesso a ela com um cliente. Vamos usar nosso fiel curl, mas qualquer cliente que permita configurar certificados deve funcionar, por exemplo, httpie.

O primeiro passo é criar um par de certificado-chave dedicado para o 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

O curl requer um caminho para o arquivo de certificado em vez do conteúdo. Podemos contornar essa limitação com a mágica do zsh: a sintaxe =( ... ) permite a criação de um arquivo temporário. Se você estiver usando outro shell, precisará encontrar a sintaxe equivalente ou baixar os arquivos manualmente.

Vamos consultar a API de administração para todas as rotas existentes. Este comando simples permite verificar se o Apache APISIX está conectado ao etcd e pode ler sua configuração a partir dele.

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 evita poluir o arquivo /etc/hosts. O curl traduzirá admin para localhost, mas a consulta é enviada para admin dentro do cluster Kubernetes, usando assim o Service correto
  2. Obtém os dados necessários dentro do Secret, decodifica-os e os usa como um arquivo temporário

Se tudo funcionar, e deve funcionar, o resultado deve ser o seguinte:

{"total":0,"list":[]}

Nenhuma rota está disponível até agora porque ainda não criamos nenhuma.

TLS com Upstreams

Por último, mas não menos importante, devemos configurar o TLS para os upstreams. A seguir, vou usar uma instância simples do nginx que responde com conteúdo estático. Use isso como uma ilustração para upstreams mais complexos.

O primeiro passo, como sempre, é gerar um Certificate dedicado para o upstream. Vou pular como fazer isso, pois já criamos alguns. Chamo-o de upstream-server e seu Secret, sem criatividade, upstream-secret. Agora podemos usar o último para proteger o 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
  1. O NGINX não permite configuração por meio de variáveis de ambiente; precisamos usar a abordagem do ConfigMap
  2. Usa o par de chave-certificado criado via Certificate
  3. Algum conteúdo estático sem importância no escopo deste post

O próximo passo é criar a rota com a ajuda da API de administração. Preparamos tudo na etapa anterior; agora podemos usar a 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 } } }"
  1. Autenticação do cliente para a API de administração, como acima
  2. Usa HTTPS para o upstream
  3. Configura o par de chave-certificado para a rota. O Apache APISIX armazena os dados no etcd e os usará quando você chamar a rota. Alternativamente, você pode manter o par como um objeto dedicado e usar a referência recém-criada (assim como para os upstreams). Depende de quantas rotas o certificado precisa. Para mais informações, consulte o endpoint SSL

Finalmente, podemos verificar se funciona como esperado:

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)

E funciona:

{ "hello": "world" }

Conclusão

Neste post, descrevi uma arquitetura funcional do Apache APISIX e implementei o TLS mútuo entre todos os componentes: etcd e APISIX, cliente e APISIX e, finalmente, cliente e upstream. Espero que isso ajude você a alcançar o mesmo.

Entre em contato conosco se tiver alguma dúvida ou consulta sobre APISIX e gerenciamento de API.

Para ir mais longe:

Tags: