[Infra] 리버스 프록시를 이용한 HTTPS 적용하기 with NginX

kyle·2023년 11월 13일
3

프론트엔드에서 사용하는 정적 웹사이트 호스팅 서비스인 Vercel은 HTTPS를 통해 배포된다.
프로젝트의 서버 도메인이 HTTP로 되어있기 때문에, 이를 HTTPS로 보안처리를 할 필요성이 생겼다.
해당 글에서 Nginx를 이용하여 HTTPS를 적용하는 과정을 살펴본다.
일단 단순히 적용하는 것에서 그치지 않고, 단계적으로 이론을 먼저 살펴보면 좋을 것 같다.

1. HTTP vs HTTPS

1.1. HTTP

HTTP(HyperText Transfer Protocol)는 클라이언트와 서버 사이에 데이터를 주고받기 위한 프로토콜이다.

HTTP는 80번 포트를 사용하고 있으며, 보통 header와 body로 나누어 데이터를 전달한다.

하지만, HTTP는 별도의 암호화가 되어있지 않은 프로토콜이므로 민감 정보를 제 3자가 가로챌 수 있다.


1.2. HTTPS

HTTPS(HyperText Transfer Protocol Secure)는 SSL/TLS 인증서를 통해 기존 HTTP에 보안 계층을 추가하였다. 443번 포트를 사용하며, 아래와 같은 프로세스를 통해 통신한다.

  1. 브라우저에 https://pickple.kr 형식을 입력하여 HTTPS 사이트를 방문한다.
  2. 브라우저는 서버에 SSL 인증서를 요청하여 사이트의 신뢰성을 검증하려고 시도한다.
  3. 서버는 공개키가 포함된 SSL 인증서를 응답한다.
  4. 브라우저는 SSL 인증서의 유효성을 검증한다. 이후 공개키를 사용하여 세션키를 발급하고 서버에 전송한다.
  5. 서버는 비밀키를 사용하여 브라우저에서 발급한 세션키를 얻는다.
  6. 브라우저와 서버는 동일한 세션키를 공유하므로, 이후 데이터 전달 시 세션키를 통해 암호화/복호화를 한다.

HTTPS는 HTTP에 비해 아래와 같은 장점이 있다.

  • 세션키를 통해 데이터를 암호화한 형태로 전달하기 때문에 민감 정보를 보호할 수 있다.
  • 검색 엔진에게 HTTP보다 HTTPS가 신뢰성이 더 높기 때문에, 웹사이트 컨텐츠 순위를 더 높게 받을 수 있다.

서버(WAS)에 HTTPS를 적용할 때는 보통 리버스 프록시 서버를 두어 SSL 인증에 대한 엔드포인트 역할을 수행하도록 한다. 이렇게 하면, SSL 인증과 같은 부가 기능처리와 비즈니스 로직을 분리하여 한쪽으로 치우치는 부하를 분산시킬 수 있다.


2. 리버스 프록시란?

프록시(Proxy)란, 클라이언트와 서버 사이에 위치한 중계 서버로써 대리자 역할을 하는 것을 말한다.

클라이언트와 서버 사이에 프록시 서버를 두면, 보안이 강화되고 통신 성능이 높아진다는 장점이 있다.

크게 포워드 프록시(Forward Proxy)리버스 프록시(Reverse Proxy)로 나눌 수 있다.

2.1. 포워드 프록시(Forward Proxy)

정의

  • 포워드 프록시클라이언트와 인터넷 사이에 존재하는 프록시 서버를 의미한다.
  • 클라이언트가 서버에 직접 요청하지 않고, 포워드 프록시 서버가 해당 요청을 받아서 서버에 전달한 후 그 응답 값을 클라이언트에게 전달한다.

역할

  • 주로 캐시(Cache) 용도로 많이 사용한다.
  • 기업에서는 내부망에 포워드 프록시를 두어 정해진 사이트만 연결하는 등의 보안 용도로도 사용한다.

2.2. 리버스 프록시(Reverse Proxy)

정의

  • 리버스 프록시인터넷과 서버 사이에 존재하는 프록시 서버를 의미한다.
  • 클라이언트의 요청을 서버가 직접 받지 않고, 리버스 프록시 서버가 해당 요청을 받아서 서버에 전달한 후 그 응답 값을 인터넷으로 전달한다. 클라이언트는 프록시 뒤의 서버의 존재를 모르게 된다.

역할

  • 로드 밸런서의 역할로 사용하여, 집중적으로 발생하는 부하를 여러 서버로 나눠 보낼 수 있다.
  • 클라이언트의 요청을 나눠서 보낼 수 있으므로, 무중단 배포 시 배포 중인 서버에 요청을 보내지 않도록 할 수 있다.
  • 서버는 내부망에 넣고, 리버스 프록시 서버는 외부에 두어 보안을 강화시킬 수 있다.

리버스 프록시 서버는, 별도의 웹 서버(Apache, Nginx)를 클라이언트와 WAS 사이에 두는 방식으로 구현한다.


3. Apache vs Nginx

웹 서버(Web Server)란, HTTP(HTTPS)를 통해 클라이언트에서 요청하는 문서나 이미지 파일 등의 정적 리소스를 전송해주는 서버를 말한다. 대표적으로 Apache HTTP Server와 Nginx가 있다.

물론 웹 서버가 반드시 정적 리소스만을 응답해야 하는 것은 아니다. CGI를 이용해 동적 리소스도 응답 가능하다.

웹 서버의 역할

  • 동적 리소스를 응답하는 WAS(웹 애플리케이션 서버) 앞에 붙어, 단순한 정적 리소스를 반환하는 역할을 하여 WAS에 가해지는 부하를 줄여준다.
  • WAS의 존재를 숨겨서 보안을 강화시킨다.
  • WAS에 장애가 났을 때 에러 페이지를 응답함으로써 사용자 경험을 해치지 않도록 한다.

3.1. Apache HTTP Server

Apache는 프로세스 / 스레드 기반 구조로 동작하는 웹 서버이다.

클라이언트로부터 요청이 들어오면 새 커넥션을 생성하기 위해 새로운 프로세스(혹은 스레드)를 할당한다.

아래와 같은 두 가지 방식으로 프로세스(혹은 스레드)를 분배한다.

  1. Prefork 방식
    • 하나의 클라이언트 요청에 하나의 자식 프로세스를 할당한다.
    • 따라서 요청이 늘어나면 프로세스도 함께 늘어난다.
    • 하나의 자식 프로세스 당 하나의 스레드를 가지며, 총 1024개의 자식 프로세스를 할당 가능하다.
  2. Worker 방식
    • 하나의 요청에 하나의 프로세스를 할당하지 않고, 하나의 요청마다 자식 프로세스에 하나의 스레드를 새로 할당한다.
    • 하나의 자식 프로세스 당 여러 개의 스레드를 가진다.
    • 따라서 Prefork에 비해 비교적 메모리를 적게 사용한다.

Apache가 등장하고 시간이 지나면서 PC의 보급률이 높아져 점점 트래픽이 많아졌다.

클라이언트 요청 하나당 하나의 프로세스(혹은 스레드)를 생성하는 구조는 C10K의 문제에 직면하게 된다.

C10K 문제란, Connection 10,000개 문제라는 의미로, 요청에 의해 생겨나는 커넥션들이 10,000개를 넘어가지 못하는 문제를 말한다.
당시 CPU는 충분히 요청을 처리할 수 있었으나 Apache 구조 상, 한 개 커넥션 당 하나의 프로세스(스레드) 할당으로 인해 메모리가 버티지 못했다.

이러한 구조적인 문제를 해결하고자 2004년에 Nginx가 등장한다.


3.2. Nginx

Nginx는 Event-Driven 기반 구조로 작동하는 웹 서버이다.

Apache와 달리 하나의 고정된 프로세스만 생성하고, 새로 들어오는 클라이언트의 요청은 이벤트 핸들러를 이용해 비동기 방식으로 처리한다.

요청이 늘어날 때 추가적으로 프로세스(혹은 스레드)를 할당하지 않기 때문에 메모리를 적게 사용한다.

Master-Worker 방식

  • Nginx는 설정 파일을 읽고 Worker 프로세스를 생성하는 Master 프로세스와, 실제로 클라이언트의 요청을 처리하는 Worker 프로세스로 이루어진다.
  • Nginx를 구동하면, Master 프로세스는 정해진 수만큼의 Worker 프로세스를 생성한다.
    (보통 CPU 코어 개수만큼 Worker 프로세스를 생성하여 각 코어의 컨텍스트 스위치 비용을 줄인다.)
  • Worker 프로세스는 Working Queue에 담긴 이벤트(ex. 커넥션 연결, 요청 처리, 커넥션 종료 등)들을 순차적으로 처리한다. 새로운 프로세스(혹은 스레드)를 생성하지 않기 때문에 이 Worker 프로세스는 끊임없이 작업을 수행한다.
  • 따라서 커넥션마다 프로세스(혹은 스레드)를 할당하던 Apache와 달리 메모리를 적게 점유하면서 훨씬 효율적으로 요청을 처리한다.

하지만 Working Queue에 시간이 오래 걸리는 작업이 들어오면(ex. Disk I/O), Worker 프로세스가 해당 이벤트를 처리하는 동안 Queue에 쌓여있는 다른 이벤트들은 계속 대기하게 된다. 이는 전체적인 시간 지연을 가져올 수 있다.

따라서 Nginx에 스레드 풀(Thread Pool) 개념을 도입하여, 오래 걸리는 작업(ex. Disk I/O, File Read)를 처리하는 스레드 묶음을 따로 만들어 이를 해결한다. (1.7.11 버전 부터 도입)

  1. 오래 걸리는 작업은 Worker 프로세스에서 Task Queue로 이관된다.
  2. Task Queue는 스레드 풀에 있는 스레드들 중 하나에 작업을 할당한다.
  3. 작업이 완료되면 다시 Worker 프로세스에게 결과를 전달한다.

이런 개선의 과정을 통해 Apache에 비해 처리 가능한 동시 커넥션 양이 100~1000배 정도 증가하게 되었다.
또한 동일한 커넥션 일 때 Apache에 비해 약 2배 정도 속도가 향상되었다.
따라서 리버스 프록시 용도로 사용할 웹 서버를 Nginx로 결정하게 되었다.


4. HTTPS 적용하기

4.1. 도메인 구입 및 DNS 등록

  1. 가비아에 접속 후 검색 창에 원하는 도메인을 검색한다.

  2. 아래와 같은 결과에서 원하는 도메인을 선택 후 구입한다.

  3. 가비아 DNS 관리 툴에서 가비아 네임서버에 EC2 인스턴스의 public IP를 등록한다.

    DNS 관리 툴 → DNS 설정에서 설정 버튼 클릭 → 레코드 수정 → EC2 public IP 추가

    • 프로젝트에서 api.pickple.kr로 백엔드 API 도메인을 지정하기 위해 서브 도메인을 지정하였다.

4.2. EC2에 Nginx 설치

  1. AWS EC2 인스턴스에 접속한다.

  2. 설치 가능한 패키지 목록을 최신화 하고, Nginx를 설치한다.

    $ sudo apt update
    $ sudo apt install nginx
  3. 정상 설치 여부를 확인한다.

    $ nginx -v
    
    nginx version: nginx/1.18.0 (Ubuntu)
  4. ubuntu의 ufw 방화벽 설정을 통해 특정 포트만 접속할 수 있도록 설정한다.

    $ sudo ufw enable              # ufw 방화벽 사용
    $ sudo ufw allow ssh           # 22번 포트 허용
    $ sudo ufw allow 'Nginx Full'  # 80, 443번 포트 허용
  5. 현재 허용된 포트 번호(22, 80, 443)를 확인한다.

    $ sudo ufw status
    
    Status: active
    
    To                         Action      From
    --                         ------      ----
    Nginx Full                 ALLOW       Anywhere
    22/tcp                     ALLOW       Anywhere
    Nginx Full (v6)            ALLOW       Anywhere (v6)
    22/tcp (v6)                ALLOW       Anywhere (v6)
  6. Nginx를 실행하고 상태를 확인한다.

    $ sudo systemctl start nginx
    $ sudo systemctl status nginx
    
    ● nginx.service - A high performance web server and a reverse proxy server
         Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
         Active: active (running) since Fri 2023-10-27 12:57:46 KST; 17min ago
           Docs: man:nginx(8)
        Process: 443 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
        Process: 536 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
       Main PID: 586 (nginx)
          Tasks: 2 (limit: 2329)
         Memory: 13.3M
            CPU: 71ms
         CGroup: /system.slice/nginx.service
                 ├─586 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
                 └─596 "nginx: worker process" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""
    
    Oct 27 12:57:45 ip-172-31-35-220 systemd[1]: Starting A high performance web server and a reverse proxy server...
    Oct 27 12:57:46 ip-172-31-35-220 systemd[1]: Started A high performance web server and a reverse proxy server.
    • active (running)이라고 나타나면 정상적으로 실행된 것이다.

4.3. Let’s Encrypt와 Certbot을 이용해 SSL 인증서 적용하기

Let’s Encrypt는 무료로 SSL/TLS 인증서를 발급해주는 비영리기관이다.

Certbot은 Let’s Encrypt를 통해 SSL/TLS 인증서를 자동으로 발급받는 일종의 봇이다.

이제 EC2 인스턴스에 Certbot을 추가하여 SSL 인증서를 발급받고, Nginx에 적용해보자.

  1. Certbot을 설치한다.

    $ sudo apt install certbot python3-certbot-nginx -y
  2. Nginx 플러그인으로 인증서를 생성한다.

    $ sudo certbot --nginx
    • 이후 이메일 입력 칸이 나오면 자신의 이메일을 입력한다.
    • 약관 동의에 Y를 입력한다.
    • 마지막으로 인증서를 등록할 도메인을 입력하라고 나오는데, 가비아에서 등록했던 도메인을 입력한다. (여기서는 api.pickple.kr)
  3. Nginx를 재시작한다.

    $ sudo systemctl restart nginx
  4. /etc/nginx/nginx.conf 를 살펴보면, 아래와 같은 정보가 추가되었음을 알 수 있다.

    server {
    	server_name api.pickple.kr;
    
    	location / {
    		proxy_pass http://localhost: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;
    	}
    
    	listen 443 ssl; # managed by Certbot
    	ssl_certificate /etc/letsencrypt/live/api.pickple.kr/fullchain.pem; # managed by Certbot
    	ssl_certificate_key /etc/letsencrypt/live/api.pickple.kr/privkey.pem; # managed by Certbot
    	include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    	ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
    }
    
    server {
    	if ($host = api.pickple.kr) {
           	return 301 https://$host$request_uri;
    	} # managed by Certbot
    
    	listen 80;
    	server_name api.pickple.kr;
    	return 404; # managed by Certbot
    }
    • server : 블록을 통해 http(80)일 때와 https(443)일 때 어떻게 처리할 것인지 정의되어 있다.
      • https 일 때는 스프링 서버로 보내고
      • http일 때는 301 MOVED PERMANENTLY 를 응답하여 https로 리다이렉트한다.
    • listen : 어떤 포트의 요청을 처리할 것인지 명시한다. (80 - http, 443 - https)
    • server_name : 어떤 도메인의 요청을 처리할 것인지 명시한다. 여러 개 지정도 가능하다.
    • location : 어떤 endpoint의 요청을 처리할 것인지 명시한다.
      • proxy_pass : 해당 요청을 지정한 경로로 포워딩한다. (스프링 서버랑 매칭 시켜줌)
      • proxy_set_header : 포워딩 시의 헤더 값을 정의한다. ($xxx 가 일종의 변수가 됨)
    • ssl_certificate, ssl_certificate_key : SSL 인증서 및 비밀키 경로를 지정한다.
    • include : 특정 파일의 설정을 불러온다. (여기서는 options-ssl-nginx.conf 를 통해 SSL 관련 설정을 불러옴)
    • ssl_dhparam : 암호화 복잡도를 높여주는 디피 헬만 파라미터를 설정하여 보안을 강화한다.
  5. 이제 도메인으로 접속하면 자물쇠가 잘 잠겨있는 것을 확인할 수 있다!


5. [번외] Certbot cron을 이용한 SSL 인증서 자동 갱신

Let’s Encrypt의 경우 90일 짜리 SSL 인증서이기 때문에 만료기간이 존재한다.

아래의 명령어를 입력해서 인증서 만료기간을 확인할 수 있다.

$ sudo certbot certificates

Saving debug log to /var/log/letsencrypt/letsencrypt.log

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
  Certificate Name: api.pickple.kr
    Serial Number: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    Key Type: RSA
    Domains: api.pickple.kr
    Expiry Date: 2024-01-23 18:01:27+00:00 (VALID: 85 days)
    Certificate Path: /etc/letsencrypt/live/api.pickple.kr/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/api.pickple.kr/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

이를 갱신하기 위해서는 직접 사용자가 아래의 명령어를 주기적으로(2, 3개월에 한번씩) 입력해야한다.

$ sudo certbot renew

하지만 너무 불편하기도 하고, 까먹을 수도 있기 때문에 cron 문법을 이용해 자동으로 갱신할 수 있도록 하면 좋다.

  1. Asia/Seoul 기준으로 로컬 타임을 일치시킨다.

    $ sudo ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
    • 심볼릭 링크를 생성하여 덮어씌우는 방식으로 설정하였다.
  2. 크론탭을 연다.

    $ sudo crontab -e
    • 에디터는 vim을 선택한다.
  3. 매 월 1일 자정에 갱신을 시도하도록 스크립트를 작성하고 저장 및 종료한다.

    0 0 1 * * certbot renew --renew-hook="sudo systemctl restart nginx"

  4. 크론탭을 갱신한다.

    $ sudo service cron restart
profile
공유를 기반으로 선한 영향력을 주는 개발자가 되고 싶습니다.

0개의 댓글

관련 채용 정보