How to block a crawler bot? with Nginx

Sentry in issues tab

Nginx를 통해 HTTP to HTTPS redirect 방법을 사용 하는 환경에서 어떻게 해외 매크로 봇이 내 서버에 접근하는 것을 막을 수 있는지 해결 과정을 공유합니다.

환경 설정

  1. Maxmind geolite2 회원 가입 후 라이센스 키를 발급 받습니다.
  2. Physical server에서 Country, City 데이터를 다운로드 받습니다.
  3. Docker-compose로 Nginx container를 관리 합니다.

STEP 1. Maxmind registration and generate license key

위 환경 설정에 Maxmind 링크를 클릭 하여 이동 한 후 회원 가입을 진행 합니다. 예시는 아래 이미지 처럼 작성 할 수 있으며 Email address 는 로그인 할 때 사용 하게 될 이메일 계정이 됩니다.

회원 가입

Maxmind/geolite2 registration

비밀 번호 변경

Email content

이메일 본문을 읽으신 뒤 “following link” 를 클릭 하시면 비밀번호를 변경 할 수 있는 Confirm view가 나오게 됩니다. 해당 브라우저에서 비밀번호를 입력 한 뒤 “Reset Password” 를 누르면 완료입니다.

Change password confirm

로그인

다시 메일 본문으로 돌아와서 “1. Access your MaxMind account” 를 클릭 하여 로그인을 진행 합니다. 이 때 로그인은 회원 가입 시 작성 했던 “Email Address”가 Username이 됩니다. 비밀번호 까지 입력이 완료 되면 “Enter Code”를 메일을 통해 받게 되니 메일을 확인한 후 코드 검증 까지 마쳐주시면 계정 생성은 완료 됩니다.

라이센스 발급

callback index page

로그인이 완료 되었다면 위와 같은 페이지가 나오게 됩니다. 왼쪽 탭에서 “Manage License Keys” 탭을 클릭 한 후 “Generate new license key” 버튼을 누르고, “Confirm” 하시면 라이센스가 정상적으로 생성이 됩니다.

지금 작성하는 시점에서 저는 이미 발급 받았기 때문에 License Key Number2로 Increase 된 것을 확인할 수 있는데요. Description은 자유롭게 작성 하시거나 Default로 설정 하셔도 상관 없습니다.

generate license key

라이센스가 정상적으로 발급이 되었다면 아래와 같이 라이센스 키와 계정 아이디 정보를 볼 수 있는데요. 이 화면에서 라이센스 키를 이용 하여 데이터를 다운로드 하게 됩니다.

successfully generated license key

STEP 2. Geo-country, Geo-city downloads

파일 변환기 다운로드

누군가 만들어 준 zip file을 dat 파일로 변환 시킬 때 편리한 파이썬 스크립트 파일을 가져옵니다.

저는 Home directory로 지정 하여 클론 받았는데, 디렉터리 위치는 편한 곳으로 지정 해주세요.

git clone https://github.com/sherpya/geolite2legacy

데이터 다운로드

데이터를 다운로드 받을 때도 마찬가지로 편한 위치에 받으시면 됩니다. 저는 클론 받은 프로젝트 디렉터리 내부에 “data” 라는 폴더를 생성 해서 그 위치에 다운로드 받았습니다.

Command

데이터를 저장 할 디렉터리로 이동 후 다운로드 커맨드를 입력 합니다.

아래 커맨드를 입력 하기 전 {license_key} 를 변경 한 뒤 커맨드를 입력 해주세요.

wget "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City-CSV&license_key=${license_key}&suffix=zip" -O ./GeoLite2-City-CSV.zip

wget "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country-CSV&license_key=${license_key}&suffix=zip" -O ./GeoLite2-Country-CSV.zip

다운로드 폴더에 GeoLite2-City-CSV.zip, GeoLite2-Country-CSV.zip 가 포함 되어 있으면 정상입니다.

파일 변환

다운로드 받은 zip 파일을 dat 파일로 변환 후 Nginx가 파일 내용을 잘 읽을 수 있도록 볼륨을 마운트 하게 될텐데요. 그 과정에서 파일을 변환 하는 작업을 거쳐야합니다.

선수 작업으로 진행 된 파일 변환기 다운로드스텝에서 클론 뜬 파이썬 스크립트 파일을 실행 할 차례 입니다.

저는 아까와 마찬가지로 클론 받은 레포지토리 내부에 data 폴더에 zip 파일을 저장 하였고, 그 위치에서 커맨드를 실행 할 예정이니 현재 위치와 디렉터리 위치를 잘 확인 하신 뒤 커맨드를 적절히 수정 하셔서 사용 하시면 됩니다.

변환 된 파일을 Docker volume과 마운트 할 디렉터리를 지정 합니다. 저는 /etc/geoip/ 라는 디렉터리를 생성 하여 서버와 도커 볼륨을 마운트 해서 사용 할 계획입니다.

아래 명령어를 입력 하여 디렉터리를 생성 하고, 소유자를 변경 해주세요.

sudo mkdir /etc/geoip/
sudo chown ubuntu:ubuntu /etc/geoip

파일 변환 시 서버 사양에 따라 시간이 소요 되며, CPU High와 풀 메모리로 서버를 한 번 Reboot 한 적 있는데요. 저는 저사양 스펙의 서버를 이용 중이기 때문에 빌드 되어있던 애플리케이션을 잠깐 모두 내린 뒤 실행 하였습니다. 아래 스펙을 참고 하신 뒤 실행 하시면 됩니다.

모두 내린 뒤 실행 한 결과 CPU 100% 점유, 60% 메모리 사용률을 고점으로 한 뒤 끝났으며 대략 수 초 걸렸던 것 같습니다. 1분이 넘지는 않았던 걸로 기억 됩니다.

SERVER-SPEC — Lightsail Instance

python3 ../geolite2legacy.py -i ./GeoLite2-Country-CSV.zip -f ../geoname2fips.csv -o /etc/geoip/GeoIP.dat

python3 ../geolite2legacy.py -i ./GeoLite2-City-CSV.zip -f ../geoname2fips.csv -o /etc/geoip/GeoLiteCity.dat

STEP 3. Nginx conf file setting

Docker compose.yaml

Example use case:

  nginx:
    restart: always
    depends_on:
        - api
    volumes:
        - static_volume:/app/staticfiles
        - media_volume:/app/mediafiles
        - /var/certbot/conf:/etc/letsencrypt/:rw
        - /var/certbot/www/:/var/www/certbot/:r
        - /etc/geoip/:/etc/nginx/geoip
    build:
        context: ./docker/prd/nginx
        dockerfile: Dockerfile
    ports:
        - "80:80"
        - "443:443"
    networks:
        - api-server

volumes 섹션에서 /etc/geoip/:/etc/nginx/geoip 내용을 본인의 프로젝트 docker-compose yaml 파일에 추가 하여 로컬 볼륨과 가상서버 볼륨을 서로 연결 해줍시다.

Nginx Dockerfile

기본 설정 파일은 모두 제거하고 새로 생성하는 파일로 변경 하였습니다.

alpine, ubuntu, debian의 설치 방법이 각자 다르기 때문에 설치 하는 모듈을 다시 확인 해 보시고 설치 하시면 됩니다.

GeoIP

해당 공식문서를 읽어 보면 OS별로 설치하는 방법이 나와있는데 모듈명이 사뭇 다른걸 확인할 수 있을텐데요. 저렇게 플러스 모듈을 사용 할 경우 제대로 설치가 되지 않았기 때문에 참고한 블로그에서 지정한 모듈명을 이용하였습니다.

Example use case:

FROM nginx:1.23.3-alpine

RUN apk add --no-cache nginx-mod-http-geoip

RUN rm /etc/nginx/nginx.conf
RUN rm /etc/nginx/conf.d/default.conf

COPY ./nginx.conf /etc/nginx/nginx.conf

COPY ./default.conf /etc/nginx/conf.d/default.conf

Nginx.conf

http와 events 정보가 담겨 있는 Nginx 설정 파일인데요. 전 프로젝트 내에서 기본 파일을 사용 하고 추가 수정을 하지 않았던 환경이었기 때문에 기본 설정 파일을 들고 나와서 내용만 조금 추가 했어요.

Example use case:

load_module /usr/lib/nginx/modules/ngx_http_geoip_module.so;  # 추가

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    geoip_country /etc/nginx/geoip/GeoIP.dat;  # 추가
    geoip_city /etc/nginx/geoip/GeoLiteCity.dat;  # 추가

    # 추가
    map $geoip_country_code $country_access {  
        "KR" 0;
        default 1;
    }

    include /etc/nginx/conf.d/*.conf;
}

“#추가” 라고 적힌 부분만 추가 하여 기본 설정 파일에서 geo module을 정확히 인식하고 사용할 수 있도록 추가 하였습니다.

conf.d/default.conf

이제 해외 IP로 접근 하는 경우 Forbidden을 맛 볼 수 있도록 403 에러를 만나게 해줍시다.

Example use case:

server {
    listen 443 ssl;
    server_name domain_name;

    ssl_certificate /etc/letsencrypt/live/domain_name/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/domain_name/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # 추가
    if ($country_access) {
        return 403;
    }

    location / {
        proxy_pass http://api;
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

추가로, 80번 포트를 Listen 하고 있는 Nginx에 접근 하는 유저가 오로지 도메인 이름으로 접근 할 수 있도록 그 외 주소는 차단 해 줍시다.

저는 현재 서브 도메인을 이용 하여 API 서버의 도메인을 설정 하였기 때문에 도메인을 1개 밖에 적용 하지 않지만, 메인 도메인을 쓰시는 분들은 제가 참고한 해당 블로그를 보시고 적용 해주세요.

Example use case:

server {
    listen 80;
    server_name api.storead.site;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    if ( $host !~* ^api\.storead\.site$ ) {
        return 444;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

Nginx에서 대부분의 요청을 처리 해줄 예정이기 때문에 임시로 열어 두었던 Lightsail Firewall의 8000번 포트를 닫아 API 서버에 직접 접근을 막아줍시다.

STEP 4. Check the nginx logs

설정한 도메인 이름이 아닌 다른 주소로 접근을 시도 한다면 아래 로그처럼 “444” 에러를 반환 하는 것을 볼 수 있습니다.

user can access only domain name

nginx-logs

👋 고통 받던 센트리가 편안해졌습니다.

이상으로 Nginx를 활용 하여 의도 하지 않은 매크로의 접근을 차단 하는 방법을 적용해 본 내용을 작성 해보았는데요. 코드를 직접 확인하고 싶으시다면 아래 깃허브 링크를 통해 보실 수 있습니다.

Source code

storead-api/docker/prd/nginx at main · LeeJuHwan/storead-api

Refference

  1. Maxmind 회원 가입
  2. Geo-country, Geo-city 데이터 다운로드
  3. Nginx geoip docs
  4. How to fix [Django] ERROR: Invalid HTTP_HOST with Nginx