새소식

PHP/PHP

PHP - Apache server status 설정 + 프록시 IP 처리

  • -

개요

Grafana로 PHP 서버 상태를 모니터링하려고 하다가 Apache 환경에서의 설정과 보안 문제를 겪게 되어, 그 과정을 정리해보았다.
특히 server-status를 외부에서 접근 가능하게 열었을 때 생기는 보안 이슈와, Nginx 프록시 환경에서의 IP 처리 문제를 어떻게 해결했는지를 중심으로 다룬다.


Apache server-status 활성화

Apache에서는 mod_status 모듈을 활성화한 뒤 /server-status 엔드포인트를 열어 서버 상태 정보를 확인할 수 있다.
Grafana로 apache 서버를 모니터링 할때는 해당 데이터를 수집하기 위해 보통 Prometheus용 Apache Exporter와 연결하여 시각화한다.

처음에는 다음과 같은 설정으로 server-status를 활성화했었다.

<Location /server-status>
    SetHandler server-status
    Require all granted
</Location>

그런데 이렇게 설정하면 누구나 해당 경로에 접근 가능하게 되어버려 보안상 매우 위험하다.


IP 제한 설정

Apache Exporter는 내부망에서 동작하도록 구성되어 있었기 때문에, 내부망에서만 접근 가능하도록 IP 제한을 걸어주었다.
여기서 말하는 내부망은 172.16.0.0/12 대역이다.

참고: 172.16.0.0/12는 사설 IP 주소 대역 중 하나로,
172.16.0.0부터 172.31.255.255까지 포함하는 범위이다.
보통 기업 내부망이나 VPC 등에서 많이 사용된다.

그래서 아래처럼 IP 제한을 설정했다.

<Location /server-status>
    SetHandler server-status
    Require ip 172.16.0.0/12
</Location>

Nginx Proxy 환경에서의 문제

문제는 여기서 발생했다.
서버는 Nginx Reverse Proxy 뒤에 있었고, Apache는 클라이언트의 IP가 아닌 프록시 서버의 IP를 클라이언트 IP로 인식하고 있었다.
즉, 실제 클라이언트가 내부망에 있지 않더라도, Apache 입장에서는 Nginx의 IP만 보이는 상황이었다.

Client → Nginx Proxy (내부망) → Apache (내부망)

Apache는 클라이언트가 아닌 프록시 IP가 172.16.0.0/12에 포함되는지만 보고 판단했는데, 프록시 서버도 내부망에 있었기 때문에 사실상 모든 요청이 이미 IP 조건을 통과하고 있었던 상황이었고 결과적으로 Require ip 조건이 실제로는 제대로 작동하지 않고 있었다.


클라이언트의 실제 IP를 인식하도록 설정

이 문제를 제대로 해결하기 위해 Apache의 mod_remoteip 모듈을 사용해서 프록시가 전달하는 X-Forwarded-For 헤더를 신뢰하도록 설정했다:

<IfModule remoteip_module>
    RemoteIPHeader X-Forwarded-For
    RemoteIPInternalProxy 172.16.0.0/12
</IfModule>
  • RemoteIPHeader: 프록시 서버가 전달하는 헤더 이름 (X-Forwarded-For)
  • RemoteIPInternalProxy: 이 IP 대역에서 오는 요청만 프록시로 신뢰함

이렇게 하면 Apache는 172.16.0.0/12에 해당하는 프록시 서버에서 온 요청에 대해서만 X-Forwarded-For 헤더를 신뢰하고 클라이언트 IP로 간주한다.


X-Forwarded-For 보안 이슈?

하지만 위처럼 설정을 하고 나니 누군가가 직접 X-Forwarded-For 헤더를 조작하면 IP 제한을 우회할 수 있지 않을까 걱정되었다.
하지만 테스트를 해보니 그렇지 않았다. 그것은 apache서버의 요청 처리 방식 때문이었다.

  • Apache는 먼저 요청의 Remote IP가 RemoteIPInternalProxy 조건을 만족하는지 검사한다.
  • 조건을 통과한 경우에만 X-Forwarded-For 값을 IP로 반영한다.
  • 즉, 프록시 서버로부터 온 요청이 아니면 헤더를 무시하고, IP 제한은 정상 동작한다.

만약 사용자가 Remote IP 자체를 172.16.x.x로 조작하면?

그래도 Remote IP를 내부망처럼 보이게 해서 우회하는 것이 가능하지 않을까 생각할 수 있다.
예를 들어

  • 출발지 IP를 172.16.0.10으로 스푸핑하고
  • X-Forwarded-For도 같이 조작해서 내부망 요청처럼 보이게 한다면?

라는 생각이 들 수 있는데, 실제로는 거의 불가능에 가까운 시나리오다.

Apache가 인식하는 Remote IP는 단순히 HTTP 헤더로 전달된 게 아니라,
커널 네트워크 계층에서 수신된 TCP 연결의 출발지 IP 주소다.

즉, RemoteAddr은 서버가 수신한 진짜 패킷의 IP이고, 이건 다음 조건을 모두 만족해야 한다:

  1. TCP 3-way handshake가 완성되어야 함
  2. 서버가 응답을 보낸 IP와 다시 통신이 가능해야 함

그런데 출발지 IP를 위조하는 순간, 응답은 진짜 공격자에게 가지 않고 조작된 IP(예: 172.16.0.10)로 가게 된다.
그러면 TCP 연결이 성립되지 않고, Apache는 요청 자체를 수락하지 않는다.

결론

외부에서 단순히 curl이나 Postman 같은 도구로는 절대 Require ipRemoteIPInternalProxy 조건을 우회할 수 없다.
출발지 IP 자체를 속이는 건 커널 수준에서 막히며, 정말로 이걸 뚫으려면 네트워크 경로를 통제하거나 내부망 장비를 뚫어야 한다.

즉, 일반적인 환경에서는 이 설정으로 충분히 안전하다는 결론이다.


최종 코드

# 외부 프록시 헤더 사용
<IfModule remoteip_module>
    RemoteIPHeader X-Forwarded-For
    RemoteIPInternalProxy 172.16.0.0/12
</IfModule>

# /server-status 설정
<Location /server-status>
    SetHandler server-status
    Require ip 172.16.0.0/12
</Location>

후기

처음엔 단순히 Grafana 연동만 잘하면 되겠지 싶었는데, 프록시 구조나 보안 설정까지 챙기게 될 줄은 몰랐다.
특히 X-Forwarded-For 같은 헤더는 자칫 잘못 쓰면 보안 구멍이 될 수 있어서 신중하게 접근해야 했는데 다행히 Apache가 이런 시나리오를 잘 고려해놔서 설정만 잘 하면 안전하게 운영할 수 있다는 걸 확인했다.
앞으로도 이런 설정은 주기적으로 점검하면서 사용하는 게 좋겠다.

반응형
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.