이전 단계

이전글에서는 Cloudflare의 고유한 HTTP 헤더에 관해 간략하게 알아보았습니다. 혹시 놓치신 분이 있다면 바로 돌아갈 수 있습니다.

이번에는 지난 번에 알아본 헤더를 활용하여, 실제로 어디에 써먹을 수 있는지 활용 사례를 알아보면 좋겠습니다.

로그 설정하기

Nginx 기본 기능을 바꾸지 않고 Cloudflare를 설정하면, 모든 방문자의 IP가 Cloudflare의 IP로 기록되는 치명적인 문제가 있습니다. 하지만 정상적으로 IP를 기록할 수 있는 방법이 있으니 걱정하지 마세요.

log_format에 선언하기

Cloudflare에서 넘어온 헤더를 Nginx에서 사용하는 것은 간단합니다. 다음과 같은 내용을 http / server 절 적당한 곳 상단에 입력해주면 됩니다.

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

$http_ 헤더 소문자, 대쉬는 언더바로 치환하여 바로 변수가 됩니다. 여기선 $http_cf_ipcountry, $http_cf_ray가 추가되어 방문자의 국가 코드와 Ray 일련번호도 동시에 기록될텐데요. 필요가 없으면 직접 수정하시면 됩니다.

여기서 왜 $http_cf_connecting_ip를 쓰지 않는지 궁금할 분들도 있을 것 같습니다. 이는 아래에서 다시 다뤄보겠습니다.

(번외) 이 로그의 효과

123.*.*.218 - - [17/May/2020:19:26:18 +0900] "GET / HTTP/1.1" 304 0 0.000 -- "https://www.google.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36" KR "594ca3b0fd091252-HKG"
40.*.*.149 - - [17/May/2020:19:32:57 +0900] "GET / HTTP/1.1" 304 0 0.000 -- "-" "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)" US "594cad6abf37cf10-IAD"
175.*.*.202 - - [17/May/2020:20:00:33 +0900] "GET / HTTP/1.1" 200 22593 0.000 -- "https://www.google.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36" KR "594cd5da2d350a68-KIX"

단편적으로 로그를 따왔는데, CF-Ray에 서버 지역 정보가 포함되어 있음을 알 수 있습니다. 이를테면 KR 이라는 똑같은 방문자 국가에 대해서 HKG (홍콩), KIX (일본서부) 같은 다양한 경로로 Cloudflare 서버를 경유를 했다는 게 나타납니다.

대한민국의 망 비용은 세계 탑급이라고 합니다. 한국 접속자는 빠른 ICN(서울) 서버가 아닌 다른 나라를 경유하여 본 서버로 오느라 근소하게 더 느릴 것을 예상할 수 있습니다.

저는 AWS 계열인 Lightsail을 쓰니까 해외망이 그리 느리지 않아 괜찮습니다.

하지만 염가 VPS를 쓰는 사람들이라면, 역시 한국 유저를 위한 Cloudflare 설정에 퍼포먼스를 염두에 둬야 하겠습니다. 아니면 Cloudflare 요금제를 비싼 것으로 끌어올리고 Argo를 적용하든가 말이죠.

다른 헤더로부터 방문객 IP 주소로 덮어씌우기

Nginx는 Cloudflare 프록시 서버를 담고 있는 방문객의 IP 주소 부분을, 실제 방문객의 IP 주소로 덮어씌울 수 있습니다. 시작하기 전에, ngx_http_realip_module 플러그인이 활성화되어 있는지 확인하세요.

$ nginx -V | grep ngx_http_realip_module
nginx version: nginx/1.19.0
... --with-http_realip_module ...

/etc/nginx/에 위치해있을, 메인 nginx.conf 설정 파일의 http 단락으로 이동합니다. 상당히 윗부분에 이미 코멘트 처리된 흔적도 보일텐데, 여기에 아래 내용을 삽입합시다.

    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 104.16.0.0/12;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 131.0.72.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 2400:cb00::/32;
    set_real_ip_from 2606:4700::/32;
    set_real_ip_from 2803:f800::/32;
    set_real_ip_from 2405:b500::/32;
    set_real_ip_from 2405:8100::/32;
    set_real_ip_from 2c0f:f248::/32;
    set_real_ip_from 2a06:98c0::/29;

이 주소들은 Cloudflare에서 사용하는 IP 주소 대역입니다. 여기 바로 아래에 이어서 붙여넣습니다.

real_ip_header CF-Connecting-IP;

이렇게 하면 CF-Connecting-IP의 내용으로 Nginx는 요청자 IP를 덮어씌우게 됩니다.

이 방법의 장점은 PHP 등 서버 스크립트의 다른 곳에서도 평소처럼 HTTP 헤더의 IP를 구해오면 방문자를 특정할 수 있다는 것입니다. 서버 기반 방문자 분석 프로그램도 정상 작동하게 만들 수 있습니다.

CF-IPCountry로 방문자 국가 판별하기

방문자 국가 판별은 서버 보안에서 꽤 중요한 부분입니다.

이를테면 관리자 페이지가 있고, 이곳은 관리자가 체류하는 나라 이외에서 들어갈 일은 없다고 봐야 합니다. 출장하는 나라가 있다면 모를까요. 아니 출장 가시더라도 한국에 적당한 VPN을 따로 두고 거길 경유해서 이용하시는게 안전합니다.

사실 Cloudflare의 대시보드를 보면 이미 이런 기능이 있음을 알 수 있습니다. Firewall 메뉴에 Firewall Rules 눌러보면 국가별 차단도 가능함을 알 수 있는데, 이건 방문자도 막아버리는지라 우리가 필요한 기능은 아닌 것 같습니다.

관리자 페이지에 접속하려는 시도인지 판별하고, 국가를 판별하여 허용 여부를 가려내면 되겠지요. 아래 설정을 http절 적당히 위쪽에 넣어주세요.

map $http_cf_ipcountry $reject_country {
        default yes;
        KR no;
        JP no;
        US no;
    }

map $request_uri $reject_sudo {
        default no;
        ~/wp-admin/? $reject_country;
        ~/wp-login(/|\.php)? $reject_country;
    }

아래는 $request_uri 기준으로 액세스를 엄격히 관리해야할 주소인지 판단합니다. 패턴이 일치하면 위에서 map한 값을 그대로 넣으니 거절해야하면 yes로 값이 들어가있을 겁니다.

location / {} 블록을 찾아서 아래 내용을 집어넣어봅시다. try_files나 다른 것들보다 가장 위에 등장해야 합니다.

if ($reject_sudo = yes) {
    return 406;
}

Nginx 전용 HTTP Response code 444를 쓰면 묻지도 따지지도 않고 바로 연결을 끊어버려서 서버에 참 좋은데, Cloudflare를 상대로 그렇게 하면 502 에러로 넘어가서 뭔가 서버에 문제가 있는 것처럼 Cloudflare를 오해하게 만듭니다. 연결이 끊긴 경우에는 Always Online 같은 서버 다운 대응 동작으로 들어가게 되니 이건 우리가 의도한 동작이 전혀 아닙니다.

따라서 HTTP 404도 아니고 적당히 돌려보낼 근거가 있는 응답 코드를 골라서 응답해주기로 했습니다. 관련 참고자료는 다음과 같습니다.

이렇게 하면 무사히 올바르게 서버의 에러 응답 그대로 클라이언트에 도달하게 되고, Brute Force 공격으로 두근거리며 들어왔을 공격자는 관리자 페이지에서 좌절을 맛보겠지요.

해외의 공격자가 관리자 페이지에 들어가면 보게될 오류

추가: 사실 특이하거나 의미심장한 오류값은 해커의 흥미를 끌 수 있습니다. 그냥 무조건 404를 출력하는게 좋아보입니다.

물론 VPN으로 나라 세탁해서 들어오거나, 국내에서 활동중인 공격자들에 대한 대책도 게을리 해서는 안 되겠지요.

다음 단계

다음으로 의도치 않게 내 서버의 실제 IP가 노출된 경우에도, Cloudflare를 거치지 않은, 직접 액세스를 거부할 수 있는 방법을 알아보고자 합니다.

2년차 개발자 이진백입니다. 현재 일본에서 웹 프로그래머로 근무중입니다. 주로 Java, Vue (Nuxt), TypeScript 언어를 활용하고 있습니다. 취미로 C#과 Windows Universal Store App을 오래 만져왔고, 개인 서버 운영에도 관심이 많습니다. 최신 이슈를 알기 쉽게 전달해드리도록, 더욱 더 노력하겠습니다.