Amazon EKS 클러스터를 위한 cert-manager를 통한 인증서 발급 및 TLS 지원

8분 분량
콘텐츠 수준: 중급
0

본 기사에서는 AWS EKS 클러스터 내 cert-manager를 설치하여 Ingress에서 cert-manager로부터 인증서를 가져오고 Secret으로 저장하여, 클러스터에 배포한 웹에 연결한 Route53에 등록한 도메인에 HTTPS를 통해 접근할 수 있는 방법을 설명합니다.

Kubernetes 클러스터에는 인증서 관리를 위한 모듈인 cert-manager[1]가 존재합니다. cert-manager는 사용하는 워크로드에 대한 TLS 인증서를 생성하고 만료되기 전 갱신을 수행하게 됩니다. cert-manager의 Certificate 리소스를 사용하게 되면, Private Key와 인증서가 Kubernetes Secret 오브젝트에 저장됩니다.

인증 과정에서 가장 중요한 오브젝트는 Issuer입니다. Issuer[2]란 인증서에 서명을 할 수 있는 인증기관(CA : Certificate Authority)를 나타내는 리소스입니다. Issuer 및 ClusterIssuer로 구분되는데 이는 리소스의 적용 범위에 대한 차이고, 특정 Namespace로 국한하기 위해서는 Issuer로, 클러스터 전체에 대해 적용하기 위해서는 ClusterIssuer로 선택하여 사용할 수 있습니다.

cert-manager를 통한 인증서 및 Secret 발급 과정에 대해 간단히 설명해보도록 하겠습니다. 본 기사에서는 Issuer를 통한 Issued 프로세스를 확인하기 위해 Issuer의 배포는 과정 중간에 진행됩니다.

Prerequisite

  • cert-manager 설치

    $ helm repo add jetstack https://charts.jetstack.io
    $ helm repo update
    $ helm install cert-manager jetstack/cert-manager —namespace cert-manager —create-namespace —set installCRDs=true
    
  • ingress-nginx 설치

    $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.0/deploy/static/provider/cloud/deploy.yaml
    
  • Target Service, Deployment/Pod 배포

    $ cat application.yaml
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: nginx-service
      namespace: sandbox
    spec:
      selector:
        app: nginx-deployment
      ports:
        - protocol: TCP
          port: 80
          targetPort: 80
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx-deployment
      namespace: sandbox
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
            - name: nginx
              image: public.ecr.aws/nginx/nginx:latest
              ports:
                - name: tcp
                  containerPort: 80
    
  • Public 도메인 준비 인증서 발급을 위하여 public 도메인을 준비합니다.

  • Route53 도메인 연결 및 CLB 연결 ingress-nginx를 설치하면 기본적으로 CLB(Classic Load Balancer)가 생성됩니다. 준비한 public 도메인의 CNAME에 CLB의 DNS name을 연결합니다. 본 실습에서는 Route53에 준비한 도메인을 등록하여 Nameserver를 변경한 상태에서 CNAME을 추가하여 CLB를 연결합니다.

Step 1. Ingress 배포

Ingress를 배포할 때 tls 설정 및 인증 기관인 Issuer를 함께 정의합니다.

$ cat ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-ingress
  namespace: sandbox
  annotations:
    cert-manager.io/issuer: nginx-issuer
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  rules:
  - host: <test-host.com>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
            service:
              name: nginx-service
              port:
                number: 80
  tls:
    - hosts:
      - <test-host.com>
      secretName: nginx-app-tls-secret

이 때, Issuer가 없는 상태임에도 cert-manager.io/issuer로 nginx-issuer를 정의하였고, tls 통신을 위한 정보로 host, secret을 정의하였기에 내부적으로 다음과 같은 작업이 수행됩니다.

Certificate[3] 생성

아직 Issuer(nginx-issuer)가 없는 상태이기 때문에 Issuing 상태 및 Ready:False 상태를 가진 Certificate가 생성됩니다.

$ kubectl get certificate -n sandbox
NAME                        READY   SECRET                      AGE
nginx-app-tls-secret   False   nginx-app-tls-secret   3h11m
$ kubectl get certificate nginx-app-tls-secret -n sandbox -o yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  generation: 1
  name: name: nginx-app-tls-secret
...
spec:
  dnsNames:
  - <test-host.com>
  issuerRef:
    group: cert-manager.io
    kind: Issuer
    name: nginx-issuer
  secretName: nginx-app-tls-secret
  usages:
  - digital signature
  - key encipherment
status:
  conditions:
  - lastTransitionTime: "2024-03-20T05:23:37Z"
    message: Issuing certificate as Secret does not exist
    observedGeneration: 1
    reason: DoesNotExist
    status: "False"
    type: Ready
  - lastTransitionTime: "2024-03-20T05:23:37Z"
    message: Issuing certificate as Secret does not exist
    observedGeneration: 1
    reason: DoesNotExist
    status: "True"
    type: Issuing
  nextPrivateKeySecretName: nginx-app-tls-secret-2hh6m

Secret[7] 생성

기존에 존재하는 Secret이 없기 때문에 임시로 임시 Private Key를 생성합니다. 그 결과 아래와 같이 Secret이 생성됩니다.

$ kubectl get secret -n sandbox
NAME                              TYPE     DATA   AGE
nginx-app-tls-secret-2hh6m   Opaque   1      3h12m
$ kubectl get secret nginx-app-tls-secret-2hh6m -n sandbox -o yaml
apiVersion: v1
items:
- apiVersion: v1
  data:
    tls.key: <Base64 Encoded Private Key>
  kind: Secret
  metadata:
    generateName: acme-nginx-app-tls-secret-
    labels:
      cert-manager.io/next-private-key: "true"
      controller.cert-manager.io/fao: "true"
    name: nginx-app-tls-secret-2hh6m
    namespace: sandbox    ownerReferences:
    - apiVersion: cert-manager.io/v1
      blockOwnerDeletion: true
      controller: true
      kind: Certificate
      name: nginx-app-tls-secret
      uid: 9945bcf5-01bb-4352-8ae4-70817bcaf4e3
    resourceVersion: "238823"
    uid: adf8544b-991c-4c7e-8320-c317c768cae0
  type: Opaque
kind: List
metadata:
  resourceVersion: ""

CertificateRequest[4] 생성

또한 Certificate가 인증받기 위한 요청을 처리하기 위해 CertificateRequest 오브젝트를 자동으로 생성합니다. 이 때 Secret의 Private Key를 통한 PEM encoded 정보가 Request에 포함됩니다.

$ kubectl get certificaterequest -n sandbox
NAMESPACE   NAME                          APPROVED   DENIED   READY   ISSUER                  REQUESTOR                                         AGE
sandbox     nginx-app-tls-secret-1   True                False   nginx-issuer   system:serviceaccount:cert-manager:cert-manager   3h13m
$ kubectl get certificaterequest nginx-app-tls-secret-1 -n sandbox -o yaml
apiVersion: cert-manager.io/v1
kind: CertificateRequest
metadata:
  annotations:
    cert-manager.io/certificate-name: nginx-app-tls-secret
    cert-manager.io/certificate-revision: "1"
    cert-manager.io/private-key-secret-name: nginx-app-tls-secret-2hh6m
...
spec:
  ...
  issuerRef:
    group: cert-manager.io
    kind: Issuer
    name: acme-nginx-app-issuer
  request: <Encoded Request>
status:
  conditions:
  - lastTransitionTime: "2024-03-20T05:23:37Z"
    message: Certificate request has been approved by cert-manager.io
    reason: cert-manager.io
    status: "True"
    type: Approved
  - lastTransitionTime: "2024-03-20T05:23:37Z"
    message: 'Referenced "Issuer" not found: issuer.cert-manager.io "nginx-issuer"
      not found'
    reason: Pending
    status: "False"
    type: Ready

이 역시 아직 Issuer가 존재하지 않기 때문에, Pending 상태로 유지됩니다.

Step 2. Issuer 생성

이제 기존에 생성된 리소스들의 인증 과정을 진행하기 위해 Issuer를 생성합니다.

$ cat issuer.yaml
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: nginx-issuer
  namespace: sandbox
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: <email address>
    privateKeySecretRef:
      name: nginx-app-private-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

인증을 위해 CA 서버로 Let’s Encrypt(https://letsencrypt.org/)를 사용합니다. Let’s Encrypt는 staging/production이 구분되어 있으며, 일정 시간 내 여러번 인증서를 요청하는 경우 일정 시간 내에는 인증서 발급이 제한되므로 테스트시에는 staging 경로를 사용할 수 있도록 합니다.

Issuer가 생성되고 나면 다음과 같은 과정이 진행됩니다.

Order[6] 생성

Order는 CertificateRequest를 처리하기 위해 Issuer가 생성하게 되며 이어 Domain마다 각각 Challenge[5]를 생성합니다.

$ kubectl get order -A --watch
nginx-app-tls-secret-1-310547795           0s
nginx-app-tls-secret-1-310547795   pending   0s
nginx-app-tls-secret-1-310547795   ready     22s
nginx-app-tls-secret-1-310547795   valid     22s

Challenge 생성

$ kubectl get challenge -A --watch
nginx-app-tls-secret-1-310547795-2104847025           <test-host.com>   0s
nginx-app-tls-secret-1-310547795-2104847025   pending   <test-host.com>   2s
nginx-app-tls-secret-1-310547795-2104847025   valid     <test-host.com>   21s
nginx-app-tls-secret-1-310547795-2104847025   valid     <test-host.com>   22s

현재 샘플상으로는 Domain이 1개이기 때문에 하나의 Challenge가 생성되었고, 해당 Challenge는 Let’s Encrypt를 통해 해당 도메인의 소유자가 맞는지 확인하고, 확인이 완료되면 Order 내 인증서 정보를 업데이트하고 Order 역시 상태가 변경됩니다. 이 때, 확인을 위해 생성된 Challenge 오브젝트는 완료 후 자동으로 삭제됩니다.

CertificateRequest 변경

이제 Order 및 Challenge를 통해 도메인이 확인되었기 때문에 인증서를 발급받기 위한 CertificateRequest가 어떻게 변경되었는지 확인해보겠습니다.

$ kubectl get certificaterequest -n sandbox
NAMESPACE   NAME                          APPROVED   DENIED   READY   ISSUER                  REQUESTOR                                         AGE
sandbox     nginx-app-tls-secret-1   True                True    nginx-issuer   system:serviceaccount:cert-manager:cert-manager   3h28m

Approve는 되었으나 Ready가 false인 상태에서, true로 변경된 것을 확인하실 수 있습니다. 상세 정보를 확인해보면 다음과 같습니다.

$ kubectl get certificaterequest nginx-app-tls-secret-1 -n sandbox -o yaml
apiVersion: cert-manager.io/v1
kind: CertificateRequest
metadata:
  annotations:
    cert-manager.io/certificate-name: nginx-app-tls-secret
    cert-manager.io/certificate-revision: "1"
    cert-manager.io/private-key-secret-name: nginx-app-tls-secret-2hh6m
...
spec:
  ...
  issuerRef:
    group: cert-manager.io
    kind: Issuer
    name: acme-nginx-app-issuer
  request: <Encoded Request>
status:
  certificate: <Encoded Certificate>     # 신규 생성
  conditions:
  - lastTransitionTime: "2024-03-20T05:23:37Z"
    message: Certificate request has been approved by cert-manager.io
    reason: cert-manager.io
    status: "True"
    type: Approved  
  - lastTransitionTime: "2024-03-20T08:52:30Z"    # 상태, 시간 등 변경
    message: Certificate fetched from issuer successfully
    reason: Issued
    status: "True"
    type: Ready

Certificate 변경

다른 정보는 모두 동일하나 Status 내 Ready 타입의 값이 Issued로 상태로 변경되었고, 그에 따라 certificate 정보가 추가되었습니다. 이제 인증서가 발급되었습니다.이제 Certificate가 어떻게 변경되었는지 확인해보겠습니다.

$ kubectl get certificate -n sandbox
NAME                        READY   SECRET                      AGE
nginx-app-tls-secret   True   nginx-app-tls-secret   3h28m
$ kubectl get certificate nginx-app-tls-secret -n sandbox -o yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  generation: 1
  name: name: nginx-app-tls-secret
...
spec:
  dnsNames:
  - <test-host.com>
  issuerRef:
    group: cert-manager.io
    kind: Issuer
    name: nginx-issuer
  secretName: nginx-app-tls-secret
  usages:
  - digital signature
  - key encipherment
status:         # 인증서 정보 변경
  conditions:
  - lastTransitionTime: "2024-03-20T08:52:30Z"
    message: Certificate is up to date and has not expired
    observedGeneration: 1
    reason: Ready
    status: "True"
    type: Ready
  notAfter: "2024-06-18T07:52:29Z"
  notBefore: "2024-03-20T07:52:30Z"
  renewalTime: "2024-05-19T07:52:29Z"
  revision: 1  # CertificateRequest revision과 일치

Certificate 역시 false 상태였던 Ready 값이 true로 바뀌고 issued 된 것을 확인하실 수 있습니다. 또한 CertificateRequest의 revision 정보가 포함되어 어떠한 Request를 통해 발급된 Certificate 인지 정보를 확인할 수 있습니다.

Secret 변경

Certificate가 Ready:True로 변경되는 과정에서 Secret 역시 변경되었습니다.

$ kubectl get secret -A --watch
nginx-app-private-key        Opaque   1      0s
nginx-app-tls-secret         kubernetes.io/tls   2      0s
nginx-app-tls-secret-2hh6m   Opaque              1      3h28m
$ kubectl get secret -n sandbox
NAMESPACE       NAME                                 TYPE                 DATA   AGE
sandbox        nginx-app-private-key           Opaque               1      8m8s
sandbox        nginx-app-tls-secret            kubernetes.io/tls    2      7m45s

기존의 nginx-app-els-secret-2hh6m은 임시로 생성되었던 Secret이기 때문에 삭제되었고, 실제 인증서 정보가 포함된 Secret인 nginx-app-tls-secret이 새로 생성된 것을 확인하실 수 있습니다.

$ kubectl get secret nginx-app-tls-secret -n sandbox -o yaml
apiVersion: v1
data:
  tls.crt: <Encoded Certificate>
  tls.key: <Encoded RSA Private Key>
kind: Secret
metadata:
  annotations:
    cert-manager.io/alt-names: <test-domain.com>
    cert-manager.io/certificate-name: nginx-app-tls-secret
    cert-manager.io/common-name: <test-domain.com>
    cert-manager.io/ip-sans: ""
    cert-manager.io/issuer-group: cert-manager.io
    cert-manager.io/issuer-kind: Issuer
    cert-manager.io/issuer-name: nginx-issuer
    cert-manager.io/uri-sans: ""
  creationTimestamp: "2024-03-20T08:52:30Z"
  labels:
    controller.cert-manager.io/fao: "true"
  name: nginx-app-tls-secret
  namespace: acme-ns
  resourceVersion: "273468"
  uid: 48e2ae2d-824a-46c1-a025-24899820b2f6
type: kubernetes.io/tls

이렇게 실제 Secret이 신규 생성되고 나서 Certificate는 Ready:True로 상태가 변경되게 됩니다.

이제 <test-domain.com>에 접속하면 신뢰할 수 있는 사이트라는 표식과 함께 HTTPS를 통해 접속이 가능합니다.

Issuer의 종류에는 본 기사에서 다룬 ACME(Automatic Certificate Management Environment)는 물론 Cluster 내에서 사용하기 위해 Self-Signed가 있으며 다수의 in-tree issuer가 존재하합니다. ACM Private CA를 이용하는 경우를 위해 aws-privateca-issuer Helm chart[9]를 제공하고 있으며 이에 대해 자세한 단계가 설명된 블로그[10]를 참고하실 수 있습니다.

References:

[1] https://cert-manager.io/docs/

[2] https://cert-manager.io/docs/configuration/

[3] https://cert-manager.io/docs/usage/certificate/

[4] https://cert-manager.io/docs/usage/certificaterequest/

[5] https://cert-manager.io/docs/reference/api-docs/#acme.cert-manager.io/v1.Challenge

[6] https://cert-manager.io/docs/reference/api-docs/#acme.cert-manager.io/v1.Order

[7] https://kubernetes.io/docs/concepts/configuration/secret/

[8] https://cert-manager.io/docs/configuration/issuers/

[9] https://github.com/cert-manager/aws-privateca-issuer

[10] https://aws.amazon.com/blogs/security/tls-enabled-kubernetes-clusters-with-acm-private-ca-and-amazon-eks-2/

profile pictureAWS
지원 엔지니어
게시됨 한 달 전356회 조회