Docker compose + certbot + nginx 로 SSL 인증서 발급하기

스윗포테이토·2022년 10월 20일
10

web-etc

목록 보기
3/3

프로젝트에 https를 적용하는 과정에서 겪었던 오류에 대해 작성해두려고 한다.
우선 내 프로젝트는 docker-compose를 통해 관리되고 있었고, 웹서버는 nginx를 활용하고 있다.

HTTPS, SSL

https와 ssl 인증서에 관해서는 블로그 글을 참고하였다.

HTTPS

  • HTTP Over 'Secure Socket Layer'(SSL), SSL을 사용하는 HTTP
    이름에서 알 수 있듯이, 보안적인 부분을 강화한 HTTP라고 생각하면 된다.

CA

  • HTTPS를 사용하기 위한 SSL 인증서를 발급해주는 공인된 기관
  • 브라우져는 이 기관들을 리스팅해서 가지고 있다.

Let's Encrypt

  • CA 중 하나로, 무료로 SSL 인증서를 발급해준다.
  • 유효기간이 90일이므로 기간이 다 되면 재발급 받아서 사용해야 한다.

Certbot

  • Let's Encrypt에서 SSL인증서를 발급받는 자동화 툴
  • 오픈소스이며, 도커에 공식 이미지가 올라와 있어서 컨테이너 방식으로 편하게 사용할 수 있다.

발급 과정

certbot을 활용하면 CLI를 통해 발급 받을 수 있지만, 90일 마다 재발급을 해주어야 한다. 다행히 docker compose + nginx + certbot을 사용하여 이 과정을 자동화 해둔 설정 파일이 올라와 있어서 편하게 사용하였다.

가이드가 상당히 자세히 작성되어 있기 때문에, 발급을 시도하기 전에 먼저 읽고 시도하길 권한다.

미래의 나를 위해 정리해보자면,

  1. 사전 준비 사항 - 도메인 발급 (A or AAAA record)

  2. 인증서 발급을 위한 certbot 컨테이너 만들기

  3. https는 443포트를 이용하고 있기 때문에 nginx 컨테이너에 443포트 추가로 열어두기

  4. nginx 설정파일 변경
    기존 80포트에 작성되었던 내용을 전부 443 포트로 변경하고, 80으로 오는 요청을 전부 443으로 리다이렉션 해준다.

    server {
        listen 80;
        server_name example.org;
        location / {
            return 301 https://$host$request_uri;
        }    
    }
    server {
        listen 443 ssl;
        server_name example.org;
    
        이하 기존 80포트에 작성했던 것들...
    }
  5. certbot이 발급한 인증서를 nginx가 브라우져의 요청에 따라 반환할 수 있어야 하므로 certbot 컨테이너와 nginx 컨테이너는 같은 폴더를 공유해야 한다.
    따라서 nginxcertbotvolume 추가

  - ./data/certbot/conf:/etc/letsencrypt
  - ./data/certbot/www:/var/www/certbot
  1. nginx 설정 파일 수정
    certbot이 발급한 challenge 파일을 nginx가 서빙하도록 80포트에 추가

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
  2. 재발급이 가능하도록 command 추가
    인증서 만료가 될 때쯤 자동으로 다시 SSL인증서를 다시 발급하도록 nginx, certbot 컨테이너에 커맨드를 추가해주어야 한다.

    nginx:
        ...
        command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
    
    certbot:
        ...
        entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
  3. .init-letsencrypt.sh
    1~6까지만 하면 SSL재발급 자동화가 완성된다. 그렇지만 재발급을 하기 위해서는 첫번째 발급을 해야 하는데, 현재 nginx폴더에 SSL 관련 정보가 있기 때문에 nginx를 돌리기 위해서는 SSL 인증서가 필요하다. 따라서 이걸 위해서 더미 인증서를 발급받아 nginx를 구동하고, 더미 인증서를 삭제하고 SSL인증서를 발급 받을 것이다. 이 과정을 자동화 한 쉘 스크립트가 .init-letsencrypt.sh이다.

프로젝트에 적용하기

위에 단계가 여럿 있지만, 크게 수정 & 작성 해야 하는 파일은 세가지이다.

  • nginx.conf 변경사항

    server {
        listen 80;
        server_name <your domain>;
        server_tokens off;
    
        location /.well-known/acme-challenge/ {
            allow all;
            root /var/www/certbot;
        }
    
        location / {
            return 301 https://$host$request_uri;
        }
    }
    
    server {
        listen 443 ssl;
        server_name <your domain>;
        server_tokens off;
    
        ssl_certificate /etc/letsencrypt/live/<your domain>/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/<your domain>/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
    
        <이하 기존 80포트 내용 채워넣기>
    }
  • docker-compose.yml

    version: "3"
    
    services:
      nginx:
        container_name: nginx
        restart: unless_stopped
    
        ...
    
        volumes:
          ...
          - <data_path>/conf:/etc/letsencrypt
          - <data_path>/www:/var/www/certbot
        ports:
          - 80:80
          - 443:443
        command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
    
      certbot:
        container_name: certbot
        image: certbot/certbot:arm32v6-latest
        restart: unless-stopped
        volumes:
          - <data_path>/conf:/etc/letsencrypt
          - <data_path>/www:/var/www/certbot
        entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
    
        ...
    
  • .init-letsencrypt.sh

    • line 8: domain - 서비스 도메인 기재
    • line10: data_path - certbot 폴더 위치 기재.
      주의) docker-compose.yml의 volumes의 data_path와 일치시켜야 함
    • line11: email - 발급되면 메일이 온다는데 나는 안왔다...
    • line12: staging - 발급 시도 횟수가 일정 수준을 넘어가면 한동안 발급을 받을 수 없다. 따라서 테스트 중에는 1로 두고 오류 없이 발급이 끝나고 https로 접속이 잘 되는 것을 확인한 후 0으로 바꿔서 다시 실행하자.
      참고로 1로 두고 발급을 마치면 https로 접속은 되지만, 크롬에서 Not Secure하다고 나온다.

여기까지 다 마쳤으면

docker compose build
./init-letsencrypt.sh
docker compose up

으로 실행하면 된다. 만약 성공했다면, 축하한다.
하지만 난 엄청난 시행착오를 거쳤기 때문에, 기록을 남겨놓겠다.

발급 과정 중 troubleshooting

./init-letsencrypt.sh 실행 중 발생한 에러들...

  1. Error: docker compose is not installed.
    이건 3번줄에서 걸리는 건데, 나는 docker-compose가 매우 잘 깔려있는 것을 확인 했음에도 번번히 이 명령줄에 걸려 스크립트가 종료되었다.
    한가지 다른 것은 내 환경에서는 명령어가 docker-compose가 아닌 docker compose 였기 때문에 그 부분을 고쳐주었으나, 전혀 먹지 않았다. 쉘 스크립트를 잘 다루지 못하는 나의 탓이니 그냥 주석처리해서 넘어갔다.

  2. docker-compose not found
    위에서 언급한 명령어 차이 때문이다. 스크립트 내의 모든 docker-composedocker compose로 바꿔주었다.

  3. container nginx not found
    이건 내 세팅 상의 문제인데, 나는 nginx 컨테이너와 서비스 이름을 frontend로 주었는데, 이 쉘 스크립트에서는 nginx라고 되어 있어서 문제가 생겼다. 쉘 스크립트에 언급된 nginx 컨테이너를 frontend로 바꾸었더니 해결

    echo "### Starting nginx ..."
    docker compose up --force-recreate -d frontend
    echo
    
    ...
    
    echo "### Reloading nginx ..."
    docker compose exec frontend nginx -s reload
  4. 실행했는데 아무 변화가 없음

    docker compose build

    빌드 하고 스크립트를 실행하는게 좋다. 빌드하다가 에러가 나는 경우도 있으니 디버깅에 유리.

  5. exec /usr/bin/openssl: exec format error
    certbot 버전 문제가 있는거 같다. docker-compose.yml 파일에서 버전을 수정했더니 해결되었다.

    image: certbot/certbot:arm32v6-latest
  6. 디스크 용량 부족
    갑자기 빌드 에러가 났다. 읽어보니까 디스크에 용량이 없단다. 아니 내가 뭘했다고...?
    디스크 사용량을 조회해보자

    df -h

    usage가 100%가 나왔다...
    도커 이미지, 볼륨 캐시파일 등 뭐가 잔뜩 있어서 용량이 꽉 차버렸다.

    # 우선 실행중인 컨테이너 중단
    docker compose down
    # 중단된 컨테이너, 볼륨 다 삭제
    docker system prune -a

    이거 한방에 51%로 줄었다.

  7. Fetching connection refused
    이게 제일 시간을 많이 잡아 먹었는데, 정확한 해결 원인을 모르겠다. 우선 시도한 모든 사항을 리스팅할 예정.
    우선 살펴보니까 nginx 컨테이너가 죽어서 /.well-known/acme-challenge/으로 요청을 보냈을 때 응답이 없었던거 같다.

    7-1. docker-compose.yml
    nginx: restart: unless_stopped 추가
    전혀 해결되지 않았다.

    7-2. 로그를 보니까 ssl_certificate 등 파일을 찾을 수 없어서 에러가 났다고 나왔다. ssl 발급 전이라서 그런가? 하고 우선 nginx.conf에서 443부분을 모두 주석처리하고 스크립트 실행 -> 성공

    이후 주석을 해제하고

    docker compose exec nginx nginx -s reload

    리로딩 -> 실패.

    생각해보니까 nginx.conf는 볼륨 연결을 하지 않아서 초기에 빌드할 때 설정파일을 복사하여 컨테이너에 따로 저장되어 있으니 변경사항이 반영될 리 X
    따라서 volume에 nginx 설정파일을 연결한 뒤 다시 443 주석처리 -> 스크립트 실행 -> 주석해제 -> nginx reload => 성공.

    우선... 성공했다.
    근데 대체 이유를 알 수가 없다. 분명 쉘 스크립트에서 더미 ssl을 받아서 nginx를 구동한 뒤 삭제하고 있는데...?

    7-3. 이유를 찾다가 도커 파일에 nginx 컨테이너에 command 부분을 봤다. 생각해보니까 Dockerfile에 CMD가 있는데...? 혹시 이것 때문인가 싶어서 Dockerfile에 CMD를 주석처리한 후에 nginx 설정파일을 443부분을 주석처리하지 않고 스크립트를 실행해봤다.

    됐다...

    근데 찾아보니까 command와 CMD가 동시에 있으면 docker-comsose의 command가 실행된다는데 그럼 상관 없는게 아닐까...?

아무튼... 나는 CMD를 주석처리한 후에는 한번에 쉘 스크립트가 실행되고 있다.

마무리

init-letsencrypt.sh는 이름대로 초기화, 즉 최초의 ssl 발급 시에 사용되는 스크립트이다. 따라서 이후에 새로 빌드할 일이 있는 경우 그냥 docker compose build, docker compose up등으로 실행하면 된다.

reference

HTTPS, SSL 인증서: 아주 쉽고 간단하면서도, 매우 상세한 정리.
Docker-compose + Nginx SSL 적용하기 (certbot)
Boilerplate for nginx with Let’s Encrypt on docker-compose
Nginx and Let’s Encrypt with Docker in Less Than 5 Minutes

profile
나의 삽질이 미래의 누군가를 구할 수 있다면...

1개의 댓글

comment-user-thumbnail
2022년 12월 13일

은인이십니다...

답글 달기