Post

[CICD] nginx, https, certbot, lets encrypt 적용기

HTTPS

이전 글에서 Cloudflare 를 활용하여 Https 적용을 했다.

[CICD] nginx, cloudflare, https, docker compose, github action 적용기

이번글에서는 Certbot, Let’s encrypt 를 활용해서 인증을 해볼려고 한다. Cloudflare 보다는 어렵지만, 한국리전을 사용할 수 없는 Cloudflare 는 너무 느리다. 이번 글에서는 이전 글에서 설명했던것은(Https, Nginx 등등) 상세한 설명은 넘어가겠다.

Architecture

이미지

이전과 달리 자체인증서를 발급하므로, 중간에 Cloudflare 를 사용할 필요가 없다. 대신 인증서를 발급받기 위해서 Certbot 을 이용한다. Certbot 은 호스트에 설치하는 방법도 있지만 docker 에 띄워서 사용했다. 그리고 Certbot 은 Let’s encrypt 인증기관을 통해 인증서를 발급받고 이를 nginx 에 설정하여 Https 통신을 할 수 있게 된다.

Let’s encrypt

HTTPS 를 적용하기위해서는 SSL/TLS 인증서를 발급받아야 한다. 해당 인증서를 발급해주는 기관을 CA(Certificate Authority) 라고하며 여러곳이 있는데 그 중 하나가 Let’s encrypt 이다 Let’s encrypt 는 고맙게로 무료로 SSL/TLS 인증서를 발급해준다. Let’s encrypt 는 인증 발급방법이 webroot, 웹서버, Standalone, DNS 로 4가지 있는데 그 중 웹서버 방식을 소개한다. 참고로 Let’s encrypt 인증서는 만료기간이 90일 이므로 주기적으로 인증서를 갱신 해주어야 한다.

Certbot

Certbot 은 Let’s encrypt 에서 인증서를 발급해주는 과정을 쉽게해주는 봇이라고 보면 된다. 원래 인증서를 발급하는데는 복잡한 절차가 필요하지만 Certbot 으로 간단하게 발급받을 수 있다.

준비물

참고로 Let’s encrypt 도 도메인주소는 필수다. 이전글에서 말했듯이 나는 가비아에서 구입하였으며, 이번에는 Cloudflare 를 사용하지 않으므로 가비아 네임 서버를 사용하면 된다. 그리고 가비아에서 DNS 레코드를 등록해줘야 한다. DNS 레코드 설정은 이전에 작성한 글과 동일하다.

인증서 발급 과정

Certbot, Nginx, Let’s encrypt 를 통해 웹서버 방식으로 인증서를 어떻게 발급받는지 설명하겠다.

  1. 호스트(나)는 도메인A 에 사용할 Let’s encrypt 에서 인증서를 발급해달라고 요청한다.
  2. Let’s encrypt 에서는 파일B 를 주면서 호스트에게 파일B에 인증문자를 넣어 달라고 요청한다.
  3. 호스트는 인증문자를 넣은 파일B 를 도메인A를 사용하는 웹서버 Nginx 에 넣어둔다.
  4. 호스트는 Let’s encrypt 에게 Nginx 를 확인해보라고 요청한다.
  5. Let’s encrypt 는 도메인A를 사용하는 웹서버 Nginx 에 인증문자를 넣은 파일B 가 있는지 확인한다.
  6. Let’s encrypt 는 파일을 확인하고 호스트에게 인증서를 발급해준다.

이런 귀찮은 과정을 수행해야 인증서를 발급받을 수 있다. 하지만 Certbot을 이용하면 저 과정들을 대신해주면서 간편하게 인증서를 발급받을 수 있는것이다. 이론은 여기까지 설명하고 어떻게 실제로 적용하는지 알아보겠다.

nginx.conf 설정

현재 내 아키텍처에서는 EC2 서버에있는 nginx.conf 를 확인하여 nginx 를 docker-compose 를 이용해 컨테이너로 띄운다. Nginx를 컨테이너로 띄우기 전에 nginx 를 미리 설정해 줄 필요가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http {

    # -------------------- spring-boot-dev WAS --------------------
    # Redirect all traffic to HTTPS
    server {
        listen 80; # 80 포트는 HTTP 를 의미한다.
        server_name dev.example.com; # 적용시킬 도메인 주소를 의미한다. 당연히 여기에는 여러분의 도메인 주소를 적어야한다.

        # certbot 이 소유자임을 확인하는 경로
        location /.well-known/acme-challenge {
            root /var/lib/letsencrypt/; # 사용자 인증을 위한 파일이 생성 되는곳
        }

        location / {
            return 308 https://$host$request_uri; # HTTP 로 오면 그대로 HTTPS로 리다이렉트 한다는 뜻이다.   
        }
    }
}

여기서 눈여겨볼점은 아래 코드다.

1
2
3
4
        # certbot 이 소유자임을 확인하는 경로
        location /.well-known/acme-challenge {
            root /var/lib/letsencrypt/; # 사용자 인증을 위한 파일이 생성 되는곳
        }

해당 코드는 Certbot, Let’s encrypt 가 인증을 확인하는 곳이다. 앞서 Certbot 은 인증문자를 넣은 파일B 를 Nginx 에 넣고, Let’s encrypt 가 인증문자를 넣은 파일B 를 확인하는 장소인 곳이다.

443 설정은 안하나요?

Https 를 사용할려면 443 포트를 설정해주어야 하지만 nginx.conf 에는 해당 과정이 없다. 스킵한게 아니라 적으면 안된다. 왜냐하면 아직 인증서를 발급받지 않았기 때문에 443 포트를 인증해줄 방법이 없다. 인증서를 발급하고 나서야 설정할 수 있다. 나는 여기서 헷갈려서 많이 삽질을 했었다.

Docker-compose 설정

EC2 서버에서는 docker-compose 를 통해 Nginx 를 컨테이너로 띄우는데 설정해줄 부분이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
services:
  back: # docker-compose 서비스명
    image: [dockerhub_id]/back:latest # 이미지 이름
    container_name: back # 실행될 컨테이너명
    ports:
      - 8080:8080 

  nginx: 
    image: nginx:1.25-alpine
    container_name: nginx
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf # EC2 에 있는 nginx 설정을 컨테이너로 복사합니다
      - /etc/letsencrypt:/etc/letsencrypt # 인증서 보관장소
      - /var/lib/letsencrypt:/var/lib/letsencrypt # cerbot 과 let's encrypt 가 소유자 확인용으로 쓰는 임시디렉토리
    ports: # nginx 는 http, https 로 주고받으니 해당 포트를 열어둔다.
      - 80:80  
      - 443:443
    depends_on:
      - back # back 서비스가 실행된후 시작한다.

여기서 눈여겨 볼 코드는 아래 코드다.

1
2
    - /etc/letsencrypt:/etc/letsencrypt # 인증서 보관장소
    - /var/lib/letsencrypt:/var/lib/letsencrypt # cerbot 과 let's encrypt 가 소유자 확인용으로 쓰는 임시디렉토리

나는 nginx 를 컨테이너로 사용하기때문에, 필요한게 있으면 EC2 서버에있는 파일 또는 디렉토리를 컨테이너로 복사(마운트) 해주어야 한다. 그래서 컨테이너 안에 인증서보관장소와, 소유자 확인용으로 사용하는 임시 디렉토리를 생성해주어야 한다. 해당 코드가 없으면 인증서 보관할곳과 소유자 확인할곳이 없어 에러가 발생한다. 참고도 volume 을 이용한 마운트는 EC2 서버에 해당 디렉토리가 없어도 컨테이너에 디렉토리를 생성하므로 EC2 서버에 디렉토리를 만들어 둘 필요는 없다.

인증서 발급

먼저 docker-compose 를 실행 해야한다. Springboot 가 연결된 Nginx 컨테이너가 띄워져 있어야 한다. 컨테이너가 잘 띄워져 있다면 아래 코드를 실행하자.

1
2
3
4
5
6
7
8
9
10
11
docker run -it --rm --name certbot \
            -v "/etc/letsencrypt:/etc/letsencrypt" \
            -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
            certbot/certbot \
            certonly \
            --webroot \
            -w /var/lib/letsencrypt \
            -d dev.example.com \
            -d www.dev.example.com \
            --agree-tos

Certbot 이 Dockerhub 에 certbot/certbot 이라는 이미지로 존재해서, Dockerhub 에 받아와서 컨테이너로 띄울 수 있다. 해당 컨테이너를 띄울건데 인증서만 발급받으면 필요 없으므로 인증서만 발급받고 컨테이너를 종료시킨다. 인증서 만료기간이 90일인데 상시로 certbot 을 컨테이너로 띄울 필요가 없기 때문이다. 그래서 certbot 를 docker-compose 에 구성하지 않았고 필요할 때만 띄워서 쓰도록 한다.

여기서 여러분이 수정해야할 부분은 도메인 부분이다.

1
2
        -d dev.example.com \
        -d www.dev.example.com \

여러분이 발급받은 도메인을 dev.example.com, www.dev.exampe.com 대신에 적어주면 된다. 인증받을 도메인이 1개라면 한줄 지워주고, 3개 이상이라면 그만큼 더 추가해주면 된다. 여러분이 정상적으로 인증서를 발급받았다면 아래 화면을 볼 수 있을것이다.

이미지

하지만 나는 이 화면을 볼 때까지 여러 에러 과정을 거쳤는데 아래 서술해두겠다.

Trouble Shooting - Connection refused

이미지

소유자임을 확인하는 /.well-known/acme-challenge 경로에 연결이 거부가 됐다고 한다. 이런 에러가 뜨는 이유는 Nginx 가 실행되지 않았기 때문이다. Nginx 컨테이너를 안띄워놓으면 Certbot 과 Let’s encrypt 가 소유자임을 확인할 장소가 없으므로 에러가 뜨는것이다.

Trouble Shooting - Unauthorized 404

이미지

해당 에러가 발생한다면, 무언가 설정을 잘못한거다. 일단 Nginx 에 연결은 됐지만 작업 수행 중에 에러가 발생했다는거다. 나 같은 경우에는 docker-compose 에서 아래 코드를 안적어서 문제가 발생했다.

1
2
    - /etc/letsencrypt:/etc/letsencrypt # 인증서 보관장소
    - /var/lib/letsencrypt:/var/lib/letsencrypt # cerbot 과 let's encrypt 가 소유자 확인용으로 쓰는 임시디렉토리

해당 디렉토리를 컨테이너에 만들어두지 않았으니, Certbot 과 Let’s encrypt 가 작업을 할 수 없어서 에러가 발생했다.

Trouble Shooting - Too many failed authorizations recently

이미지

말그대로 최근에 너무 많은 인증서발급을 실패했는 뜻이다. 1시간내에 5번넘게 실패하면 발생한다. 삽질을 할때도 신중하게 하라는 뜻이다.. 해당 에러가 떴다면 잠시 머리를 식히고 오자.

Trouble Shooting - Does not support any combination of challenges

이미지

도메인을 추가할려다가 해당 에러를 마주쳤다. 도메인에 적절한 IP 가 아니면 발생하는것 같다. 이게 무슨 뜻이면, 내가 dev.example.com 을 DNS 레코드로 13.24.32.55 IP 를 설정해놨다면, Springboot 를 띄운 서버의 IP 도 13.24.32.55 이어야 한다는 뜻이다. 그런데 내가 prod.example.com 을 DNS 레코드로 24.35.43.66 IP 를 설정해놨는데 Springboot 를 띄운 서버의 IP 가 13.24.32.55 면 서로 IP 가 다르기 때문에 에러가 발생한다.

인증서 확인

내가 인증서를 발급했는데 잘 발급했는지 확인하고 싶으면 아래 코드를 사용해서 확인할 수 있다. Certbot 컨테이너를 띄워 인증서를 발급했기때문에 docker 로 다시 컨테이너를 띄워서 확인해야한다.

1
docker run --rm -it -v "/etc/letsencrypt:/etc/letsencrypt" certbot/certbot certificates

인증서 삭제

혹시 인증서를 잘못 발급했다면, 아래 명령어를 통해 인증서를 삭제할 수 있다.

1
docker run --rm -it -v "/etc/letsencrypt:/etc/letsencrypt" certbot/certbot delete --cert-name dev.example.com

Nginx 설정

인증서를 잘 발급받았다면, nginx 컨테이너 안에 인증서와 개인키가 발급되어 있다. 그렇다면 nginx 서버에 https 설정을 추가해주어야 한다. nginx.conf 를 아래같이 다시 수정해 준다.

nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
http {

    # -------------------- spring-boot-dev WAS --------------------

     upstream backend {
        server back:8080; # back:8080 을 정의한다. 여기서 back 은 docker-compose 의 서비스명이다. ip 를 적어서 통신을 하게해도 된다.
    }

    # Redirect all traffic to HTTPS
    server {
        listen 80; # 80 포트는 HTTP 를 의미한다.
        server_name dev.example.com; # 적용시킬 도메인 주소를 의미한다. 당연히 여기에는 여러분의 도메인 주소를 적어야한다.

        # certbot 이 소유자임을 확인하는 경로
        location /.well-known/acme-challenge {
            root /var/lib/letsencrypt/; # 사용자 인증을 위한 파일이 생성 되는곳
        }

        location / {
            return 308 https://$host$request_uri; # HTTP 로 오면 그대로 HTTPS로 리다이렉트 한다는 뜻이다.   
        }
    }

    server {
        listen 443 ssl;
        server_name dev.example.com;

        ssl_certificate /etc/letsencrypt/live/dev.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/dev.example.com/privkey.pem;

        location / {
            proxy_pass http://backend;
            proxy_set_header Host $host; # 클라이언트가 요청한 호스트의 도메인
            proxy_set_header X-Real-IP $remote_addr; # 클라이언트의 실제 IP 주소
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 원격 클라이언트의 실제 IP 주소와, 이전에 거친 프록시 서버의 IP 주소들이 쉼표로 구분되어 포함
        }
    }
}

여기서 눈여겨볼 코드는 아래와 같다.

1
2
ssl_certificate /etc/letsencrypt/live/dev.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/dev.example.com/privkey.pem;

인증서를 성공적으로 발급받으면, 인증서와 개인키가 위의 경로에 발급된다. Nginx 는 이 개인키와 인증서를 이용해 HTTPS 통신을 할 수 있게 된다. 인증서를 발급받기전에는 해당키가 없으므르, 발급받기전에 위 코드를 사용하면 인증서와 개인키가 없다고 에러가 난다.

Nginx 재시작

nginx.conf 수정을 완료했다면 Nginx 컨테이너를 재시작 하자. 정상적으로 실행이 됐다면 여러분이 설정한 도메인주소에 https 로 접속이 가능하다!

HTTPS 통신과정

HTTPS 통신과정을 간략하게 소개하고 넘어간다. 바쁘다면 넘어가도록 하자. 인증서는 서버(Nginx) 의 public key 와 인증기관 서명(signature) 으로 이루어져있다.

  1. 클라이언트가 Nginx 에게 Https 통신을 요청한다.
  2. Nginx 는 발급받은 인증서를 클라이언트에게 준다.
  3. 클라이언트는 인증서의 인증기관(Let’s encrypt) 를 확인하고 인증기관의 public key 를 받는다. 그리고 해당 키로 인증서를 검증하여 Nginx의 public key 를 얻는다.
  4. 클라이언트는 client key 를 생성하고 Nginx의 public key 로 client key 를 암호화하여 Nginx 에 보낸다.
  5. Nginx 는 Nginx 의 public key 로 암호호된 client key 를, Nginx 의 private key 로 복호화 하여 client key 를 얻는다.
  6. 결과적으로 클라이언트는 client key, nginx public key 를 알고, Nginx 는 client key, nginx public key, nginx private key 를 알고 있다. 그렇기에 내용을 주고받을때 암호화해서 보낼 수 있어 HTTPS 통신을 할 수 있는것이다.

해당과정이 HTTPS 통신과정이며 생각보다 복잡했다. 그래서 Springboot 에서 처리하지않고 Nginx 에서처리해 Springboot 서버에 가해지는 부하를 줄일 수 있다.

인증서 자동 갱신

Let’s encrypt 인증서는 유효기간이 90일 이다. 그래서 90일이 지나면 재발급을 받아야 한다. 생각보다 긴 시간은 아니기에 재발급을 자동화 해보자.

EC2 서버에서 아래 명령어를 적어주자.

1
crontab -e

그리고 아래 명령어를 적어 자동화하자.

1
2
0 5 5,15,25 * * /usr/bin/docker run --rm --name certbot -v "/etc/letsencrypt:/etc/letsencrypt" -v "/var/lib/letsencrypt:/var/lib/letsencrypt" certbot/certbot renew >> /var/log/certbot_renew.log
10 5 5,15,25 * * /usr/bin/docker restart nginx

해당 명령어는 5일 ,15일, 25일, 새벽5시 마다 인증서를 갱신해준다. 갱신했다면 로그를 남겨둔다. 그리고 새벽5시 10분에는 nginx 를 재시작한다. 재시작하는 이유는 인증서를 갱신했다면 재시작을 해줘야 되기 때문이다.

마무리

자체인증서를 이용하는데 많은 삽질을 했었다. 특히 인증서 발급전과 발급후에 nginx.conf 를 다르게 설정해주어야 되서 헷갈렸다. SSL/TLS 를 적용할려고 급급하게 nginx 를 사용했었는데, nginx 는 사실 많은 기능을 제공한다. 다음에는 SSL/TLS 기능만 사용하지 말고 다른 캐싱기능, 로드밸런싱기능 등을 사용해볼려고 한다.

Reference

HTTPS 작동원리
Certbot, Nginx, Let’s encrypt 로 https 연결
Let’s encrypt 설명과 발급방법 종류

This post is licensed under CC BY 4.0 by the author.