ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Infra] Nginx Reverse Proxy & SSL 적용
    프로젝트/뉴스타 2024. 6. 10. 08:56

    Nginx Reverse Proxy & SSL 적용 왜 궁금했을까❓

    Nginx를 Proxy 서버로 두어 SSL을 적용하고 내부 서버에 직접 접근하는 것을 막고자 Reverse Proxy 기술을 적용해 보려고 한다. 또한, Forward Proxy와 Reverse Proxy의 동작 원리와 차이점을 알아보고자 한다.
     

    [Infra] Nginx 란?

    Nginx 왜 궁금했을까❓뉴스타 프로젝트에서 인프라 구축을 담당하여 Reverse Proxy, SSL, LoadBalancing 등의 장점이 있는 Nginx를 활용해 보기로 했다. 해당 포스트를 통해 Apache Server와 Nginx의 동작 원리와

    pslog.co.kr

    위 포스팅을 통해 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
    인증서 발급 스크립트의 순서는 다음과 같다.
    1. Nginx 구동을 위해 Dummy 인증서를 발급
    2. Nginx Container 실행
    3. Dummy 인증서 삭제
    4. Let's Encrpyt로 인증서 발급 요청
    5. 발급 완료 시, nginx reload
    chmod +x init-letsencrpyt.sh
    ./init-letsencrpyt.sh
    
    ./init-letsencrpyt.sh
    

     

    이를 통해 Nginx를 Reverse Proxy로 구성하고 SSL을 적용하여 암호화 통신을 지원했다.
Designed by Tistory.