쿠버네티스를 처음 써보는 입장에서 백엔드, AI 서버를 배포하면서 했던 가장 큰 고민은 백엔드 서버의 통신에 https를 설정하는 것이었습니다. AI 서버는 ClusterIP로 배포해 백엔드에서만 활용해서 https 설정이 필요없지만 외부에서 별도로 배포하는 프론트엔드와 통신하기 위해서는 백엔드 서버에 https 설정이 필수적이어서 일단 쿠버네티스 공식문서를 따라가기 시작했습니다.
저는 EKS 환경에서 Nginx Ingress Controller로 서비스를 배포했습니다!
kubernetes.github.io/ingress-nginx를 참고했습니다.
[Kubernetes] Ingress #TLS를 따라가는 방법은 다음과 같습니다.
cert.crt
와 key.pem
을 생성합니다. ${HOST}는 https를 설정할 도메인 값으로 바꿔야합니다.openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout key.pem -out cert.crt \
-subj "/CN=${HOST}/O=${HOST}" \
-addext "subjectAltName = DNS:${HOST}"
"""
CN에 도메인을 넣는 건 필수입니다!
생성한 TLS 시크릿이 https-example.foo.com 의 정규화 된 도메인 이름(FQDN)이라고 하는 일반 이름(CN)을 포함하는 인증서에서 온 것인지 확인해야 한다.
[Kubernetes] Ingress #TLS 중..
"""
kubectl create secret tls tls-secret --key key.pem --cert cert.crt -n default -o yaml > secret.yaml
# ingress.yaml
# kubectl apply -f ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backend-ingress
spec:
tls:
- hosts:
- <도메인>
secretName: tls-secret
rules:
- host: <도메인>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 80
인증서는 잘 등록되었으나 이 방법은 애초에 공개키, 비밀키를 만드는 시작부터 잘못됐습니다..
공식문서를 믿어서 아무 생각없이 따라가고 있었는데 정신을 차려보니 어이없는 행동을 하고 있었다는 걸 깨달았습니다.
애초에 HTTPS에 사용하는 TLS 인증서는 인증기관(CA)가 발급해서 안전한 연결임을 보장하는 것인데 Self-Assigned 인증서를 만들어서 안전하다고 주장하는 건 정말 제가 인쇄한 지폐를 가지고 물건을 사려는 것만큼 멍청한 행동이었습니다..
아주 멍청하게도 그럼 인증 키파일만 CA에서 발급받아서 교체해주면 되는 것 아닌가? 생각했습니다. 그래서 클러스터에서 돌고있는 파드에서 exec로 들어가서 발급받을까 하다가 별도의 서버에서 발급받아서 키파일만 교체하자! 생각했습니다.
물론 개별적으로 TLS 인증서를 CA에게 인증받고 설정한다면 안될리 없지만.. 저는 항상 certbot으로 간편하게 설정했기 때문에.. 다른 방법을 찾지 못했습니다.
그래서 certbot으로 시도했는데 DNS에 대한 이해부터 해야겠다고 강력하게 깨달을 수 있었습니다. 도메인의 A 레코드에 별도의 서버 IP를 등록하고 dig로 확인했습니다.
그리고 certbot certonly --standalone -d <도메인>
으로인증서 발급을 시도하면..
에러를 뱉어냅니다. 사실 도메인에 A 레코드 등록 안하고 발급 시도했다가 dns 에러도 뜨고 연결이 안돼서 connection 에러도 떴지만 결론적으로 이 상황을 비유하자면 타 국가에서 원화라고 인증받은 지폐를 한국에서 사용하려는 느낌입니다. 마찬가지로 답이 없죠..?
Let's Encrypt는 HTTP-01 챌린지로 요청한 경로에 대한 HTTP 요청을 처리해야하고 내부 레코드까지 확인 및 검증하여 인증서를 발급하기 때문에 3.x.x.x에서 도메인을 연결해서 받은 인증서를 116.x.x.x 사용할 수는 없는 것이죠.
추가로 위의 에러가 발생한 이유는 저는 애초에 서버를 돌리지 않고 인증서만 발급받을 목적으로 서비스 구동 없이 인증서 발급만 시도했기 때문입니다..
nginx ingress controller를 활용하고 있고 nginx의 기본 기능 중에 하나가 TLS CNI 지원인데 뭔가 방법이 있지 않을까 찾기 시작했고 앞의 삽질이 무색하게 방법이 있었습니다.
ingress-nginx 페이지를 읽던 중 Automated Certificate Management with cert-manager가 눈에 들어왔고 cert-manager로 Let's Encrypt에서 tls 인증서를 받는 과정을 시작했습니다.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
그리고 cert-manager를 쓰려면 Issuer와 Certificates가 필요합니다.
Issuers: cert-manager가 TLS 인증서를 요청하는 방법을 정의
+) Issuers(namespace) vs ClusterIssuers(cluster)Certificates: 요청하려는 인증서의 세부정보 (이메일 등)
# tls-issuer.yaml
# kubectl apply -f tls-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-nginx
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <이메일>
privateKeySecretRef:
name: letsencrypt-nginx
solvers:
- http01:
ingress:
class: nginx
# ingress.yaml
# kubectl apply -f ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-nginx
acme.cert-manager.io/http01-edit-in-place: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
kubernetes.io/tls-acme: "true"
name: backend-ingress
namespace: default
spec:
ingressClassName: "nginx"
tls:
- hosts:
- <도메인>
secretName: tls-secret
rules:
- host: <도메인>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend-svc
port:
number: 80
이렇게 하면 issuers를 기반으로 cert-manager가 작동하여 자동으로 certificates 인증서를 생성하고 이 인증서 내용을 tls-secret이라는 이름으로 secret에 등록합니다.
certificates READY 상태가 True이고 secret이 생성된 것을 확인하면..
주소창에 빨간색이 안 뜨는 게 감격스러웠습니다..
Let's Encrypt에서 발급한 만료기간 3개월짜리 인증서가 잘 등록된 것을 알 수 있습니다!
기존에 nginx.conf 또는 파일로 작성해서 nginx.conf에 포함시켜줬던 proxy, header 등의 설정은 ingress의 annotation으로 설정할 수 있습니다! 아래 문서에서 적절한 annotation을 찾아 값을 주입하면 됩니다.
ingress-nginx annotations 목록
예를 들어 저는 기존에 nginx를 https를 위해 웹서버로 백엔드 WAS 앞에 띄워두었고 proxy_pass로 요청을 전달했습니다. 그리고 쿠키를 활용한 OAuth 로그인을 위해 헤더와 CORS 설정을 추가했습니다. 당시에 작성했던 nginx 설정 파일은 다음과 같습니다.
server {
listen 80;
server_name <도메인> www.<도메인>;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name <도메인> www.<도메인>;
ssl_certificate /etc/letsencrypt/live/<도메인>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<도메인>/privkey.pem;
add_header 'Access-Control-Allow-Origin' 'https://<쿠키_도메인(FE)>';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass_header Set-Cookie;
proxy_pass http://127.0.0.1:8080;
proxy_cookie_domain localhost <쿠키_도메인(FE)>;
proxy_cookie_path / /;
proxy_redirect off;
}
}
위의 설정 파일에서 add_header
와 location
블럭의 설정을 그대로 ingress로 옮겨오면 다음과 같습니다. 쿠키를 위한 설정은 남겨두고 두 경우에 해당하지 않는 annotation은 주석 처리했습니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
# cert-manager.io/cluster-issuer: letsencrypt-nginx
# acme.cert-manager.io/http01-edit-in-place: "true"
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
# kubernetes.io/tls-acme: "true"
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "https://<쿠키_도메인(FE)>"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, POST, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "Content-Type,Authorization,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
nginx.ingress.kubernetes.io/auth-always-set-cookie: "true"
nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none: "true"
nginx.ingress.kubernetes.io/session-cookie-samesite: "None"
nginx.ingress.kubernetes.io/proxy-cookie-domain: "localhost <쿠키_도메인(FE)>"
nginx.ingress.kubernetes.io/proxy-cookie-path: "/ /"
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-hash: "sha1"
nginx.ingress.kubernetes.io/session-cookie-name: "route"
name: <ingress_name>
namespace: default
spec:
...
다른 해야할 일도 DNS와 관련되어있는데 이번에 삽질하면서 TLS 인증서 발급, 등록 과정도 직접 해보며 더 자세하게 알게 되었고 DNS에 대해서도 알게된 부분이 많아서 뜻깊은 삽질이었습니다..!
추가 참고 자료:
NGINX Ingress Controller SSL/TLS 구성 튜토리얼
Kubernetes에서 인증서 관리 자동화