Skip to document
pieroot log
Markdown Preview

OpenStack 환경에서 Cloudflare DNS와 certbot을 활용한 SSL 인증서 자동 발급 및 갱신 방법

Frontmatter

OpenStack 환경에서 Horizon 대시보드에 HTTPS를 적용하기 위해 certbot과 Cloudflare DNS 플러그인을 사용해 SSL 인증서를 자동으로 발급하고, NFS 공유와 HAProxy용 PEM 파일 병합 스크립트를 통해 두 컨트롤러에 동일하게 적용하며, Python 스크립트와 cron을 이용해 90일 만료 주기의 자동 갱신까지 완전 자동화하는 방법을 단계별로 설명한다.

OpenStack 환경에서 Cloudflare DNS와 certbot을 활용한 SSL 인증서 자동 발급 및 갱신 방법
OpenStack 환경에서 Horizon 대시보드에 HTTPS를 적용하려면 SSL 인증서가 필수입니다. 특히 우리 연구실처럼 다중 컨트롤러 환경에서는 인증서 관리가 복잡해질 수 있죠.
이번 포스트에서는 certbot과 Cloudflare DNS 인증을 활용하여 SSL 인증서를 발급받고, 여러 서버에서 공유하며 자동으로 갱신하는 방법을 알아보겠습니다.

🤔 우리 환경은 좀 특이합니다

현재 구축된 OpenStack 환경의 구성은 다음과 같습니다:
구성 요소
설명
컨트롤러 서버
2대 (Active-Active)
로드밸런서
HAProxy + Keepalived (VIP 구성)
웹 대시보드
각 컨트롤러에 Horizon 설치
이런 구성에서는 두 서버 모두에 동일한 SSL 인증서가 있어야 하고, 인증서 갱신 시에도 양쪽에 동시에 적용되어야 합니다.
💡
해결 방법
NFS를 통해 SSL 인증서 파일을 공유하고, certbot의 자동 갱신 기능에 후처리 스크립트를 연결하여 HAProxy용 인증서 파일을 자동으로 생성합니다.

🔐 SSL 인증서 기초 지식

Let's Encrypt란?

Let's Encrypt는 무료로 SSL/TLS 인증서를 발급해주는 비영리 인증 기관(CA)입니다. 자동화된 발급 과정을 통해 누구나 쉽게 HTTPS를 적용할 수 있도록 해줍니다.
특징
설명
가격
완전 무료
유효 기간
90일 (자동 갱신 권장)
발급 방식
ACME 프로토콜 기반 자동화
인증 방식
HTTP-01, DNS-01 등

SSL 인증서 유형

SSL 인증서로 받을 수 있는 도메인 유형은 두 가지가 있습니다:
  • Wildcard SSL: *.pieroot.xyz와 같이 특정 도메인의 모든 서브도메인을 한 번에 인증
📝
언제 어떤 걸 선택해야 할까요?
  • 하나의 서버에서 여러 서브도메인을 호스팅한다면 → Wildcard SSL
  • 각 서버마다 별도의 도메인을 사용한다면 → Standard SSL
사실 설정 방법은 거의 동일하고, 도메인 지정 방식만 다릅니다.

DNS 인증 방식의 장점

certbot은 여러 인증 방식을 지원하지만, DNS-01 Challenge(DNS 인증)를 사용하면 다음과 같은 장점이 있습니다:
  • 웹 서버를 중단하지 않고 인증서 발급 가능
  • 내부망 서버에서도 인증서 발급 가능
  • Wildcard 인증서 발급 가능
  • 80/443 포트가 막혀 있어도 발급 가능

⚙️ 사전 준비: certbot 및 Cloudflare 플러그인 설치

certbot과 Cloudflare DNS 플러그인을 설치합니다. 이 플러그인은 Cloudflare API를 통해 자동으로 DNS TXT 레코드를 추가/삭제하여 도메인 소유권을 인증합니다.
bash
sudo apt update
sudo apt install certbot python3-certbot-dns-cloudflare
💡
왜 Cloudflare인가요?
Cloudflare는 무료 DNS 서비스를 제공하면서 API를 통한 자동화를 지원합니다. certbot의 Cloudflare 플러그인을 사용하면 인증서 발급과 갱신 과정이 완전히 자동화됩니다.

☁️ Cloudflare DNS 설정

1단계: 도메인 등록

Cloudflare 대시보드에서 SSL을 적용할 도메인을 등록합니다.
notion image
  • 유형: A 레코드
  • 이름: 서브도메인 (예: openstack)
  • IPv4 주소: SSL을 연결할 서버의 실제 IP
notion image
위와 같이 설정하면 openstack.dmslab.xyz 도메인이 지정한 IP로 연결됩니다.

2단계: API 토큰 발급

certbot이 DNS 레코드를 자동으로 관리할 수 있도록 API 토큰을 발급받아야 합니다.
Cloudflare 대시보드에서 내 프로필 → API 토큰 → 토큰 생성으로 이동합니다.
notion image
⚠️
토큰 권한 설정
DNS 영역을 편집할 수 있는 권한이 필요합니다. "Zone:DNS:Edit" 권한을 포함한 토큰을 생성하세요.

🔑 certbot으로 SSL 인증서 발급

API 토큰 저장

발급받은 토큰을 certbot이 사용할 수 있도록 파일에 저장합니다:
bash
echo "dns_cloudflare_api_token = <발급받은 API 토큰>" | sudo tee /etc/letsencrypt/cloudflare.ini
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
🔒
보안 주의
API 토큰이 담긴 파일은 반드시 chmod 600으로 권한을 제한하세요. certbot도 권한이 너무 열려 있으면 경고를 표시합니다.

인증서 발급 명령어

다음 명령어로 SSL 인증서를 발급받습니다:
bash
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  --email <Cloudflare 계정 이메일> \
  --agree-tos \
  --no-eff-email \
  -d openstack.dmslab.xyz \
  -v

주요 옵션 설명

옵션
설명
certonly
인증서만 발급 (웹서버 설정 변경 안 함)
--dns-cloudflare
Cloudflare DNS 인증 방식 사용
--dns-cloudflare-credentials
API 토큰 파일 경로
--agree-tos
서비스 약관 동의
-d
인증서를 발급받을 도메인
-v
상세 로그 출력

발급 결과 확인

성공적으로 발급되면 다음과 같은 출력이 나타납니다:
notion image
plain text
Certificate is saved at: /etc/letsencrypt/live/openstack.dmslab.xyz/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/openstack.dmslab.xyz/privkey.pem
✅
발급된 파일 설명
  • fullchain.pem: 서버 인증서 + 중간 인증서 (체인)
  • privkey.pem: 개인 키
  • cert.pem: 서버 인증서만
  • chain.pem: 중간 인증서만

🔄 HAProxy용 인증서 파일 생성

Nginx는 fullchain.pem과 privkey.pem을 별도로 지정하지만, HAProxy는 두 파일을 하나로 합친 PEM 파일을 사용합니다.

인증서 파일 병합

현재 우리는 두 컨트롤러가 동일한 NFS 경로(/mnt/ssl)를 공유하여 SSL 인증서를 함께 사용합니다.
bash
cat /etc/letsencrypt/live/openstack.dmslab.xyz/fullchain.pem \
    /etc/letsencrypt/live/openstack.dmslab.xyz/privkey.pem \
    > /mnt/ssl/openstack.dmslab.xyz.pem
📝
NFS 공유 디렉토리 활용
우리 환경에서는 /mnt/ssl 경로를 NFS로 공유하여 두 컨트롤러가 동일한 인증서 파일을 사용합니다. 이렇게 하면 한 번의 갱신으로 모든 서버에 자동 적용됩니다.

🤖 자동 갱신 설정

Let's Encrypt 인증서는 90일마다 만료되므로 자동 갱신이 필수입니다. certbot은 기본적으로 자동 갱신 기능을 제공하지만, HAProxy용 병합 파일까지 자동으로 생성하려면 추가 설정이 필요합니다.
⚠️
알아두면 좋은 점
certbot은 설치 시 자동으로 /etc/cron.d/certbot 파일을 생성하지만, 간혹 생성되지 않는 경우도 있습니다. 또한 기본 갱신은 fullchain.pem과 privkey.pem만 갱신하므로, HAProxy용 병합 파일은 별도로 처리해야 합니다.

자동 갱신 Python 스크립트

다음 스크립트는 인증서 갱신 후 모든 도메인의 인증서를 HAProxy 형식으로 자동 병합합니다:
python
#!/usr/bin/python3

import os
import subprocess

SRC_DIR = '/etc/letsencrypt/live'
DST_DIR = '/mnt/ssl'

def run_certbot_renew():
    """
    Executes the certbot renew command.
    """
    try:
        result = subprocess.run(
                ['certbot', 'renew'],
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True
                )
        print("[certbot] Renewal succeeded:")
        print(result.stdout)
    except subprocess.CalledProcessError as e:
        print("[certbot] Renewal error:")
        print(e.stderr)

def merge_certificates():
    # Ensure that the destination directory exists
    if not os.path.exists(DST_DIR):
        os.makedirs(DST_DIR)

    for directory in os.listdir(SRC_DIR):
        domain_path = os.path.join(SRC_DIR, directory)
        if os.path.isdir(domain_path):
            print(domain_path)
            fullchain_path = os.path.join(domain_path, 'fullchain.pem')
            privkey_path = os.path.join(domain_path, 'privkey.pem')

            if os.path.exists(fullchain_path) and os.path.exists(privkey_path):
                try:
                    with open(fullchain_path, 'r') as fc_file:
                        fullchain_content = fc_file.read()
                    with open(privkey_path, 'r') as pk_file:
                        privkey_content = pk_file.read()
                    # Combine the contents with a newline in between
                    combined_content = fullchain_content.rstrip() + "\n" + privkey_content.lstrip()
                    # Build the destination file path, for example: /mnt/ssl/example.com.pem
                    dst_file = os.path.join(DST_DIR, f"{directory}.pem")

                    with open(dst_file, 'w') as out_file:
                        out_file.write(combined_content)

                    print(f"Merged certificates for '{directory}' into '{dst_file}'")
                except Exception as e:
                    print(f"Error processing '{directory}': {e}")
            else:
                print(f"Skipped '{directory}': missing fullchain.pem or privkey.pem")

if __name__ == "__main__":
    run_certbot_renew()
    merge_certificates()
/usr/local/bin/ssl_renew.py
📝
스크립트 동작 설명
  1. certbot renew 명령어로 만료 임박한 인증서를 갱신합니다
  1. /etc/letsencrypt/live 디렉토리 내 모든 도메인을 자동으로 탐색합니다
  1. 각 도메인의 fullchain.pem과 privkey.pem을 병합하여 HAProxy 형식으로 저장합니다

crontab 등록

스크립트를 적절한 위치에 저장하고 실행 권한을 부여합니다:
bash
sudo mv ssl_renew.py /usr/local/bin/
sudo chmod +x /usr/local/bin/ssl_renew.py
그 다음 crontab에 등록하여 자동 실행되도록 설정합니다. /etc/cron.d/ssl-renew 파일을 생성합니다:
bash
0 6 * * * root /usr/local/bin/ssl_renew.py >> /var/log/ssl_renew.log 2>&1
필드
값
의미
분
0
정각
시
6
오전 6시
일
*
매일
월
*
매월
요일
*
모든 요일
💡
왜 매일 실행하나요?
certbot은 인증서 만료 30일 전부터만 갱신을 시도합니다. 매일 실행해도 불필요한 갱신은 발생하지 않으며, 갱신 시점을 놓치지 않을 수 있습니다.

cron 서비스 확인

cron 설정 적용 후 서비스가 정상 동작하는지 확인합니다:
bash
sudo systemctl restart cron
sudo systemctl status cron
notion image
위와 같이 active (running) 상태라면 정상적으로 설정이 완료된 것입니다.

✅ 마무리

지금까지 certbot과 Cloudflare DNS 인증을 활용하여 SSL 인증서를 발급받고 자동 갱신하는 방법을 알아보았습니다.
📋
요약
  1. Cloudflare DNS 설정: 도메인 등록 및 API 토큰 발급
  1. certbot 설치: DNS 플러그인과 함께 설치
  1. 인증서 발급: DNS-01 Challenge를 통한 자동 인증
  1. HAProxy 적용: fullchain + privkey 병합
  1. 자동 갱신: Python 스크립트 + crontab 설정
이 방식의 장점은 다음과 같습니다:
  • 무중단 갱신: 웹 서버를 중단하지 않고 인증서 갱신 가능
  • 멀티 서버 지원: NFS를 통해 여러 서버에서 동일한 인증서 공유
  • 완전 자동화: 한 번 설정하면 90일마다 자동으로 갱신
  • Wildcard 지원: 필요시 *.domain.com 형태의 인증서도 발급 가능