Docker Nginx에서 HTTPS를 위한 SSL 인증서 적용 (Let's encrypt, Docker compose)

rivkode·2024년 1월 16일
2
post-thumbnail

지난글에 이어 이번시간에는 Docker 환경에서 Nginx를 띄우고 Https를 위해 인증서를 적용하는 과정을 설명드리겠습니다.

SSL 적용하는 과정을 바로 보고싶으신 분들은 우측의
SSL 인증서를 적용하는 방법
클릭을 부탁드립니다.


지난 시간에는 아래의 내용들을 완료하였었습니다.

  • Github Actions, Compose를 활용한 CI/CD 파이프라인 구축
  • 위 과정에서 필요한 Nginx Image와 Server Image 생성, Container실행

Github Actions를 이해하며 Github에 특정 event(git push, PR등)가 발생되었을때 여러 Job들이 실행되는 (ci.yml, cd.yml 스크립트가 Github의 가상환경에서 실행) 과정을 거쳤습니다

위 과정을 진행하게되면 EC2내에 Docker Container가 2개 생성되며 외부의 요청들은 Nginx를 통해 spring server인 프록시 서버로 포워딩을 해주게 됩니다

이렇게 되면 현재 아래의 구조가 갖춰지게 됩니다



비록 지금은 spring 서버가 1대라 트래픽에 대해 Load Balancing을 해줄 게 없어 보이지만 여러대가 있다고 가정합니다.

이제 CI/CD는 구축이 완료되었지만 실제 프로젝트에서 사용하게 된다면 보안상의 이유로 HTTPS 가 필요할 수 있습니다

HTTPS로 배포하기 위해서는 Nginx에 SSL 인증서를 적용하는 방법이 있습니다


이전에 배포한 내용은 HTTP 프로토콜(default 통신포트 번호가 80)로 통신이 가능하도록 구성되어있으며 보안이 적용된 프로토콜인 HTTPS 프로토콜(S가 Secure을 의미, default 통신포트 번호가 443)로 통신을 하기 위함입니다

SSL은 웹사이트와 브라우저 사이(또는 두 서버 사이)에 전송되는 데이터를 암호화하여 인터넷 연결을 보호하기 위한 표준 기술입니다 이 기술은 해커가 개인 데이터나 금융 데이터 등의 전송되는 정보를 보거나 훔치는 것을 방지합니다

HTTPS는 웹사이트가 SSL/TLS 인증서로 보호되는 경우 HTTPS가 URL에 표시됩니다. 사용자는 브라우저 표시줄의 자물쇠 기호를 클릭해 발급 기관 및 웹사이트 소유자의 상호를 포함한 인증서의 세부 정보를 볼 수 있습니다.

velog의 경우 아래와 같이 확인됩니다.

아래는 HTTP와 HTTPS 의 차이에 대한 간략한 그림입니다.


아래에서 HTTPS를 적용하면 암호화가 어떻게 이루어지는지 간략하게 살펴보도록 하겠습니다.


암호화 방식


  • 대칭키
  • 비대칭키
  • HTTPS에서의 사용

암호화 방식에는 크게 2가지가 있습니다.
1. 비밀키 (대칭키)
2. 공개키 (비대칭키)

비밀키 방식은 암호화 키와 복호화 키가 같은 방식이며,
공개키 방식은 암호화 키와 복호화 키가 다른 방식을 의미합니다.


비밀키

비밀키 방식은 1개의 키를 통해 암호화화 복호화를 진행하여 데이터를 암호화 합니다.

즉 A라는 키를 통해 암호화를 하고 이후 암호화된 데이터를 복호화 하기 위해 다시 A라는 키를 사용합니다.

대칭키 암호화

정의: 하나의 키를 사용해서 데이터를 암호화하고 복호화하는 방식.

동작 방식:

키 생성: 암호화를 할 때 사용할 하나의 키(비밀키)를 생성합니다.
암호화: 데이터를 보낼 때, 이 비밀키를 사용해서 데이터를 암호화합니다.
전송: 암호화된 데이터를 받는 사람에게 보냅니다.
복호화: 데이터를 받는 사람이 같은 비밀키를 사용해서 데이터를 복호화합니다.

비유: 집 열쇠 하나를 만들어서 집에 들어갈 때도 쓰고, 나올 때도 쓰는 것과 같습니다.


공개키

공개키 방식은 2개의 키를 통해 암호화와 복호화를 진행하며 이때 암호화키와 복호화키는 서로 다른점이 특징입니다.
서로 다른 키를 통해 암호화와 복호화가 이루어져서 비대칭키라고도 부릅니다.

정의: 두 개의 키(공개키와 개인키)를 사용하는 암호화 방식.

동작 방식:

키 쌍 생성: 공개키와 개인키 쌍을 생성합니다.
공개키 배포: 공개키는 누구나 알 수 있게 공개합니다.
암호화: 데이터를 보낼 때, 받는 사람의 공개키를 사용해서 데이터를 암호화합니다.
전송: 암호화된 데이터를 받는 사람에게 보냅니다.
복호화: 데이터를 받는 사람은 자신의 개인키를 사용해서 데이터를 복호화합니다.

비유: 우체통에 열쇠 두 개가 있는데, 하나는 누구나 사용할 수 있는 우체통 열쇠(공개키)고, 다른 하나는 우체통을 여는 열쇠(개인키)입니다. 누구나 우체통에 편지를 넣을 수 있지만, 우체통을 열어서 편지를 꺼내는 건 개인키를 가진 사람만 할 수 있습니다.

HTTPS에서의 사용

HTTPS (HyperText Transfer Protocol Secure): 웹 상에서 안전하게 데이터를 주고받기 위한 프로토콜.

동작 방식: HTTPS는 대칭키와 비대칭키를 결합해서 사용합니다.
비대칭키를 이용한 초기 연결: 클라이언트(브라우저)와 서버가 안전하게 대칭키를 교환하기 위해 비대칭키를 사용합니다.
대칭키를 이용한 데이터 전송: 이후에는 교환된 대칭키를 사용해서 데이터를 암호화하고 복호화합니다.

위 내용들을 요약해보면 아래와 같습니다.

대칭키: 하나의 키로 암호화와 복호화를 모두 수행.

비대칭키: 공개키와 개인키 쌍을 사용해서 안전한 통신을 수행.

HTTPS: 비대칭키로 대칭키를 안전하게 교환한 후, 대칭키로 데이터 전송.

이렇게 대칭키와 비대칭키 암호화 방식을 이해하면, HTTPS가 어떻게 데이터를 안전하게 보호하는지 쉽게 알 수 있습니다.


SSL 인증서

이제 왜 HTTPS 로 통신을 해야하는지 알았다면 위와같이 저희 웹사이트에 SSL 인증서를 적용시키는 방법을 알아보도록 하겠습니다

Nginx는 현재 Docker Container 환경이며 인증서 만료시 업데이트 또한 자동으로 이루어져야 합니다

그러기 위해서는 certbot를 사용해주려고 합니다

certbot의 역할은 인증서 발행 서버가 인증서를 발급하는 과정에서 인증문자를 Nginx에 설정해주는 역할을 합니다



아래 순서로 진행해보도록 하겠습니다

  • Docker compose 를 통해 컨테이너가 실행되는 과정
  • HTTPS 적용시 필요한 것들
  • SSL 인증서를 적용하는 방법
  • 에러 해결 과정

Docker compose 를 통해 컨테이너가 실행되는 과정


이미 다른 여러 블로그나 사이트에서 어떻게 설정하는지는 너무나도 잘 나와있으므로 실행되는 과정, 동작원리에 조금 집중하여 설명해보겠습니다

우선 결론부터 말씀드리겠습니다


결론은 EC2의 /home/ubuntu/compose/docker-compose.yml 에 있는 파일의 설정값으로 이루어진 내용들을 통해 컨테이너가 생성

이때 필요한 이미지들(Spring, Nginx, Certbot 등)은 Docker hub에서 가져오고 Nginx에 대한 설정값은 EC2의 /home/ubuntu/compose/conf/nginx.conf 에 있는 설정파일을 참조하여 컨테이너가 실행되게 됩니다

데이터베이스를 RDS와 연동하므로 Github secrets 를 활용하여 설정파일을 가져오도록하였으며 spring의 Docker 이미지를 만들때 해당 설정파일을 참고하도록 설계하였습니다


Docker compose를 통해 컨테이너들을 만든다는 것은 docker-compose.yml에 작성한 스크립트의 내용들을 통해 컨테이너를 생성한다는 것입니다

저의 경우 cd.yml 파일에는 아래 순서대로 script가 진행됩니다


  • Set up JDK
  • Make application-database.yml
  • Gradle Caching
  • Grant Execute Permission For Gradlew
  • Build with Gradle
  • Docker build & Push
  • Deploy Images with Docker compose

여기서 주의깊게 보셔야할 순서는 가장 마지막인 Deploy Images with Docker compose 입니다
해당 순서 이전에는 말그대로 Deploy를 하기 위한 준비이며 실제 배포는 해당 순서에서 진행하기 때문입니다
이때 아래와 같이 docker compose down, Image pull, docker compose up을 하는 명령어 script가 있습니다


 ...
 script: |
            sudo docker-compose -f $COMPOSE down
            sudo docker pull ${{ secrets.DOCKER_REPO }}/preonb
            sudo docker-compose -f $COMPOSE up -d

그 전에는 컨테이너들을 생성하기 위한 준비를 하는 과정이었고 이 순서에서 docker compose를 통해 컨테이너들이 생성되는 것 입니다


HTTPS 적용시 필요한 것들


HTTPS 적용시 필요한 것들은 아래와 같습니다


  • docker, docker compose (설치를 진행)
  • 도메인 주소
  • EC2 80, 443 포트 개방
  • nginx.conf 파일
  • certbot, nginx 도커 이미지 (docker hub에서 pull 받을 예정)
  • 인증서를 발급받는 script (github에서 다운로드 예정, 인증서를 처음 발급받을 시 nginx 컨테이너가 실행상태여야 합니다!)

SSL 인증서를 적용하는 방법


진행 순서는 아래와 같습니다

  • docker compose up -d 명령어로 docker-compose.yml + nginx.conf 파일을 통해 nginx와 certbot 컨테이너를 실행
  • docker compose ps 로 컨테이너 상태 확인
  • 인증서 발급 script 다운로드
  • nginx 컨테이너가 실행중인 상태에서 인증서 발급 script를 통해 인증서 발급 진행
  • nginx.conf를 SSL에 맞게 수정
  • docker compose로 컨테이너 재시작
  • HTTPS 적용 완료

1

nginx.conf 파일을 EC2에 /home/ubuntu/compose/conf/nginx.conf 에 생성

파일 생성 방법은 해당 conf 폴더 이동 후

sudo touch ./nginx.conf 입력

nginx.conf 내용


events {
    worker_connections  1024;
}

http {
server {
     listen 80;

     server_name domain; // 등록한 도메인으로 변경

     location /.well-known/acme-challenge/ {
             allow all;
             root /var/www/certbot;
     } 
}
}

2

docker-compose.yml 파일을 EC2에 /home/ubuntu/compose/docker-compose.yml 에 생성

파일 생성방법은 폴더로 이동후

sudo touch ./docker-compose.yml 입력

docker-compose.yml 내용 (volumes에 있는 ./data 폴더는 자동으로 생성됩니다)

version: '3'
services:
  nginx:
    container_name: preonb-nginx
    image: jonghuni/preonb-nginx:latest
    volumes:
      - ./conf/nginx.conf:/etc/nginx/nginx.conf
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    ports:
      - 80:80
      - 443:443
    networks:
      - preon_net
    depends_on:
      - application
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

  certbot:
    image: certbot/certbot
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

3


위 파일 생성이후 docker compose up -d 로 컨테이너 상태 확인
(만약 여기서 컨테이너가 종료되거나 할 경우 어떤 에러 사항인지 logs를 통해 확인)

4


인증서 발급 script 다운로드 및 script를 실행하여 인증서 발급


아래 script를 명령어로 입력하여 진행

curl -L https://raw.githubusercontent.com/wmnnd/nginx-certbot/master/init-letsencrypt.sh > init-letsencrypt.sh
chmod +x init-letsencrypt.sh
vi init-letsencrypt.sh // 도메인, 이메일, 디렉토리 수정
sudo ./init-letsencrypt.sh // script를 실행하여 인증서 발급

이제 ssl인증서를 발급받았으니 이전에 작성하였던 nginx.conf 내용을 SSL에 맞게 수정

수정된 nginx.conf 내용

events {
    worker_connections  1024;
}

http {
server {
     listen 80;
     server_name synergyy.link;

     location /.well-known/acme-challenge/ {
             allow all;
             root /var/www/certbot;
     }
     location / {
        return 301 https://$host$request_uri;
    }
}

server {
        listen 443 ssl;
        server_name synergyy.link;

        ssl_certificate /etc/letsencrypt/live/synergyy.link/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/synergyy.link/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

        location / {
        proxy_pass  http://preonb:8080; // preonb는 spring 컨테이너 이름
        proxy_set_header    Host                $http_host;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
    }
}
}

5

docker compose로 컨테이너 재시작

docker compose down -> docker compose up -d -> docker compose ps

로 재시작 후 상태 확인

Postman 등으로 정상 HTTPS 적용된 내용 확인


에러 해결 과정


에러 - 1

nginx: [emerg] host not found in upstream "web" in /etc/nginx/conf.d/nginx.conf:7

계속 Nginx가 엉뚱한 web 이라는 호스트를 찾으려고 했습니다 저는 web을 정의한 적도 없고 Docker hub에 있는 Image 또한 계속 최신화를 해주고 있었습니다
Local에서 다시 Image를 Build 하고 docker compose를 사용하여 컨테이너화 시킬때 의도적으로 spring 컨테이너는 실행시키지 않고 preonb 를 가리키도록 설정하였더니 동일하게 이번에는 preonb 호스트를 찾을 수 없다는 내용의 에러가 발생했습니다
이를 보고 지금 설정값은 문제가 없다고 판단했고 "Local에서는 문제없던게 왜 EC2에서 docker compose 만 하면 이렇지 ?" 라는 의문을 가지고 어떤게 다른지 다시 생각해보았습니다

제가 놓쳤던 것은 Nginx 이미지의 최신화 상태 였습니다 배포 cd.yml script를 통해 EC2에서 docker compose가 실행되게 되면 image: abc 와 같이 image에 적힌 내용을 통해 Docker Image를 EC2 호스트에서 찾고 없으면 docker hub에서 pull을 해오는 형태인데 이미 이전에 Nginx 이미지를 다른 블로그를 참고하며 테스트용으로 만들어놓고는 완전히 잊어버렸던 것이었습니다
그래서 기존에 있는 Nginx 이미지를 삭제하고 올바른 Docker Image(제가 Local에서 만든 이미지)를 EC2에서 Docker hub로부터 Pull 받아 docker compose 명령어로 실행하니 정상 동작하였습니다

요약하자면 EC2에서 엉뚱한 Nginx Image를 만들어서 참조하게 설정해놓고 왜 이상한 호스트를 가리키냐고 했던 것이었습니다 Nginx Image를 다시 설정해주니 문제없이 진행되었습니다


에러 - 2

DNS problem: NXDOMAIN looking up A for www.synergyy.link

Nginx 설정 중 nginx.conf에서 server_name을 synergyy.link, www.synergyy.link 와 같이 2개를 설정해주었습니다
이렇게 하다보니 Nginx가 실행되지 못하고 계속 종료되는 에러가 발생하였습니다
DNS 에러이고 www.synergyy.link를 DNS 서버에서 찾지 못한다는 에러 같았습니다
이유를 생각해보니 Route 53에서 IPv4(A)는 설정해주었지만 IPv6(AAAA)는 설정해주지 않았기 때문입니다
synergyy.link -> IPv4, www.synergyy.link -> IPv6 로 찾아야 하지만 IPv4를 DNS 서버에서 찾을때는 문제없이 설정한대로 찾았지만 server_name에 설정한 www.synergyy.link 는 Route 53에서 따로 IPv6 값을 설정해주지 않았기 때문에 찾지 못하는 것이었습니다
그래서 처음에는 Route 53에서 해당 IPv6값을 생성해주려 했었고 동일하게 IPv4에서 사용한 값을 넣었습니다 이는 AWS에서 설정한 탄력적 IP와 동일한 값이었습니다
하지만 IPv6는 당연하게도 IPv4와 다르게 128 비트로 구성되며 16비트씩 나누어 각 필드를 16진수로 표현하는 형식이었습니다 (정말 바보같았습니다 / 예시 : 2001:0db8:85a3:0000:0000:8a2e:0370:7334)
그래서 굳이 www 는 필요하지 않았기 때문에 server_name에는 synergyy.link만 설정하여 정상동작하는 것을 확인하였습니다


에러 - 3

인증서 발급 실패

이 부분은 Nginx 컨테이너를 실행하지 않아서 발생했던 간단한 문제였습니다 Nginx 컨테이너를 실행 후 실행한 Nginx 컨테이너를 대상으로 발급을 진행하니 문제없이 성공하였습니다


에러 - 4

EPROTO 77992968:error:100000f7:SSL routines:OPENSSL_internal:WRONG_VERSION_NUMBER

위와 같이 설정을 마치고 Nginx 컨테이너를 실행하니 80 port에서는 동작하는데 SSL 적용이 되지 않아 443 port에서는 동작하지 않는 즉 HTTPS 설정이 적용되지 않는 에러가 발생하였습니다

문제는 proxy pass 설정이었습니다

이전에 Docker가 아닌 EC2에 직접 Nginx를 설치하고 Nginx에서 Spring 컨테이너로 포워딩 해줄때에는 http://127.0.0.1:8080 (localhost로 하면 되지 않았었다) 으로 설정을 하여 포워딩이 잘 진행되었지만 이번에는 Docker 컨테이너 환경이었기 때문에 각각 독립적인 환경이라 127.0.0.1:8080 을 가리킨다고 해서 preonb 이름을 가진 Spring 컨테이너로 접근할 수 없었던 것이다 (당연한 얘기..)
그래서 server 세팅의 proxy pass를 proxy_pass http://preonb:8080; 로 수정하니 정상적으로 동작하였다

참고자료

0개의 댓글