Nginx Reverse Proxy & SSL 적용 왜 궁금했을까❓
Nginx를 Proxy 서버로 두어 SSL을 적용하고 내부 서버에 직접 접근하는 것을 막고자 Reverse Proxy 기술을 적용해 보려고 한다. 또한, Forward Proxy와 Reverse Proxy의 동작 원리와 차이점을 알아보고자 한다.
위 포스팅을 통해 Nginx에 대한 개념과 원리를 학습할 수 있다.
1. Proxy란?
- Proxy는 사전적 의미로 "대리"라는 의미이며, 다른 네트워크 서비스에 간접적으로 접근할 수 있게 한다.
- 서버와 클라이언트 사이에서 데이터를 중계하여 대리로 통신을 수행하는 역할이다.
1.1. Proxy Server의 목적
- 응답 속도 향상 - Proxy 서버는 캐싱을 활용하여 동일한 요청이 들어오면 캐싱된 자원을 반환한다.
- 접근 통제 - ACL을 활용하여 사이트 접근에 대한 정책(허용/차단)을 설정할 수 있다.
- 보안 - 내부 서버에 직접적으로 접근하는 것을 방지하고 SSL을 통해 네트워크 보안을 강화할 수 있다.
2. Forward Proxy
- Forward Proxy는 요청 서버에 직접 접근하지 않고 중간에 Proxy 서버를 통해 간접적으로 요청과 응답을 받아온다.
- 인터넷으로 나가기전에 사용자의 요청을 검증하여 접근 정책에 맞는 요청만 처리될 수 있도록 제한하는 역할을 한다.
- 위 그림에서 볼 수 있듯이, 사용자 요청을 Nginx가 받아서 접근 정책을 검증하고 Nginx의 IP로 인터넷에 나가는 것을 알 수 있다.
2.1. Forward Proxy 역할
- 방화벽 - 접근 통제를 통해 부적절한 클라이언트 요청을 제한함으로써 보안성을 강화할 수 있다.
- 캐시 서버 - 초기 사용자 요청을 캐싱해놓고 동일한 요청이 들어어오면 캐시에 저장한 값을 반환한다.
- 암호화 통신 - 인터넷으로 나가기전에 Proxy 서버의 IP로 변환되어 나가기 때문에 클라이언트의 IP를 추적할 수 없다.
3. Reverse Proxy
- Reverse Proxy는 실제 요청을 처리하는 내부 서버로 요청하는 것이 아닌 Reverse Proxy로 설정된 Nginx로 요청을 한다.
- Nginx는 해당 요청들을 받아 각 내부 서버에 포워딩하는 방식으로 작동하여 내부 서버 정보를 숨길 수 있다.
3.1. Reverse Proxy 역할
- 로드 밸런싱 - 트래픽이 많은 서비스는 많은 서버를 운영하는데 Nginx가 서버 상태와 알고리즘을 통해 부하가 적은 서버에 요청을 전달한다.
- 캐시 서버 - Forward Proxy와 마찬가지로 초기 사용자 요청을 캐싱해놓고 동일한 요청이 들어어오면 캐시에 저장한 값을 반환한다.
- 암호화 - 내부 서버에 통신을 forwarding 하는 역할을 담당하여 SSL과 같은 암복호화를 대신 수행하며 서버의 부하를 줄이고 보안성을 향상시킨다.
- 보안 - 내부 서버의 요청을 대신 받아 forwarding 하기 때문에 서버 IP가 직접적으로 노출되지 않아 네트워크 공격을 방어할 수 있다.
4. Reverse Proxy 적용
뉴스타 서비스에서는 Docker를 활용하여 Nginx를 구축해보려고 한다.
4.1. Nginx Config 설정 (default.conf)
server {
listen 80;
listen [::]:80;
server_name newstar.world;
access_log off;
location /.well-known/acme-challenge/ {
allow all;
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
- listen - 80 포트로 들어오는 요청을 server {}에 맞게 처리하도록 해놨으며 ipv6에서도 지원하도록 했다.
- server_name - 호스트 이름을 지정하는 곳으로 도메인을 작성하거나 Host IP를 작성하면 된다.
- access_log - 접속 로그가 쌓이는 경로를 지정하는 곳인데 실제 배포에서는 중요한 부분이지만 테스트 단계로 작성하지 않았다.
- location /.well-known/acme-challenge/ - Let's Encrypt의 경우 well-known URL을 통해 도메인을 검증하기 때문에 allow all을 통해 모든 접근을 허용하고 인증서 경로를 작성했다.
- location / - SSL 통신만을 지원하고 있기게 80 포트로 들어오는 요청에 대해서는 SSL를 주소로 redirect 되도록 설정했다.
server {
listen 443 ssl;
server_name newstar.world;
ssl_certificate /etc/letsencrypt/live/newstar.world/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/newstar.world/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
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;
location /api {
proxy_pass $spring_url;
proxy_set_header X-Forwarded-Host $server_name;
}
location /api/data/ {
proxy_pass $fastapi_url;
proxy_set_header X-Forwarded-Host $server_name;
}
location / {
proxy_pass $react_url;
}
}
- 4번째 줄 ~ 7번째 줄까지 인증서 파일이 저장될 경로를 지정해준다.
- proxy_set_header - 프록시 헤더 설정
- Host - 클라이언트 요청의 Host 헤더가 비어있다면 아무것도 전달되지 않는다고 한다. 그래서 해당 옵션을 통해 Host가 비어있다면 기본 서버 이름을 부여한다.
- X-Real-IP - 클라이언트 IP 주소
- X-Forwarded-For - 클라이언트 IP 주소를 식별하기 위한 설정으로 해당 설정이 없다면 Proxy 서버가 한것으로 기록
- X-Forwarded-Proto - Reverse Proxy 접속 시 사용한 프로토콜을 설정 (Https)
- X-Forwarded-Host - 클라이언트 호스트 이름을 식별하기 위한 설정
- proxy_pass - forwarding 할 서버 주소를 입력하면 된다.
- /api 주소로 접근하면 API Server인 Spring Boot로 forwarding 된다.
- /api/data 주소로 접근하면 GPU Server인 FastAPI로 forwarding 된다.
- / 주소로 접근하면 웹 서버 역할을 하는 nginx로 forwarding 된다.
4.2. Docker-compose 작성
version: '3'
services:
nginx:
container_name: nginx
image: nginx:latest
restart: always
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/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
restart: unless-stopped
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;'"
networks:
default:
name: app-net
external: true
- volumes 옵션을 통해 위에서 수정한 default.conf를 mount 시켜 적용한다.
- certbot 인증서가 생성될 디렉토리 또한 mount 하여 인증서를 공유한다.
- command를 통해 nginx의 경우 6시간마다 reload하고 certbot은 인증서를 12시간마다 갱신할 수 있도록 한다.
4.3. SSL 인증서 발급
vi init-letsencrpyt.sh
#!/bin/bash
# 서버 도메인
domains="j10b302.p.ssafy.io"
rsa_key_size=4096
# 인증서 경로
data_path="./data/certbot"
# 개발자 이메일
email="chanhong9784@naver.com" # Adding a valid address is strongly recommended
# 1 - 테스트, 0 - 배포
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits
if [ -d "$data_path" ]; then
read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
exit
fi
fi
if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
echo "### Downloading recommended TLS parameters ..."
mkdir -p "$data_path/conf"
curl -s <https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf> > "$data_path/conf/options-ssl-nginx.conf"
curl -s <https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem> > "$data_path/conf/ssl-dhparams.pem"
echo
fi
echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
docker compose run --rm --entrypoint "\\
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\\
-keyout '$path/privkey.pem' \\
-out '$path/fullchain.pem' \\
-subj '/CN=localhost'" certbot
echo
echo "### Starting nginx ..."
docker compose up --force-recreate -d nginx
echo
echo "### Deleting dummy certificate for $domains ..."
docker compose run --rm --entrypoint "\\
rm -Rf /etc/letsencrypt/live/$domains && \\
rm -Rf /etc/letsencrypt/archive/$domains && \\
rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo
echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
domain_args="$domain_args -d $domain"
done
# Select appropriate email arg
case "$email" in
"") email_arg="--register-unsafely-without-email" ;;
*) email_arg="--email $email" ;;
esac
# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi
docker compose run --rm --entrypoint "\\
certbot certonly --webroot -w /var/www/certbot \\
$staging_arg \\
$email_arg \\
$domain_args \\
--rsa-key-size $rsa_key_size \\
--agree-tos \\
--force-renewal" certbot
echo
echo "### Reloading nginx ..."
docker compose exec nginx nginx -s reload
인증서 발급 스크립트의 순서는 다음과 같다.
- Nginx 구동을 위해 Dummy 인증서를 발급
- Nginx Container 실행
- Dummy 인증서 삭제
- Let's Encrpyt로 인증서 발급 요청
- 발급 완료 시, nginx reload
chmod +x init-letsencrpyt.sh
./init-letsencrpyt.sh
./init-letsencrpyt.sh
이를 통해 Nginx를 Reverse Proxy로 구성하고 SSL을 적용하여 암호화 통신을 지원했다.