대부분의 경우 AWS Certificate Manager(ACM) 외에는 아무것도 필요하지 않지만, 고객의 도메인을 ALB로 호스팅하면서 고객으로부터 SSL 인증서를 받을 수 없는 경우에는 우회 방법이 필요하다. 이 글에서는 AWS Application Load Balancer에서 Let's Encrypt SSL을 발급하고 자동화하는 방법을 다룬다!

Let's Encrypt에 SSL 인증서를 요청하면, LE는 web-challenge, route53 등과 같은 몇 가지 검증 방법을 사용한다. 이 시나리오에서는 도메인의 DNS 관리 권한이 나에게 없기 때문에 web-challenge를 사용하기로 결정했다. 나는 애플리케이션만 호스팅하고 SSL을 발급해야 한다.

web-challenge로 SSL을 요청하면, Let's Encrypt는 https://domain.com/.well-known/acme-challenge 경로를 확인하여 검증한다. 이것이 무엇을 의미할까? Let's Encrypt가 포트 80으로 도메인에 요청을 보내 검증을 수행한다는 의미다. Load Balancer 설정을 일부 조정하고 acme-challenge 요청을 특별히 리디렉션하기 위한 또 다른 Target Group이 필요하다.

1단계: 새 EC2 실행 및 새 Target Group 생성

리포지토리 클론

쉬운 관리와 모든 도메인에 대해 동일한 표준을 유지하기 위해 bash 스크립트를 작성했다. 내 GitHub 리포지토리를 확인하거나 /opt/ 경로 아래 acme-challange-server에 리포지토리를 클론하면 된다.

팁: # git clone https://github.com/flightlesstux/alble.git

nginx, Let's Encrypt 설치 및 구성

/.well-known/acme-challenge/* 요청이 들어오면 nginx가 설치된 EC2가 모든 acme-challenge 요청을 처리하고 SSL을 발급해준다.

nginx 설치 및 /.well-known/acme-challenge/ 구성

amazon-linux-extras install nginx1 명령으로 nginx를 쉽게 설치할 수 있다. 설치 후, nginx 루트 설정에 새로운 location 블록을 추가해야 한다. 루트 설정 파일은 /etc/nginx/nginx.conf에 있다.

        location /.well-known/acme-challenge {
           root /opt/alble/certbot-challange;
        }

server_name 값은 모든 도메인에 대해 _;여야 한다. 정적 DNS 이름을 설정할 수 없기 때문이다.

nginx 모듈과 함께 Certbot 설치

먼저 epel 리포지토리에서 certbot을 설치하며, epel을 먼저 설치해야 한다. epel 설치를 위해 amazon-linux-extras install -y epel 명령을 사용하고, 이후 yum install -y nginx certbot python2-certbot-nginx jq dig 명령을 실행하여 사전 요구사항을 처리한다.

acme-challenge 서버용 IAM Role Policy

acme-challenge 서버는 Let's Encrypt SSL을 통해 ACM 및 ALB 작업에 접근하기 위한 IAM Role을 가진다. 아래에서 IAM Policy를 확인할 수 있다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AWSLEPolicy1",
            "Effect": "Allow",
            "Action": [
                "acm:DescribeCertificate",
                "acm:RemoveTagsFromCertificate",
                "acm:GetCertificate",
                "acm:AddTagsToCertificate",
                "acm:ListCertificates",
                "acm:ImportCertificate",
                "acm:ListTagsForCertificate"
            ],
            "Resource": "*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        },
        {
            "Sid": "AWSLEPolicy2",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:RemoveTags",
                "elasticloadbalancing:DescribeTags",
                "elasticloadbalancing:AddTags",
                "elasticloadbalancing:AddListenerCertificates"
            ],
            "Resource": "AWS::ALB::ARN",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        },
        {
            "Sid": "AWSLEPolicy3",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeSSLPolicies",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:DescribeListenerCertificates"
            ],
            "Resource": "*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        },
        {
            "Sid": "AWSLEPolicy4",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:RemoveTags",
                "elasticloadbalancing:DescribeTags",
                "elasticloadbalancing:AddTags"
            ],
            "Resource": "AWS::ALB::ARN",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        },
        {
            "Sid": "AWSLEPolicy5",
            "Effect": "Allow",
            "Action": [
                "elasticloadbalancing:DescribeSSLPolicies",
                "elasticloadbalancing:DescribeListeners",
                "elasticloadbalancing:AddListenerCertificates",
                "elasticloadbalancing:DescribeListenerCertificates"
            ],
            "Resource": "*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "acme-challenge-server-Elastic-IP/32"
                }
            }
        }
    ]
}

Elastic IP를 할당한 후 이 정책을 acme-challenge 서버에 직접 연결해도 보안 문제나 유출이 발생하지 않는다. 로컬 자격 증명을 사용하려는 경우에도 동일한 IAM Policy를 사용할 수 있다.

새 Target Group 생성

EC2 인스턴스 구성 후, acme-challenge라는 이름의 Target Group을 생성하고 acme-challange-server라는 nginx가 설치된 EC2로 요청을 리디렉션하며, acme-challenge-server를 대상으로 등록한다. 모든 것이 문제없이 진행되면 화면은 아래와 같을 것이다.

2단계: Application Load Balancer Listener Rule 생성

/.well-known/acme-challenge/* 요청을 수락하고 acme-challenge Target Group으로 리디렉션하기 위해 새 Listener Rule을 편집 및 생성해야 한다. 이 단계를 완료하면 화면은 아래와 같을 것이다.

이제 ACME에서 Let's Encrypt SSL을 요청하고 문제없이 발급하여 ACM으로 가져오고 AWS ALB에 할당할 준비가 되었다 :) 이 작업은 발급을 위해 세 가지 다른 단계를 포함한다.

  1. Let's Encrypt에서 SSL 요청
  2. 도메인 또는 서브도메인이 AWS ALB CNAME 레코드와 일치하면 요청된 SSL 가져오기
  3. 가져온 SSL 인증서를 프로덕션에서 사용할 AWS ALB에 할당

ACME에서 Let's Encrypt SSL 발급 및 요청하기

git 리포지토리를 클론하면 /opt/alble/renewal-hooks 폴더를 /etc/letsencrypt/ 경로 아래로 이동해야 한다. 그렇지 않으면 ACM에서 인증서를 갱신할 수 없으며, 사용자와 고객의 보안에 실패하게 된다. mv /opt/alble/renewal-hooks /etc/letsencrypt/를 복사하여 붙여넣으면 된다.

git 리포지토리의 README.md 파일을 읽고 변수를 env 파일에 작성하기만 하면 된다. 그 후 AWS Application Load Balancer에서 Let's Encrypt SSL 인증서를 쉽게 실행/관리/생성할 수 있다. SSL 발급을 위해 AWS SSM을 통해 acme-challange-server에 명령을 실행하는 것을 권장한다. 이를 위해 IAM Role에 AmazonEC2RoleforSSM 정책을 추가해야 한다.

테스트를 위해 서브도메인을 사용하기로 결정했지만 상관없다. 루트 도메인도 사용할 수 있다. 내 테스트 도메인은 awsle-1.ercanermis.com과 awsle-2.ercanermis.com이다.

/opt/alble/ 경로에 있을 때 ./create-new-site.sh awsle-1.ercanermis.com과 같은 명령을 실행하면 아래와 같은 출력을 볼 수 있다.

내 ALBLe 스크립트는 도메인 및/또는 서브도메인에 대한 CNAME 레코드를 먼저 확인하고 계속 진행하기 전에 확인한다. 잘못된 도메인/서브도메인을 작성하거나 오타가 발생하면 Slack 알림을 통해 경고한다. 알림은 자동화에서 매우 중요하다. env 파일에서 Slack 알림을 설정할 수 있다. 다음은 이전에 Let's Encrypt SSL을 발급한 경우의 예시다.

프로덕션

내 AWS ALB DNS 이름은 web-application-elb-1302305711.us-east-1.elb.amazonaws.com이며 https://web-app.ercanermis.com을 통해 접근할 수 있다. SSL을 얻기 위해 AWS Certificate Manager를 사용 중이다.

내 테스트 도메인은 https://awsle-1.ercanermis.com과 https://awsle-2.ercanermis.com이며 AWS Application Load Balancer와 함께 Let's Encrypt를 사용 중이다!

https://web-app.ercanermis.com은 Amazon 제공 SSL을 사용 중
https://awsle-1.ercanermis.com은 Let's Encrypt 제공 SSL을 사용 중
https://awsle-2.ercanermis.com은 Let's Encrypt 제공 SSL을 사용 중

AWS ALB SSL 인증서와 ACM은 어떻게 보이는가?

ALB SSL 인증서 스크린샷
Amazon Certificate Manager 스크린샷

이 글이 도움이 되길 바란다! 추신: 갱신 요청을 위한 cron 설정을 잊지 말자.
소스 코드: https://github.com/flightlesstux/alble/