mTLS Everywhere: APISIX에서 TLS를 구성하는 방법
July 31, 2023
간단히 살펴보는 TLS
TLS는 다음과 같은 기능을 제공합니다:
- 서버 인증: 클라이언트가 데이터를 교환하는 서버가 올바른 서버임을 확신할 수 있습니다. 이는 기밀 데이터를 잘못된 대상에게 보내는 것을 방지합니다.
- 선택적 클라이언트 인증: 반대로, 서버는 신원이 확인된 클라이언트만 허용합니다.
- 기밀성: 제3자는 클라이언트와 서버 간에 교환된 데이터를 읽을 수 없습니다.
- 무결성: 제3자는 데이터를 변조할 수 없습니다.
TLS는 인증서를 통해 작동합니다. 인증서는 ID와 유사하며, 인증서 소유자의 신원을 증명합니다. ID와 마찬가지로, 이를 발급한 주체를 신뢰해야 합니다. 신뢰는 체인을 통해 확립됩니다: 내가 Alice를 신뢰하고, Alice가 Bob을 신뢰하며, Bob이 Charlie를 신뢰하고, Charlie가 인증서를 발급했다면, 나는 후자를 신뢰합니다. 이 시나리오에서 Alice는 루트 인증 기관으로 알려져 있습니다.
TLS 인증은 공개 키 암호화를 기반으로 합니다. Alice는 공개 키/개인 키 쌍을 생성하고 공개 키를 공개합니다. 공개 키로 데이터를 암호화하면, 해당 공개 키를 생성한 개인 키만이 이를 복호화할 수 있습니다. 다른 사용법은 개인 키로 데이터를 암호화하고, 공개 키를 가진 모든 사람이 이를 복호화하여 신원을 증명하는 것입니다.
마지막으로, 상호 TLS, 즉 mTLS는 양방향 TLS의 구성입니다: 서버가 클라이언트에게 인증하는 것은 물론, 클라이언트도 서버에게 인증합니다.
이제 개념을 충분히 이해했으니, 실제로 적용해 보겠습니다.
cert-manager로 인증서 생성하기
기본적으로 몇 개의 루트 인증 기관이 브라우저에 설치되어 있습니다. 이것이 우리가 HTTPS 웹사이트를 안전하게 탐색할 수 있는 이유이며, https://apache.org가 그들이 주장하는 사이트임을 신뢰할 수 있습니다. 인프라는 사전 설치된 인증서가 없으므로, 처음부터 시작해야 합니다.
최소한 하나의 루트 인증서가 필요합니다. 이 루트 인증서는 다른 모든 인증서를 생성할 것입니다. 모든 것을 수동으로 할 수도 있지만, Kubernetes에서 cert-manager를 사용할 것입니다. 이름에서 알 수 있듯이, 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는 HTTP 엔드포인트를 통해 관리 API를 제공합니다. 마지막으로, 게이트웨이는 클라이언트의 호출을 업스트림으로 전달합니다. 다음은 아키텍처와 필요한 인증서의 개요입니다:
기초적인 구성 요소인 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
이름, 아래 참조 - 이전에 생성한 네임스페이스 발급자 참조
- etcd의 클라이언트로서 Apache APISIX용 인증서
- 클라이언트의 필수 속성
위 매니페스트를 적용한 후, 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
객체를 생성했지만, 그것들이 무엇인지 설명하지 않았습니다. 실제로, 이들은 cert-manager가 제공하는 간단한 Kubernetes CRD입니다. 내부적으로 cert-manager는 Certificate
에서 Kubernetes Secret
을 생성합니다. 전체 라이프사이클을 관리하므로, 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
Certificate
로 생성된 Secret
은 세 가지 속성을 제공합니다:
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-----
etcd와 APISIX 간의 mTLS 구성
인증서가 준비되었으니, 이제 etcd와 APISIX 간의 상호 TLS를 구성할 수 있습니다. 먼저 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는 환경 변수를 통해 구성을 허용하지 않습니다. 일반
config.yaml
파일을 반영하는ConfigMap
을 사용해야 합니다. - etcd에 대한 클라이언트 인증 구성
- Admin API에 대한 서버 인증 구성
- 일반 HTTPS 포트
- Admin HTTPS 포트
- 서버 인증을 위한 인증서
- 클라이언트 인증을 위한 인증서
- 두 세트의 인증서가 사용됩니다. 하나는 Admin API 및 일반 HTTPS에 대한 서버 인증용이고, 다른 하나는 etcd에 대한 클라이언트 인증용입니다.
이 시점에서 위 매니페스트를 적용하고 두 포드가 통신하는 것을 볼 수 있습니다. 연결 시 Apache APISIX는 HTTPS를 통해 apisix-client
인증서를 전송합니다. 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
로 변환하지만, 쿼리는 Kubernetes 클러스터 내부의admin
으로 전송되어 올바른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 엔드포인트를 확인하세요.
마지막으로, 예상대로 작동하는지 확인할 수 있습니다:
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 관리에 대한 질문이나 문의 사항이 있으면 문의하기를 통해 연락주세요.
더 알아보기: