mTLS em Todo Lugar: Como Configurar TLS para o APISIX
July 31, 2023
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
- Adiciona o repositório de charts
- Instala os objetos em um namespace dedicado
- 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
- Autoridade certificadora que gera certificados em todo o cluster
- Cria um namespace para nossa demonstração
- Certificado raiz no namespace usando o emissor em todo o cluster. Apenas usado para criar um emissor no namespace
- 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:
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
- Certificado para o etcd
- Nome do
Secret
do Kubernetes, veja abaixo - Usos para este certificado
- Nome do
Service
do Kubernetes, veja abaixo - Referencia o emissor no namespace criado anteriormente
- Certificado para o Apache APISIX como cliente do etcd
- 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
- Certificado criado anteriormente
- 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 sitls.key
: A chave privadaca.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
- Define a CA confiável
- Define o certificado
- Define a chave privada
- Exige que os clientes passem seu certificado, garantindo assim a autenticação mútua
- 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
- O Apache APISIX não oferece configuração por meio de variáveis de ambiente. Precisamos usar um
ConfigMap
que espelha o arquivoconfig.yaml
regular - Configura a autenticação cliente para o etcd
- Configura a autenticação servidor para a API de administração
- Porta HTTPS regular
- Porta HTTPS de administração
- Certificados para autenticação do servidor
- Certificados para autenticação do cliente
- 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'
--resolve
evita poluir o arquivo/etc/hosts
. Ocurl
traduziráadmin
paralocalhost
, mas a consulta é enviada paraadmin
dentro do cluster Kubernetes, usando assim oService
correto- 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
- O NGINX não permite configuração por meio de variáveis de ambiente; precisamos usar a abordagem do
ConfigMap
- Usa o par de chave-certificado criado via
Certificate
- 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
}
}
}"
- Autenticação do cliente para a API de administração, como acima
- Usa HTTPS para o upstream
- 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: