클라우드플레어 뒤 nginx 로그에 방문자 IP가 안 찍힐 때 해결법

먼저 결론

nginx 접속 기록의 IP가 전부 클라우드플레어 주소로 찍힙니다. nginx에 진짜 IP를 꺼내는 설정(set_real_ip_from, real_ip_header)이 없습니다. 클라우드플레어 대역을 모두 set_real_ip_from 으로 적고 real_ip_header CF-Connecting-IP 를 넣은 뒤 nginx를 다시 읽힙니다.

클라우드플레어 프록시를 켠 워드프레스 서버에서 접속 기록을 열었더니 IP가 모두 같은 몇 개 대역이었습니다. 이 상태에서는 공격을 IP로 가를 수도 없고, 로그인 차단 플러그인도 제 역할을 못 합니다. 아래는 실제로 겪고 고친 순서입니다.

접속 기록이 전부 클라우드플레어 주소

서치콘솔에 5xx 오류가 늘어서 원인을 쫓다가 nginx 접속 기록을 봤습니다. IP 칸이 162.159, 172.64 로 시작하는 클라우드플레어 대역뿐이었습니다.

그 서버에는 하루 1만 회쯤 들어오는 로그인 비밀번호 대입 공격이 있었습니다. 그런데 모든 요청이 클라우드플레어 주소로 보이니 IP로 막을 방법이 없었습니다. 로그인 시도 횟수를 IP별로 세는 플러그인도 사실상 아무 일을 하지 못했습니다. 결국 IP 없이 User-Agent 만 보고 원인이 사람 방문이 아니라 봇과 공격이라는 것을 겨우 찾았습니다.

nginx는 연결한 상대를 방문자로 기록한다

클라우드플레어를 거치면 서버에 직접 연결하는 쪽은 방문자가 아니라 클라우드플레어입니다. 진짜 방문자 주소는 CF-Connecting-IP 라는 요청 헤더에 따로 실려 옵니다.

nginx의 realip 모듈(ngx_http_realip_module, nginx 공식 문서에 있는 모듈)은 믿을 수 있는 프록시 주소를 set_real_ip_from 으로 알려 주면, real_ip_header 로 지정한 헤더 값을 방문자 주소로 바꿔 줍니다. 이 설정이 없었기 때문에 nginx는 연결한 상대, 곧 클라우드플레어 주소를 그대로 기록했습니다.

real_ip 설정과 함께 건 것들

먼저 서버에 이미 설정이 있는지 확인합니다.

grep -r real_ip /etc/nginx/

아무것도 안 나오면 conf.d 아래에 파일을 하나 만듭니다. 아래 대역은 당시 적용한 값입니다. 클라우드플레어가 대역을 바꿀 수 있으니 적용할 때는 공식 IP 목록 페이지와 대조하는 것이 좋습니다.

# /etc/nginx/conf.d/cloudflare-real-ip.conf
set_real_ip_from 173.245.48.0/20;
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 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
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 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
real_ip_header CF-Connecting-IP;

진짜 IP가 보이자 공격을 다시 셀 수 있었습니다. 공격은 598개 주소로 흩어져 있어서 IP당 제한만으로는 거의 걸리지 않았습니다. 그래서 다음을 같이 걸었습니다.

  • wp-login 요청 제한: 1분에 2회
  • 수집봇 User-Agent에 403
  • PHP 작업자 수 5에서 20으로 증설
  • 페이지 캐시

fail2ban 은 이보다 앞선 5월에 걸어 두었습니다. 이번에 다시 들여다보니 한 번도 막은 적이 없었고, 이유가 두 가지 있었습니다. 걸어 둔 설정부터 적습니다.

# /etc/fail2ban/filter.d/wordpress-login.conf
[Definition]
failregex = ^<HOST> -.*"POST /wp-login\.php HTTP.*" (200|301|302|401|403)
ignoreregex =

# /etc/fail2ban/jail.d/wordpress-login.conf
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/*.access.log
findtime = 300
maxretry = 5
bantime = 3600
bantime.increment = true
bantime.factor = 24
bantime.maxtime = 604800

10월에 fail2ban-client status wordpress-login 을 열어 보니 다섯 달 동안 실패 0, 차단 0이었습니다. 그날 하루 접속 기록에만 로그인 시도가 400줄 넘게 있었고, fail2ban-regex 로 필터를 돌리면 걸리는 줄도 나왔습니다. 필터는 맞는데 감옥이 파일을 읽지 않고 있었습니다.

상태 화면에 Journal matches: 줄이 보이면 파일이 아니라 systemd 저널을 읽는 중입니다. 제 서버(우분투 24.04, fail2ban 1.0.2)에서는 기본 파일 /etc/fail2ban/jail.d/defaults-debian.conf 에 backend = systemd 가 들어 있어서, 감옥에 logpath 를 적어도 그 파일은 보지 않습니다. 파일을 읽게 하려면 감옥에 backend = auto 를 따로 넣어야 합니다. 이 수정의 효과는 아직 확인하기 전입니다.

둘째는 구조 문제라 시험하지 않아도 알 수 있는 것입니다. fail2ban 의 기본 차단은 서버 방화벽에서 IP를 막는 것인데, 프록시를 켜 두면 서버에 직접 연결하는 상대는 언제나 클라우드플레어 주소입니다. 로그에는 real_ip 덕분에 진짜 IP가 찍히지만 방화벽은 그 IP를 볼 일이 없으니, 차단 목록이 쌓여도 실제로는 막히지 않습니다. 클라우드플레어 뒤에서 fail2ban 을 쓰려면 클라우드플레어 쪽에서 막는 동작으로 바꿔야 하는데, 저는 그것까지는 시험하지 못했습니다. 그래서 지금 이 서버에서 fail2ban 은 막는 장치로 세지 않습니다.

로그인 차단 플러그인 값은 재시도 3회, 잠금 3600초, 긴 잠금 604800초로 두었습니다.

같이 밟기 쉬운 함정 두 가지

함정 증상 대처
location 안에 add_header 를 넣음 server 영역의 HSTS 같은 보안 헤더가 그 location 에서 사라짐 그 location 안에 같은 보안 헤더를 다시 적음
아스트라 테마에 페이지 캐시 휴대폰과 PC가 body 클래스를 다르게 받아야 하는데 한쪽 화면이 다른 쪽에 나갈 수 있음 캐시 키에 기기 구분을 넣음

nginx는 하위 블록에 add_header 가 하나라도 있으면 상위 블록의 add_header 를 물려받지 않습니다. 캐시 설정을 하다가 헤더 하나를 넣는 순간 보안 헤더가 빠지니 주의해야 합니다.

고쳐졌는지 확인하는 법

  1. nginx -t 로 문법을 확인하고 다시 읽힙니다.
  2. 접속 기록 끝부분을 보며 IP 칸에 클라우드플레어 대역이 아닌 다양한 주소가 찍히는지 봅니다.
  3. fail2ban 을 쓴다면 "실패" 숫자가 0에서 움직이는지 봅니다. 0에 멈춰 있으면 감옥이 로그를 못 읽는 것입니다.
nginx -t && systemctl reload nginx
tail -n 20 /var/log/nginx/example.com.access.log
fail2ban-client status wordpress-login | grep -E 'Total failed|Journal'

접속 기록을 셀 때 내 확인용 요청이 섞여 결론이 뒤집히는 일도 있었습니다. 로그로 상태를 판정할 때의 실수는 로그로 상태를 판정하다 틀리는 경우에 따로 적었습니다.

확인한 환경

항목 내용
웹서버 nginx, PHP-FPM
프로그램 워드프레스, 아스트라 테마, 로그인 차단 플러그인, fail2ban
앞단 클라우드플레어 프록시
확인한 날짜 2026년 9월 23일(조사), 9월 26일(페이지 캐시 추가), 10월 5일(fail2ban 재확인)

캐시를 건 뒤 화면이 안 바뀌어 헤맨 이야기는 워드프레스를 고쳤는데 화면에 안 바뀔 때에 있습니다.

이 글의 내용은 2026년 10월 5일에 마지막으로 확인했습니다. 틀린 곳을 발견하시면 연락처로 알려 주세요.