문제상황
프론트 팀원으로부터 어느 날부터 swagger 접속시 해당 화면이 뜬다는 이슈가 들어왔다.
확인해보니 해당 문제 기존의 ssl 인증서가 만료되어서 발생한 일이었다.
근데 나는 certbot으로 기존에 인증서 만료되기 전 자동 갱신을 구현해두었는데..?
무엇이 문제인지 다시 살펴보도록 하자.
ssl 인증서 발급 방법에 따른 자동화 가능 여부
certbot 을 이용한 인증서 발급 방법은 아래와 같이 크게 5가지 방법이 존재한다.
- webserver(--nginx, --apache) 옵션을 통한 인증서 발급
- webroot(--webroot) 옵션을 이용한 인증서 발급
- standalone(--standalone) 옵션을 이용한 인증서 발급
- 수동 인증 (--manual) 옵션을 이용한 인증서 발급
- DNS 인증 (--dns-<provider>) 을 이용한 인증서 발급
기존 나는 --manual 방식으로 ssl 인증서를 발급하였고 따라서 cerbot의 renew 명령어가 먹히지 않았던 것이다.
docker logs -f certbot
certbot 컨테이너의 로그를 확인해보면 아래와 같다.
The error was: PluginError('An authentication script must be provided with --manual-auth-hook when using the manual plugin non-interactively.')
All renewals failed. The following certificates could not be renewed:
/etc/letsencrypt/live/[도메인]/fullchain.pem (failure)
-- manual 명령어를 사용하여 인증서를 발급받으면 자동으로 renew가 불가능하다는 이야기이다.
왜 --manual 방식으로 ssl 인증서를 발급했었나?
--manual 방식으로 ssl 인증서를 발급하게 되면 *.mopl.kr 같이 mopl.kr 하위의 모든 도메인을 와일드 카드 방식으로 인증 받을 수 있다.
하지만 매번 갱신 시에도 수동으로 DNS TXT 레코드를 변경해야 하므로 certbot의 renew 명령어가 먹히지 않았던 것이다.
webroot 방식으로 재발급 자동화를 구성하기
먼저 기존 인증서가 만료가 되어있는지 재확인이 필요하다.
인증서 위치는 자신의 구성에 따라 다를 수 있으니 유의하자.
~/nginx$ tree
.
├── certbot
│ ├── conf
│ ├── logs
│ └── www
├── cert.sh (manul 방식의 인증서 발급 script)
├── conf
│ ├── [도메인].conf
│ └── nginx.conf
└── docker-compose.yml
필자는 위와 같이 nginx관련 파일들을 nginx 디렉토리에 패키지화 해두었고 관련 인증 파일은 certbot 디렉토리에 저장되고 있다.
docker run --rm --name cmd_certbot \
-v "$(pwd)/certbot/conf:/etc/letsencrypt" \
-v "$(pwd)/certbot/logs:/var/log/letsencrypt" \
-v "$(pwd)/certbot/www:/var/www/certbot" \
certbot/certbot certificates
임시 cerbot 컨테이너를 실행하여 현재 인증서의 만료여부를 파악하였다.
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
Certificate Name: mopl.kr
Serial Number: 3112cd52010948dc5d8600c33b144bb46d4
Key Type: ECDSA
Domains: mopl.kr *.mopl.kr
Expiry Date: 2025-01-06 06:24:27+00:00 (INVALID: EXPIRED)
Certificate Path: /etc/letsencrypt/live/mopl.kr/fullchain.pem
Private Key Path: /etc/letsencrypt/live/mopl.kr/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
위와같이 인증서가 (INVALID: EXPIRED) 된것을 볼 수 있다.
기존 인증서 삭제
기존 인증서를 삭제해주고 다시 발급받도록 하자
docker run --rm --name certbot \
-v "$(pwd)/certbot/conf:/etc/letsencrypt" \
-v "$(pwd)/certbot/logs:/var/log/letsencrypt" \
-v "$(pwd)/certbot/www:/var/www/certbot" \
certbot/certbot delete --cert-name mopl.kr
인증서 재발급
webroot 방식으로는 와일드 카드 방식의 인증서 발급이 불가능하기 때문에 개발환경과 운영환경 도메인 각각 모두 발급해주었다.
docker run --rm --name certbot \
-v "$(pwd)/certbot/conf:/etc/letsencrypt" \
-v "$(pwd)/certbot/logs:/var/log/letsencrypt" \
-v "$(pwd)/certbot/www:/var/www/certbot" \
certbot/certbot certonly --webroot -w /var/www/certbot \
-d [개발환경 도메인] -d [운영환경 도메인] \
--email [이메일] --agree-tos --no-eff-email
nginx.conf 설정 변경
인증서 발급 시에 인증서 이름이 기존 인증서 이름과 달라졌는지 확인하여 달라졌다면 nginx의 설정도 변경해주어야한다.
docker run --rm --name cmd_certbot \
-v "$(pwd)/certbot/conf:/etc/letsencrypt" \
-v "$(pwd)/certbot/logs:/var/log/letsencrypt" \
-v "$(pwd)/certbot/www:/var/www/certbot" \
certbot/certbot certificates
위 명령어를 실행하면 아래와 같이 현재 인증서의 정보가 나온다.
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
Certificate Name: [인증서 이름]
Serial Number: 3c91b2db290853377b7298a95ca9c592ec7
Key Type: ECDSA
Domains: [개발 환경 도메인] [운영환경 도메인]
Expiry Date: 2025-05-09 13:05:58+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/[인증서 이름]/fullchain.pem
Private Key Path: /etc/letsencrypt/live/[인증서 이름]/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
필자는 인증서 이름이 변경되어 nginx 설정도 변경해주었다.
log_format main '$sent_http_X_DEBUG_MESSAGE';
server {
listen 80; # HTTP 포트
server_name [개발 환경 도메인] [운영 환경 도메인]; # 서브도메인
# Let's Encrypt 인증서 갱신을 위한 경로 설정
location /.well-known/acme-challenge/ {
root /var/www/certbot;
allow all;
}
# HTTP 요청을 HTTPS로 리디렉션
location / {
return 301 https://$server_name$request_uri;
}
}
## 개발 환경 설정
server {
listen 443 ssl; # HTTPS 포트
server_name [개발환경 도메인]; # 개발 도메인
# SSL 인증서 경로 설정
ssl_certificate /etc/letsencrypt/live/[인증서 이름]/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/[인증서 이름]/privkey.pem;
location / {
add_header X_DEBUG_MESSAGE "[개발환경 도메인]";
proxy_pass http://moplus-server-dev:8080;# 개발 서버로 라우팅
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
access_log /var/log/nginx/access.log main;
}
## 운영 환경 설정
server {
listen 443 ssl;
server_name [운영환경 도메인];
# SSL 인증서 경로 설정
ssl_certificate /etc/letsencrypt/live/[인증서 이름]/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/[인증서 이름]/privkey.pem;
# 요청 처리
location / {
add_header X_DEBUG_MESSAGE "[운영환경 도메인]";
proxy_pass http://moplus-server-prod:8080; # 프로덕션 서버로 라우팅
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
access_log /var/log/nginx/prod_access.log main;
}
위와 같이 SSL 인증서 경로 설정에 발급한 인증서 이름을 적용해주어야한다.
certbot 인증서 재발급 컨테이너 생성
docker-compose.yml을 만들어 certbot이 7일에 한번 인증서를 갱신하도록 구성하였다.
재발급된 인증서가 nginx에 반영되려면 nginx도 restart되어야하니 24시간에 한번 reload되도록하였다.
version: '3.8'
services:
certbot:
image: certbot/certbot
container_name: certbot
volumes:
- ./certbot/logs:/var/log/letsencrypt:rw
- ./certbot/conf:/etc/letsencrypt:rw
- ./certbot/www:/var/www/certbot:rw
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot --quiet --no-self-upgrade; sleep 7d & wait $${!}; done;'"
nginx:
image: nginx
container_name: nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
- ./conf/nginx.conf:/etc/nginx/nginx.conf
- ./conf/[도메인].conf:/etc/nginx/conf.d/[도메인].conf"
command: "/bin/sh -c 'trap exit TERM; while :; do sleep 24h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
networks:
- nginx-network
networks:
nginx-network:
external: true
nginx -s reload 명령어를 사용하면 nginx 컨테이너 재시작 없이 새로운 설정을 반영할 수 있기 때문에 중간에 서비스 중단(다운타임)이 일어나는 걱정 없이 재설정을 할 수 있다.
또한 재발급을 한다고해서 이전 인증서가 만료가 되진 않기 때문에 이미 Nginx 컨테이너에 복사되어서 사용되고 있는 이전 인증서의 만료는 걱정하지 않아도 된다.
마무리
시스템을 구축할 때, 단순히 설정하는 것뿐만 아니라 정상적으로 작동하는지 철저히 테스트한 후 적용하는 것이 중요함을 다시 한번 깨달았다.
특히 인증서 발급 방식에 따라 재발급이 불가능할 수도 있다는 점을 미리 인지하고, 구축 초기 단계에서 재발급 가능 여부까지 확인했다면 프론트 팀원의 개발 지연을 방지할 수 있었을 것이다.
참고
만약 docker-compose 로 certbot을 컨테이너가 아닌 cron job으로 일정 주기마다 실행시키고 싶다면 아래 블로그에 나와있는
cron job 방식도 좋다고 생각한다.
필자가 docker-compose로 해당 재발급 주기를 관리한 것은 docker container로 관리할 시 로그를 기록하고 조회하는 것이 수월하기 때문이다.
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
da6a31a00f15 nginx 0.00% 20.68MiB / [서버 메모리] 0.06% 20.5kB / 36.5kB 0B / 0B 28
da7d1cea967a certbot 0.00% 532KiB / [서버 메모리] 0.00% 2.51kB / 845B 0B / 8.19kB 2
또한 certbot을 컨테이너로 띄운다고 해서 위처럼 서버의 메모리를 많이 잡아먹지는 않기 때문에 로그를 따로 파일로 명시적으로 관리하기 싫다면 docker cotainer로 관리하는 것도 괜찮다고 생각한다.
'프로젝트' 카테고리의 다른 글
[Swagger] blocked:mixed-content CORS 해결 (0) | 2025.02.08 |
---|---|
[Nginx] Nginx로 정적 파일 서빙을 통한 웹 페이지 로딩 시간 개선 (2) | 2024.12.07 |