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: