---
title: "OpenStack 환경에서 Cloudflare DNS와 certbot을 활용한 SSL 인증서 자동 발급 및 갱신 방법"
description: "OpenStack 환경에서 Horizon 대시보드에 HTTPS를 적용하기 위해 certbot과 Cloudflare DNS 플러그인을 사용해 SSL 인증서를 자동으로 발급하고, NFS 공유와 HAProxy용 PEM 파일 병합 스크립트를 통해 두 컨트롤러에 동일하게 적용하며, Python 스크립트와 cron을 이용해 90일 만료 주기의 자동 갱신까지 완전 자동화하는 방법을 단계별로 설명한다."
date: "2025-06-20"
last_modified: "2026-08-06T00:36:00.000Z"
type: "Post"
tags:
  - "openstack"
  - "ssl"
  - "certbot"
  - "haproxy"
  - "network"
categories:
  - "🤖 Computer Science"
series:
  - "오픈스택 운영"
canonical_url: "https://blog.pieroot.xyz/openstack-ssl"
markdown_url: "https://blog.pieroot.xyz/openstack-ssl.md"
---

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

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

OpenStack 환경에서 Horizon 대시보드에 HTTPS를 적용하려면 SSL 인증서가 필수입니다. 특히 우리 연구실처럼 다중 컨트롤러 환경에서는 인증서 관리가 복잡해질 수 있죠.

이번 포스트에서는 **certbot**과 **Cloudflare DNS 인증**을 활용하여 SSL 인증서를 발급받고, 여러 서버에서 공유하며 자동으로 갱신하는 방법을 알아보겠습니다.

---

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

현재 구축된 OpenStack 환경의 구성은 다음과 같습니다:

이런 구성에서는 **두 서버 모두에 동일한 SSL 인증서가 있어야 하고**, 인증서 갱신 시에도 양쪽에 동시에 적용되어야 합니다.

> **해결 방법**
> 
> NFS를 통해 SSL 인증서 파일을 공유하고, certbot의 자동 갱신 기능에 후처리 스크립트를 연결하여 HAProxy용 인증서 파일을 자동으로 생성합니다.

---

## 🔐 SSL 인증서 기초 지식

### Let's Encrypt란?

**Let's Encrypt**는 무료로 SSL/TLS 인증서를 발급해주는 비영리 인증 기관(CA)입니다. 자동화된 발급 과정을 통해 누구나 쉽게 HTTPS를 적용할 수 있도록 해줍니다.

### SSL 인증서 유형

SSL 인증서로 받을 수 있는 도메인 유형은 두 가지가 있습니다:

- **Wildcard SSL**: `*.`[`pieroot.xyz`](http://pieroot.xyz/)와 같이 특정 도메인의 **모든 서브도메인**을 한 번에 인증

- **Standard SSL**: [`git.pieroot.xyz`](http://git.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을 적용할 도메인을 등록합니다.

![image](https://blog.pieroot.xyz/api/image-proxy?id=985effb9-d61b-48bb-9250-dca80fbaec89&kind=s3&pageId=218067c0-15d0-8018-8962-e7822812bccf&source=block&blockId=218067c0-15d0-80ea-9da1-c0352c801927)

- **유형**: A 레코드

- **이름**: 서브도메인 (예: `openstack`)

- **IPv4 주소**: SSL을 연결할 서버의 실제 IP

![image](https://blog.pieroot.xyz/api/image-proxy?id=99a13494-43b4-40ff-811f-64f1b2e7cd56&kind=s3&pageId=218067c0-15d0-8018-8962-e7822812bccf&source=block&blockId=219067c0-15d0-809c-84ea-c999e8508c70)

위와 같이 설정하면 [`openstack.dmslab.xyz`](http://openstack.dmslab.xyz/) 도메인이 지정한 IP로 연결됩니다.

### 2단계: API 토큰 발급

certbot이 DNS 레코드를 자동으로 관리할 수 있도록 API 토큰을 발급받아야 합니다.

Cloudflare 대시보드에서 **내 프로필 → API 토큰 → 토큰 생성**으로 이동합니다.

![image](https://blog.pieroot.xyz/api/image-proxy?id=9bb2cad1-112a-4e4a-8245-d1bbfbd01a5d&kind=s3&pageId=218067c0-15d0-8018-8962-e7822812bccf&source=block&blockId=218067c0-15d0-8015-bac5-f0280ead8ef1)

> **토큰 권한 설정**
> 
> 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
```

#### 주요 옵션 설명

### 발급 결과 확인

성공적으로 발급되면 다음과 같은 출력이 나타납니다:

![image](https://blog.pieroot.xyz/api/image-proxy?id=09f71bbb-7eff-4abf-8876-69ed4cc41de4&kind=s3&pageId=218067c0-15d0-8018-8962-e7822812bccf&source=block&blockId=218067c0-15d0-801d-9141-f273b8534112)

```
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
```

> **왜 매일 실행하나요?**
> 
> certbot은 인증서 만료 30일 전부터만 갱신을 시도합니다. 매일 실행해도 불필요한 갱신은 발생하지 않으며, 갱신 시점을 놓치지 않을 수 있습니다.

### cron 서비스 확인

cron 설정 적용 후 서비스가 정상 동작하는지 확인합니다:

```bash
sudo systemctl restart cron
sudo systemctl status cron
```

![image](https://blog.pieroot.xyz/api/image-proxy?id=e30f8720-7f64-45dc-8bfa-6c15bd9a0950&kind=s3&pageId=218067c0-15d0-8018-8962-e7822812bccf&source=block&blockId=218067c0-15d0-8004-a2c2-c02c96bc6d53)

위와 같이 `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`](http://domain.com/) 형태의 인증서도 발급 가능

> **참고 자료**
> 
> - [Let's Encrypt 공식 문서](https://letsencrypt.org/docs/)
> 
> - [Certbot 공식 문서](https://certbot.eff.org/docs/)
> 
> - [HAProxy SSL 설정 가이드](https://www.haproxy.com/documentation/haproxy-configuration-tutorials/ssl-tls/)
