프론트엔드에서 사용하는 정적 웹사이트 호스팅 서비스인 Vercel은 HTTPS를 통해 배포된다.
프로젝트의 서버 도메인이 HTTP로 되어있기 때문에, 이를 HTTPS로 보안처리를 할 필요성이 생겼다.
해당 글에서 Nginx를 이용하여 HTTPS를 적용하는 과정을 살펴본다.
일단 단순히 적용하는 것에서 그치지 않고, 단계적으로 이론을 먼저 살펴보면 좋을 것 같다.
HTTP(HyperText Transfer Protocol)
는 클라이언트와 서버 사이에 데이터를 주고받기 위한 프로토콜이다.
HTTP는 80번 포트를 사용하고 있으며, 보통 header와 body로 나누어 데이터를 전달한다.
하지만, HTTP는 별도의 암호화가 되어있지 않은 프로토콜이므로 민감 정보를 제 3자가 가로챌 수 있다.
HTTPS(HyperText Transfer Protocol Secure)
는 SSL/TLS 인증서를 통해 기존 HTTP에 보안 계층을 추가하였다. 443번 포트를 사용하며, 아래와 같은 프로세스를 통해 통신한다.
https://pickple.kr
형식을 입력하여 HTTPS 사이트를 방문한다.HTTPS는 HTTP에 비해 아래와 같은 장점이 있다.
서버(WAS)에 HTTPS를 적용할 때는 보통 리버스 프록시 서버를 두어 SSL 인증에 대한 엔드포인트 역할을 수행하도록 한다. 이렇게 하면, SSL 인증과 같은 부가 기능처리와 비즈니스 로직을 분리하여 한쪽으로 치우치는 부하를 분산시킬 수 있다.
프록시(Proxy)
란, 클라이언트와 서버 사이에 위치한 중계 서버로써 대리자 역할을 하는 것을 말한다.
클라이언트와 서버 사이에 프록시 서버를 두면, 보안이 강화되고 통신 성능이 높아진다는 장점이 있다.
크게 포워드 프록시(Forward Proxy)
와 리버스 프록시(Reverse Proxy)
로 나눌 수 있다.
정의
포워드 프록시
는 클라이언트와 인터넷 사이에 존재하는 프록시 서버를 의미한다.역할
캐시(Cache)
용도로 많이 사용한다.보안
용도로도 사용한다.정의
리버스 프록시
는 인터넷과 서버 사이에 존재하는 프록시 서버를 의미한다.역할
로드 밸런서
의 역할로 사용하여, 집중적으로 발생하는 부하를 여러 서버로 나눠 보낼 수 있다.무중단 배포
시 배포 중인 서버에 요청을 보내지 않도록 할 수 있다.보안
을 강화시킬 수 있다.리버스 프록시 서버는, 별도의 웹 서버(Apache, Nginx)를 클라이언트와 WAS 사이에 두는 방식으로 구현한다.
웹 서버(Web Server)
란, HTTP(HTTPS)를 통해 클라이언트에서 요청하는 문서나 이미지 파일 등의 정적 리소스를 전송해주는 서버를 말한다. 대표적으로 Apache HTTP Server와 Nginx가 있다.
물론 웹 서버가 반드시 정적 리소스만을 응답해야 하는 것은 아니다.
CGI
를 이용해 동적 리소스도 응답 가능하다.
웹 서버의 역할
Apache는 프로세스 / 스레드 기반 구조로 동작하는 웹 서버이다.
클라이언트로부터 요청이 들어오면 새 커넥션을 생성하기 위해 새로운 프로세스(혹은 스레드)를 할당한다.
아래와 같은 두 가지 방식으로 프로세스(혹은 스레드)를 분배한다.
Apache가 등장하고 시간이 지나면서 PC의 보급률이 높아져 점점 트래픽이 많아졌다.
클라이언트 요청 하나당 하나의 프로세스(혹은 스레드)를 생성하는 구조는 C10K
의 문제에 직면하게 된다.
C10K
문제란,Connection 10,000개 문제
라는 의미로, 요청에 의해 생겨나는 커넥션들이 10,000개를 넘어가지 못하는 문제를 말한다.
당시 CPU는 충분히 요청을 처리할 수 있었으나 Apache 구조 상, 한 개 커넥션 당 하나의 프로세스(스레드) 할당으로 인해 메모리가 버티지 못했다.
이러한 구조적인 문제를 해결하고자 2004년에 Nginx가 등장한다.
Nginx는 Event-Driven 기반 구조로 작동하는 웹 서버이다.
Apache와 달리 하나의 고정된 프로세스만 생성하고, 새로 들어오는 클라이언트의 요청은 이벤트 핸들러를 이용해 비동기 방식으로 처리한다.
요청이 늘어날 때 추가적으로 프로세스(혹은 스레드)를 할당하지 않기 때문에 메모리를 적게 사용한다.
Master-Worker 방식
Master 프로세스
와, 실제로 클라이언트의 요청을 처리하는 Worker 프로세스
로 이루어진다.하지만 Working Queue에 시간이 오래 걸리는 작업이 들어오면(ex. Disk I/O), Worker 프로세스가 해당 이벤트를 처리하는 동안 Queue에 쌓여있는 다른 이벤트들은 계속 대기하게 된다. 이는 전체적인 시간 지연을 가져올 수 있다.
따라서 Nginx에 스레드 풀(Thread Pool)
개념을 도입하여, 오래 걸리는 작업(ex. Disk I/O, File Read)를 처리하는 스레드 묶음을 따로 만들어 이를 해결한다. (1.7.11 버전 부터 도입)
이런 개선의 과정을 통해 Apache에 비해 처리 가능한 동시 커넥션 양이 100~1000배
정도 증가하게 되었다.
또한 동일한 커넥션 일 때 Apache에 비해 약 2배
정도 속도가 향상되었다.
따라서 리버스 프록시 용도로 사용할 웹 서버를 Nginx로 결정하게 되었다.
가비아에 접속 후 검색 창에 원하는 도메인을 검색한다.
아래와 같은 결과에서 원하는 도메인을 선택 후 구입한다.
가비아 DNS 관리 툴에서 가비아 네임서버에 EC2 인스턴스의 public IP를 등록한다.
DNS 관리 툴 → DNS 설정에서
설정
버튼 클릭 → 레코드 수정 → EC2 public IP 추가
api.pickple.kr
로 백엔드 API 도메인을 지정하기 위해 서브 도메인을 지정하였다.AWS EC2 인스턴스에 접속한다.
설치 가능한 패키지 목록을 최신화 하고, Nginx를 설치한다.
$ sudo apt update
$ sudo apt install nginx
정상 설치 여부를 확인한다.
$ nginx -v
nginx version: nginx/1.18.0 (Ubuntu)
ubuntu의 ufw 방화벽 설정을 통해 특정 포트만 접속할 수 있도록 설정한다.
$ sudo ufw enable # ufw 방화벽 사용
$ sudo ufw allow ssh # 22번 포트 허용
$ sudo ufw allow 'Nginx Full' # 80, 443번 포트 허용
현재 허용된 포트 번호(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)
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)
이라고 나타나면 정상적으로 실행된 것이다.Let’s Encrypt
는 무료로 SSL/TLS 인증서를 발급해주는 비영리기관이다.
Certbot
은 Let’s Encrypt를 통해 SSL/TLS 인증서를 자동으로 발급받는 일종의 봇이다.
이제 EC2 인스턴스에 Certbot을 추가하여 SSL 인증서를 발급받고, Nginx에 적용해보자.
Certbot을 설치한다.
$ sudo apt install certbot python3-certbot-nginx -y
Nginx 플러그인으로 인증서를 생성한다.
$ sudo certbot --nginx
api.pickple.kr
)Nginx를 재시작한다.
$ sudo systemctl restart nginx
/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)일 때 어떻게 처리할 것인지 정의되어 있다.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
: 암호화 복잡도를 높여주는 디피 헬만 파라미터를 설정하여 보안을 강화한다.이제 도메인으로 접속하면 자물쇠가 잘 잠겨있는 것을 확인할 수 있다!
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 문법을 이용해 자동으로 갱신할 수 있도록 하면 좋다.
Asia/Seoul 기준으로 로컬 타임을 일치시킨다.
$ sudo ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
크론탭을 연다.
$ sudo crontab -e
매 월 1일 자정에 갱신을 시도하도록 스크립트를 작성하고 저장 및 종료한다.
0 0 1 * * certbot renew --renew-hook="sudo systemctl restart nginx"
크론탭을 갱신한다.
$ sudo service cron restart