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. 환경/버전 정보

구성 요소버전
Kubernetesv1.35.3
NGINX Gateway Fabric2.6.7
Gateway APIv1.5.1
cert-managerv1.21.1

3. cert-manager Staging 인증서 발급 및 적용 확인

배포할 Gateway의 LoadBalancer 타입 Service의 External IP 값을 고정하기 위해, cert-nginx-proxy 이름으로 NginxProxy CR을 배포했습니다.

nginx-proxy.yaml
apiVersion: gateway.nginx.org/v1alpha2
kind: NginxProxy
metadata:
name: cert-nginx-proxy
namespace: devopssong
spec:
kubernetes:
service:
type: LoadBalancer
externalTrafficPolicy: Local
loadBalancerIP: 172.16.61.176

Staging 인증서를 발급받기 위한 ClusterIssuer 리소스를 클러스터에 배포합니다.

cluster-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging-cert
spec:
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 값은 인증서를 발급받을 도메인을 명시합니다.

gateway.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
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 devopssong
gateway.gateway.networking.k8s.io/cert-gateway created

Gateway를 배포하면 cert-manager가 HTTP-01 Challenge 처리를 위한 Solver Pod와 HTTPRoute를 자동으로 생성합니다. 해당 리소스는 도메인 검증이 완료되면 자동으로 제거됩니다.

$ kubectl get po -n devopssong -w
NAME READY STATUS RESTARTS AGE
cert-gateway-nginx-5c65c9f7-zwfhp 0/1 Pending 0 1s
cm-acme-http-solver-rwzqp 0/1 Pending 0 0s
cm-acme-http-solver-rwzqp 0/1 Pending 0 1s
cert-gateway-nginx-5c65c9f7-zwfhp 0/1 Init:0/1 0 2s
cm-acme-http-solver-rwzqp 0/1 ContainerCreating 0 1s
cm-acme-http-solver-rwzqp 0/1 ContainerCreating 0 2s
cert-gateway-nginx-5c65c9f7-zwfhp 0/1 Init:0/1 0 3s
cert-gateway-nginx-5c65c9f7-zwfhp 0/1 PodInitializing 0 5s
cm-acme-http-solver-rwzqp 1/1 Running 0 4s
cert-gateway-nginx-5c65c9f7-zwfhp 0/1 Running 0 6s
cert-gateway-nginx-5c65c9f7-zwfhp 1/1 Running 0 16s
cm-acme-http-solver-rwzqp 1/1 Terminating 0 32s
cm-acme-http-solver-rwzqp 1/1 Terminating 0 32s
cm-acme-http-solver-rwzqp 1/1 Terminating 0 32s
cm-acme-http-solver-rwzqp 0/1 Completed 0 33s
cm-acme-http-solver-rwzqp 0/1 Completed 0 34s
cm-acme-http-solver-rwzqp 0/1 Completed 0 34s
---
$ kubectl get httproutes.gateway.networking.k8s.io -n devopssong -w
NAME HOSTNAMES AGE
cm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 0s
cm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 0s
cm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 0s
cm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 32s
cm-acme-http-solver-jnthm ["ngf-cert.devopssong.site"] 36s
cm-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 AGE
certificate.cert-manager.io/cert-secret False cert-secret 5s
NAME APPROVED DENIED READY ISSUER REQUESTER AGE
certificaterequest.cert-manager.io/cert-secret-1 True False letsencrypt-staging-cert system:serviceaccount:cert-manager:cert-manager 5s
NAME STATE AGE
order.acme.cert-manager.io/cert-secret-1-4278311862 pending 5s
NAME STATE DOMAIN AGE
challenge.acme.cert-manager.io/cert-secret-1-4278311862-2041454743 pending ngf-cert.devopssong.site 3s
---
# 인증서 발급 후
$ kubectl get certificate,certificaterequest,order,challenge -n devopssong
NAME READY SECRET AGE
certificate.cert-manager.io/cert-secret True cert-secret 51s
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 51s
NAME STATE AGE
order.acme.cert-manager.io/cert-secret-1-4278311862 valid 51s

인증서 발급이 완료되면, Gateway의 certificateRefs에 지정한 Secret 이름으로 인증서가 생성됩니다.

$ kubectl get secret -n devopssong cert-secret
NAME TYPE DATA AGE
cert-secret kubernetes.io/tls 2 4m46s

Gateway에 발급된 인증서가 적용된 것을 확인하기 위해, Gateway의 https listener와 연결된 HTTPRoute 리소스를 클러스터에 배포합니다. 백엔드는 기본 NGINX 이미지로 실행되는 Pod로 연결되도록 구성했습니다.

https-route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: cert-https
namespace: devopssong
spec:
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 AGE
cert-https ["ngf-cert.devopssong.site"] 3s

브라우저에서 해당 호스트로 접속하고, 인증서를 확인합니다.

Staging 인증서는 브라우저에서 신뢰되지 않는 테스트용 인증서이므로 인증서 경고가 발생합니다. 다만 이를 통해 cert-manager의 HTTP-01 Challenge를 통한 인증서 발급과 NGINX Gateway Fabric Gateway에 적용이 정상적으로 이루어진 것을 확인할 수 있습니다.

NGINX Gateway Fabric cert-manager 발급 staging 인증서

4. cert-manager Production 인증서 발급 및 적용 확인

신뢰할 수 있는 인증서인 Production 인증서를 발급 받기 위한 ClusterIssuer 리소스를 클러스터에 배포합니다.
server 엔드포인트는 Staging URL과 다르며, privateKeySecretRef의 name 값도 기존 값과 중복되지 않도록 변경합니다.

cluster-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-cert
spec:
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/v1
kind: Gateway
metadata:
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 -w
NAME READY STATUS RESTARTS AGE
cert-gateway-nginx-5c65c9f7-zwfhp 1/1 Running 0 55m
nginx-7c9f5c7649-ncgg6 1/1 Running 0 8d
cm-acme-http-solver-njknm 0/1 Pending 0 0s
cm-acme-http-solver-njknm 0/1 Pending 0 0s
cm-acme-http-solver-njknm 0/1 Pending 0 1s
cm-acme-http-solver-njknm 0/1 ContainerCreating 0 2s
cm-acme-http-solver-njknm 1/1 Running 0 3s
cm-acme-http-solver-njknm 1/1 Terminating 0 22s
cm-acme-http-solver-njknm 1/1 Terminating 0 22s
cm-acme-http-solver-njknm 1/1 Terminating 0 22s
cm-acme-http-solver-njknm 0/1 Completed 0 23s
cm-acme-http-solver-njknm 0/1 Completed 0 23s
cm-acme-http-solver-njknm 0/1 Completed 0 23s
---
$ kubectl get httproutes.gateway.networking.k8s.io -n devopssong -w
NAME HOSTNAMES AGE
cert-https ["ngf-cert.devopssong.site"] 42m
cm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 0s
cm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 0s
cm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 0s
cm-acme-http-solver-cflm6 ["ngf-cert.devopssong.site"] 22s
cm-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에서 발급된 신뢰할 수 있는 인증서를 확인할 수 있습니다.

NGINX Gateway Fabric cert-manager 발급 Production 인증서

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를 통해 문의하세요.

NGINX STORE를 통한 솔루션 도입 및 기술지원 무료 상담 신청

* indicates required