백엔드 엔지니어 이재혁
컨테이너 환경 Nginx에 HTTPS 적용 및 인증서 자동 재발급 본문
오래 헤맸던 경험을, 이후에 글로 정리하다보니 일부 빠진 내용이 있을 수 있습니다.
컨테이너 환경의 Nginx는 HTTPS를 쉽게 적용하기 어려운 문제가 있다... (Certbot Nginx 플러그인 사용 불가)
일단 HTTPS를 적용하는 것에 목적을 두고, 조금 우회하는 방법으로 해봤다.
Amazon Linux 2023 기준이라는 점 참고해주세요
인증서부터 정상 발급 받기
certbot으로 인증서 발급 진행
sudo dnf install -y certbot
sudo certbot certonly --standalone -d carematching.kro.kr # 주소는 각자에 맞게 바꿔주기
# 시키는대로 진행하다보면 이제 인증서와 개인키가 발급된다.
Certificate is saved at: /etc/letsencrypt/live/carematching.kro.kr/fullchain.pem
Key is saved at: /etc/letsencrypt/live/carematching.kro.kr/privkey.pem
참고: standalone 방식으로 인증서를 발급할 때는 임시로 웹서버를 열어줘야 하기 때문에 기존 웹서버를 일시중지하고 실행해야 한다.
컨테이너에서 인증서 공유
이렇게 발급된 인증서와 키를 컨테이너 내부 Nginx에서 사용할 수 있도록 하겠다.
`-v /etc/letsencrypt:/etc/letsencrypt:ro`를 사용했다.
docker run -d \
--name carematching-front \
-p 80:80 \
-p 443:443 \
# 아래 옵션으로 호스트의 letsencrypt 경로를 마운트 (ro: readonly)
-v /etc/letsencrypt:/etc/letsencrypt:ro \
# ACME(Automatic Certificate Management Environment)
# 아래 경로는 이 서버 주인이 내가 맞음을 인증하기 위해 쓰임 (ro: readonly)
# 추후 자동 재발급을 해보려다가 필요해졌습니다. (그래서 본문 설명에는 언급 하지 않음)
-v /var/www/certbot:/var/www/certbot:ro \
ghcr.io/jaehyuk-lee/carematching-front:latest
Nginx 설정 변경
모질라에서 SSL 설정을 제공해준다! https://ssl-config.mozilla.org/
Nginx Intermediate 설정 선택, Let's Encrypt는 OCSP 지원을 종료해, OCSP Stapling 옵션은 체크 해제했다. 설치한 Nginx 버전도 확인해서 `1.29.1`로 입력했다.
아래는 모질라에서 제공해준 설정 (`http {}` 블록은 제외)
# generated 2025-09-24, Mozilla Guideline v5.7, nginx 1.27.3, OpenSSL 3.4.0, intermediate config
# https://ssl-config.mozilla.org/#server=nginx&version=1.27.3&config=intermediate&openssl=3.4.0&guideline=5.7
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # Nginx 1.25.1 이상에서 이 문법 허용
ssl_certificate /path/to/signed_cert_plus_intermediates;
ssl_certificate_key /path/to/private_key;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
}
# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# see also ssl_session_ticket_key alternative to stateful session cache
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam "/path/to/dhparam";
# HSTS
server {
listen 80 default_server;
listen [::]:80 default_server;
return 301 https://$host$request_uri;
}
제 프로젝트에 적용한 전체 설정을 확인하시고 싶으면 아래 더 보기를 눌러보세요.
파일: `default.conf`
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
ssl_certificate /etc/letsencrypt/live/carematching.kro.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/carematching.kro.kr/privkey.pem;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
# react-router-dom의 라우팅을 위한 try_files 설정
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# API reverse proxy 설정
location /api {
proxy_pass http://host.docker.internal:8080;
proxy_set_header Host $host;
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;
client_max_body_size 11M;
}
# 웹소켓 reverse proxy 설정
location /ws {
proxy_pass http://host.docker.internal:8080;
proxy_set_header Host $host;
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;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ecdh_curve X25519:prime256v1:secp384r1;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# see also ssl_session_ticket_key alternative to stateful session cache
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
# curl https://ssl-config.mozilla.org/ffdhe2048.txt > /path/to/dhparam
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # use let's encrypt dhparam
# HSTS
server {
listen 80 default_server;
listen [::]:80 default_server;
return 301 https://$host$request_uri;
}
ssl_dhparam 같은 경우에는 모질라에서 제공하는 것 대신 Let's Encrypt로 인증서 발행할 때 생성해준 dhparams를 그대로 사용했다.
OCSP
OCSP는 디지털 인증서가 유효한지, 폐지되었는지 실시간으로 확인하는 도구다.
클라이언트가 "이 인증서 지금도 유효해?" 하고 CA 서버에 질의하는 방식이다.
stateless 방식의 인증서를 중간에 강제로 폐기하기 어려웠던 문제를 해결하고자 나왔지만
여러 단점으로 인해 OCSP Stapling을 도입하거나, Let's Encrypt와 같이 OCSP를 사용하지 않는 대신 인증서 유효기간을 짧게 잡는 경우가 있다.
보안그룹 설정
AWS 보안그룹에서 HTTPS(443 포트) 접속 허용을 안해놨던걸 까먹고 별짓을 다하며 한시간 정도 헤맸다... 😭😭😭😭😭
여러분들은 보안그룹 잊지 말고 HTTPS 허용하시길...
인증서 확인
초보 개발자다운 고생 끝에 carematching.kro.kr 도메인에 HTTPS 적용을 완료했다...

carematching.kro.kr/ 접속해보시죠 하하
재발급 자동화
certbot renew로 자동 재발급하는 시스템을 도입했다.
컨테이너 설정 추가
`-v /var/www/certbot:/var/www/certbot:ro` 옵션을 넣었는데,
이 부분이 웹서버 중지 없이 자동 재발급을 위해 필요했다.
certbot은 호스트 인스턴스에서 실행되고, nginx(웹서버)는 컨테이너 내부에서 실행 중이라,
호스트에서 certbot이 변경한 내용을 컨테이너에서 nginx가 응답해주기 위해서 필요한 설정이다.
Nginx 설정 추가
# ACME challenge만 평문 HTTP로 노출 - 80번 포트 부분에 이 내용 추가
location ^~ /.well-known/acme-challenge/ { # ^~를 붙이면 다른 정규식보다 이 설정을 우선하게 됨
root /var/www/certbot;
default_type "text/plain";
try_files $uri =404;
}
자동 실행 추가
Amazon Linux 2023에는 crontab이 없다. 😓 systemd timer 방식을 써보자.
1. 서비스 유닛 만들기
`/etc/systemd/system/certbot-renew.service`
[Unit]
Description=Certbot Renewal
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --webroot -w /var/www/certbot --quiet \
--deploy-hook "docker exec carematching-front nginx -s reload" # 웹서버 컨테이너 ID 혹은 이름에 맞게 설정
# 인증서 재발급 후, Nginx가 재발급된 인증서를 사용하도록 재시작하게 해주는 훅
2. 타이머 유닛 만들기
`/etc/systemd/system/certbot-renew.timer`
[Unit]
Description=Run certbot renew twice daily
[Timer]
OnCalendar=*-*-* 03,15:00:00 # AWS EC2 인스턴스가 UTC 기준으로 도는 중 - KST 0시, 12시로 작동
Persistent=true
[Install]
WantedBy=timers.target
3. 적용
sudo systemctl daemon-reload # systemd가 서비스/타이머 유닛 파일을 다시 읽도록 갱신
sudo systemctl enable --now certbot-renew.timer # 타이머 활성화
4. 확인
systemctl list-timers | grep certbot
# 출력
Wed 2025-09-24 15:00:00 UTC 7h left - - certbot-renew.timer certbot-renew.service
certbot 타이머가 서비스를 실행하기까지 얼마나 남았는지 확인이 가능하다.
인증서 갱신 테스트
sudo certbot renew --webroot -w /var/www/certbot --dry-run # --dry-run 옵션으로 테스트 가능
# ... (이런저런 로그)
# 성공!
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/carematching.kro.kr/fullchain.pem (success)
renew 테스트를 성공하면, 이제 안심하고 자동 발급 시스템을 사용할 수 있다.
* 이상한 내용을 발견하실 경우 꼭 댓글로 알려주시면 직접 확인해서 글에도 반영하겠습니다!
'케어매칭 서비스 배포' 카테고리의 다른 글
| 결제 시스템: Enum을 활용한 유한 상태 기계 만들기 (0) | 2025.11.11 |
|---|---|
| [MSA 전환기] Day 4 - MS별 버전 관리, CORS 관리 (0) | 2025.11.04 |
| [MSA 전환기] Day 2 - 타임아웃 및 http/2 설정 (0) | 2025.11.01 |
| [MSA 전환기] Day 1 - 설계, API Gateway 만들기 (0) | 2025.10.30 |
| 케어매칭 AWS 프리티어 배포기 (0) | 2025.07.12 |