NGINX Gateway Fabric cert-manager 연동 – TLS 인증서 자동화
NGINX Gateway Fabric cert-manager 연동을 통해 Gateway에서 사용하는 TLS 인증서의 발급과 갱신을 자동화할 수 있습니다. cert-manager는 Let’s Encrypt와 같은 ACME 기반 인증 기관을 이용해 인증서를 발급하고, 발급된 인증서를 Kubernetes Secret으로 관리합니다.
이번 포스트에서는 NGINX Gateway Fabric과 cert-manager를 연동하여 먼저 Let’s Encrypt Staging 인증서 발급 과정을 검증하고, 이후 Production 인증서로 전환하여 실제 HTTPS 통신까지 확인합니다.
목차
1. NGINX Gateway Fabric cert-manager 연동 개요
1-1. 사전 요구사항
2. 환경/버전 정보
3. cert-manager Staging 인증서 발급 및 적용 확인
4. cert-manager Production 인증서 발급 및 적용 확인
5. 결론
1. NGINX Gateway Fabric cert-manager 연동 개요
cert-manager는 Kubernetes 환경에서 TLS 인증서의 발급과 갱신을 자동화하는 인증서 관리 도구입니다. Gateway API와 연동하면 Gateway에 지정된 ClusterIssuer와 TLS Secret 정보를 기반으로 Certificate를 생성하고 관리할 수 있습니다.
Let’s Encrypt와 같은 ACME 기반 인증 기관에서 인증서를 발급하려면 도메인 소유권을 검증하는 Challenge 과정이 필요합니다. 이번 포스트에서는 대상 도메인의 TCP 80과 특정 HTTP 경로를 이용하는 HTTP-01 Challenge 방식을 사용합니다.
cert-manager의 Gateway API HTTP-01 Solver를 사용하면 검증에 필요한 HTTPRoute와 Solver Pod/Service가 자동으로 생성됩니다. Challenge 요청은 NGINX Gateway Fabric의 HTTP Listener를 통해 Solver로 전달되며, 검증이 완료되면 임시 리소스는 자동으로 제거됩니다.
TLS 인증서 발급이 완료되면 인증서와 Key는 Gateway의 certificateRefs에 지정한 Kubernetes TLS Secret에 저장됩니다. NGINX Gateway Fabric은 해당 Secret을 사용하여 HTTPS 트래픽을 처리하며, 이후 인증서 갱신은 cert-manager가 자동으로 관리합니다.
1-1. 사전 요구사항
이번 구성에서는 Let’s Encrypt HTTP-01 Challenge를 사용하므로 다음 환경이 필요합니다.
- Kubernetes 클러스터에 cert-manager 설치
- cert-manager의 Gateway API 기능 활성화
- 인증서 발급 대상 FQDN의 Public DNS 레코드 구성
- 외부에서 NGINX Gateway Fabric의 TCP 80/443 port 접근 가능 환경
- 공인 IP의 TCP 80/443 port 트래픽을 Gateway LoadBalancer IP로 전달할 수 있는 NAT 등 구성
- Gateway에 HTTP Listener 구성
- HTTP-01 Challenge에 영향을 주는 HTTPS Redirect 미적용
테스트 환경에서는 cert-manager를 사전에 설치하고 Gateway API 기능을 활성화한 상태에서 진행했습니다.
또한 MetalLB를 통해 Gateway의 LoadBalancer Service에 고정된 External IP를 할당하고, 외부 공인 IP로 유입되는 TCP 80/443 트래픽이 해당 IP로 전달되도록 구성했습니다.
2. 환경/버전 정보
| 구성 요소 | 버전 |
|---|---|
| Kubernetes | v1.35.3 |
| NGINX Gateway Fabric | 2.6.7 |
| Gateway API | v1.5.1 |
| cert-manager | v1.21.1 |
3. cert-manager Staging 인증서 발급 및 적용 확인
배포할 Gateway의 LoadBalancer 타입 Service의 External IP 값을 고정하기 위해, cert-nginx-proxy 이름으로 NginxProxy CR을 배포했습니다.
apiVersion: gateway.nginx.org/v1alpha2kind: NginxProxymetadata: name: cert-nginx-proxy namespace: devopssongspec: kubernetes: service: type: LoadBalancer externalTrafficPolicy: Local loadBalancerIP: 172.16.61.176
Staging 인증서를 발급받기 위한 ClusterIssuer 리소스를 클러스터에 배포합니다.
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-staging-certspec: acme: email: devopssong@example.co.kr # ACME 계정에 사용할 이메일 주소 server: https://acme-staging-v02.api.letsencrypt.org/directory # Let's Encrypt Staging ACME 서버 privateKeySecretRef: name: letsencrypt-staging-issuer-account-key # ACME 계정의 개인 키를 저장할 secret 이름 지정 solvers: - http01: gatewayHTTPRoute: parentRefs: # 인증서를 발급할 Gateway 정보 입력 - kind: Gateway name: cert-gateway namespace: devopssong
$ kubectl apply -f cluster-issuer.yaml clusterissuer.cert-manager.io/letsencrypt-staging-cert created
인증서를 발급받을 Gateway 리소스를 클러스터에 배포합니다.
앞서 배포한 ClusterIssuer 리소스의 이름을 annotation에 지정하고, NginxProxy CR도 지정합니다. 또한 http, https listener를 모두 구성하며 https listener의 hostname 값은 인증서를 발급받을 도메인을 명시합니다.
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: cert-gateway namespace: devopssong annotations: cert-manager.io/cluster-issuer: letsencrypt-staging-cert # 앞서 배포한 ClusterIssuer 지정spec: gatewayClassName: nginx infrastructure: parametersRef: # External IP 고정을 위해 생성한 NginxProxy 지정 group: gateway.nginx.org kind: NginxProxy name: cert-nginx-proxy listeners: - name: http port: 80 protocol: HTTP allowedRoutes: namespaces: from: Same - name: https hostname: "ngf-cert.devopssong.site" # 인증서를 발급할 도메인. 해당 도메인을 통해 외부로부터의 Gateway 연결이 되어야 함 port: 443 protocol: HTTPS tls: mode: Terminate certificateRefs: # 발급된 인증서, 개인 키를 저장할 Secret 이름 지정 - kind: Secret name: cert-secret
$ kubectl apply -f gateway.yaml -n devopssonggateway.gateway.networking.k8s.io/cert-gateway created
Gateway를 배포하면 cert-manager가 HTTP-01 Challenge 처리를 위한 Solver Pod와 HTTPRoute를 자동으로 생성합니다. 해당 리소스는 도메인 검증이 완료되면 자동으로 제거됩니다.
$ kubectl get po -n devopssong -wNAME READY STATUS RESTARTS AGEcert-gateway-nginx-5c65c9f7-zwfhp 0/1 Pending 0 1scm-acme-http-solver-rwzqp 0/1 Pending 0 0scm-acme-http-solver-rwzqp 0/1 Pending 0 1scert-gateway-nginx-5c65c9f7-zwfhp 0/1 Init:0/1 0 2scm-acme-http-solver-rwzqp 0/1 ContainerCreating 0 1scm-acme-http-solver-rwzqp 0/1 ContainerCreating 0 2scert-gateway-nginx-5c65c9f7-zwfhp 0/1 Init:0/1 0 3scert-gateway-nginx-5c65c9f7-zwfhp 0/1 PodInitializing 0 5scm-acme-http-solver-rwzqp 1/1 Running 0 4scert-gateway-nginx-5c65c9f7-zwfhp 0/1 Running 0 6scert-gateway-nginx-5c65c9f7-zwfhp 1/1 Running 0 16scm-acme-http-solver-rwzqp 1/1 Terminating 0 32scm-acme-http-solver-rwzqp 1/1 Terminating 0 32scm-acme-http-solver-rwzqp 1/1 Terminating 0 32scm-acme-http-solver-rwzqp 0/1 Completed 0 33scm-acme-http-solver-rwzqp 0/1 Completed 0 34scm-acme-http-solver-rwzqp 0/1 Completed 0 34s---$ kubectl get httproutes.gateway.networking.k8s.io -n devopssong -wNAME HOSTNAMES AGEcm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 0scm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 0scm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 0scm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 32scm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 36scm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 36s
인증서 발급 과정에서는 Certificate, CertificateRequest, Order, Challenge 리소스가 순차적으로 생성되며, 각 리소스의 상태를 통해 발급 진행 상황을 확인할 수 있습니다. Challenge 리소스는 검증 완료 후 자동으로 제거됩니다.
# 인증서 발급 전$ kubectl get certificate,certificaterequest,order,challenge -n devopssong NAME READY SECRET AGEcertificate.cert-manager.io/cert-secret False cert-secret 5sNAME APPROVED DENIED READY ISSUER REQUESTER AGEcertificaterequest.cert-manager.io/cert-secret-1 True False letsencrypt-staging-cert system:serviceaccount:cert-manager:cert-manager 5sNAME STATE AGEorder.acme.cert-manager.io/cert-secret-1-4278311862 pending 5sNAME STATE DOMAIN AGEchallenge.acme.cert-manager.io/cert-secret-1-4278311862-2041454743 pending ngf-cert.devopssong.site 3s---# 인증서 발급 후$ kubectl get certificate,certificaterequest,order,challenge -n devopssongNAME READY SECRET AGEcertificate.cert-manager.io/cert-secret True cert-secret 51sNAME APPROVED DENIED READY ISSUER REQUESTER AGEcertificaterequest.cert-manager.io/cert-secret-1 True True letsencrypt-staging-cert system:serviceaccount:cert-manager:cert-manager 51sNAME STATE AGEorder.acme.cert-manager.io/cert-secret-1-4278311862 valid 51s
인증서 발급이 완료되면, Gateway의 certificateRefs에 지정한 Secret 이름으로 인증서가 생성됩니다.
$ kubectl get secret -n devopssong cert-secret NAME TYPE DATA AGEcert-secret kubernetes.io/tls 2 4m46s
Gateway에 발급된 인증서가 적용된 것을 확인하기 위해, Gateway의 https listener와 연결된 HTTPRoute 리소스를 클러스터에 배포합니다. 백엔드는 기본 NGINX 이미지로 실행되는 Pod로 연결되도록 구성했습니다.
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: cert-https namespace: devopssongspec: parentRefs: - group: gateway.networking.k8s.io kind: Gateway name: cert-gateway sectionName: https hostnames: - ngf-cert.devopssong.site # hostname을 발급된 인증서와 동일하게 구성 rules: - matches: - path: type: PathPrefix value: / backendRefs: - kind: Service name: nginx port: 80
$ kubectl apply -f https-route.yaml httproute.gateway.networking.k8s.io/cert-https created$ kubectl get httproutes.gateway.networking.k8s.io NAME HOSTNAMES AGEcert-https ["ngf-cert.devopssong.site"] 3s
브라우저에서 해당 호스트로 접속하고, 인증서를 확인합니다.
Staging 인증서는 브라우저에서 신뢰되지 않는 테스트용 인증서이므로 인증서 경고가 발생합니다. 다만 이를 통해 cert-manager의 HTTP-01 Challenge를 통한 인증서 발급과 NGINX Gateway Fabric Gateway에 적용이 정상적으로 이루어진 것을 확인할 수 있습니다.

4. cert-manager Production 인증서 발급 및 적용 확인
신뢰할 수 있는 인증서인 Production 인증서를 발급 받기 위한 ClusterIssuer 리소스를 클러스터에 배포합니다.
server 엔드포인트는 Staging URL과 다르며, privateKeySecretRef의 name 값도 기존 값과 중복되지 않도록 변경합니다.
apiVersion: cert-manager.io/v1kind: ClusterIssuermetadata: name: letsencrypt-prod-certspec: acme: email: devopssong@example.co.kr server: https://acme-v02.api.letsencrypt.org/directory # Let's Encrypt Production ACME 서버 privateKeySecretRef: name: letsencrypt-prod-issuer-account-key # 별개의 ACME 계정의 개인 키를 저장할 secret 이름 지정 solvers: - http01: gatewayHTTPRoute: parentRefs: - kind: Gateway name: cert-gateway namespace: devopssong
$ kubectl apply -f cluster-issuer.yaml clusterissuer.cert-manager.io/letsencrypt-prod-cert created
기존 Gateway의 설정을 수정합니다. annotation에 지정된 ClusterIssuer 이름을 Production용으로 변경합니다.certificateRefs의 경우 신규 인증서로 Secret이 갱신되므로, 동일 이름을 사용해도 무방합니다.
apiVersion: gateway.networking.k8s.io/v1kind: Gatewaymetadata: name: cert-gateway namespace: devopssong annotations: cert-manager.io/cluster-issuer: letsencrypt-prod-cert # Production용 cluster issuer로 변경spec: gatewayClassName: nginx infrastructure: parametersRef: group: gateway.nginx.org kind: NginxProxy name: cert-nginx-proxy listeners: - name: http port: 80 protocol: HTTP allowedRoutes: namespaces: from: Same - name: https hostname: "ngf-cert.devopssong.site" port: 443 protocol: HTTPS tls: mode: Terminate certificateRefs: - kind: Secret name: cert-secret
$ kubectl apply -f gateway.yaml -n devopssong gateway.gateway.networking.k8s.io/cert-gateway configured
신규 인증서가 발급되므로, staging 인증서 발급 과정과 동일하게 발급을 위한 임시 리소스가 생성되고, 완료되면 삭제됩니다.
$ kubectl get po -n devopssong -wNAME READY STATUS RESTARTS AGEcert-gateway-nginx-5c65c9f7-zwfhp 1/1 Running 0 55mnginx-7c9f5c7649-ncgg6 1/1 Running 0 8dcm-acme-http-solver-njknm 0/1 Pending 0 0scm-acme-http-solver-njknm 0/1 Pending 0 0scm-acme-http-solver-njknm 0/1 Pending 0 1scm-acme-http-solver-njknm 0/1 ContainerCreating 0 2scm-acme-http-solver-njknm 1/1 Running 0 3scm-acme-http-solver-njknm 1/1 Terminating 0 22scm-acme-http-solver-njknm 1/1 Terminating 0 22scm-acme-http-solver-njknm 1/1 Terminating 0 22scm-acme-http-solver-njknm 0/1 Completed 0 23scm-acme-http-solver-njknm 0/1 Completed 0 23scm-acme-http-solver-njknm 0/1 Completed 0 23s---$ kubectl get httproutes.gateway.networking.k8s.io -n devopssong -wNAME HOSTNAMES AGEcert-https ["ngf-cert.devopssong.site"] 42mcm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 0scm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 0scm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 0scm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 22scm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 24s
동일 Secret 이름을 유지하면 기존 Certificate 리소스가 그대로 재사용되어 Production 인증서 발급 시 revision만 증가하고, 완료 후 이전 revision의 CertificateRequest는 자동으로 정리됩니다.
$ kubectl get certificate,certificaterequest,order,challenge -n devopssong
NAME READY SECRET AGE
certificate.cert-manager.io/cert-secret False cert-secret 55m
NAME APPROVED DENIED READY ISSUER REQUESTER AGE
certificaterequest.cert-manager.io/cert-secret-1 True True letsencrypt-staging-cert system:serviceaccount:cert-manager:cert-manager 55m
certificaterequest.cert-manager.io/cert-secret-2 True False letsencrypt-prod-cert system:serviceaccount:cert-manager:cert-manager 4s
NAME STATE AGE
order.acme.cert-manager.io/cert-secret-1-4278311862 valid 55m
order.acme.cert-manager.io/cert-secret-2-1933551558 pending 3s
NAME STATE DOMAIN AGE
challenge.acme.cert-manager.io/cert-secret-2-1933551558-1367905932 pending ngf-cert.devopssong.site 2s
---
$ kubectl get certificate,certificaterequest,order,challenge -n devopssong
NAME READY SECRET AGE
certificate.cert-manager.io/cert-secret True cert-secret 56m
NAME APPROVED DENIED READY ISSUER REQUESTER AGE
certificaterequest.cert-manager.io/cert-secret-2 True True letsencrypt-prod-cert system:serviceaccount:cert-manager:cert-manager 34s
NAME STATE AGE
order.acme.cert-manager.io/cert-secret-2-1933551558 valid 33s
다시 브라우저로 접속하여 인증서를 확인하면, Let’s Encrypt에서 발급된 신뢰할 수 있는 인증서를 확인할 수 있습니다.

5. 결론
이번 포스트에서는 NGINX Gateway Fabric과 cert-manager를 연동하여 Let’s Encrypt TLS 인증서를 발급하고 적용하는 과정을 확인했습니다. Staging 인증서로 HTTP-01 Challenge 동작을 검증한 뒤 Production 인증서로 전환했으며, 동일한 TLS Secret에 신뢰 가능한 인증서가 적용되는 것을 확인했습니다.
cert-manager를 활용하면 Gateway에서 사용하는 인증서의 발급과 갱신을 자동화할 수 있습니다. 이를 통해 인증서를 수동으로 발급하고 교체하는 작업을 줄이면서 NGINX Gateway Fabric의 HTTPS 환경을 운영할 수 있습니다.
Gateway API에 맞춰서 Ingress에서 전환하기 위해 NGINX Gateway Fabric 도입을 고민 중이시라면 NGINX STORE를 통해 문의하세요.
댓글을 달려면 로그인해야 합니다.