[Lap 04] Cloudflare WAF와 컨테이너 PHP 5중 보안 설계

🏁 LAP 04 / 07 · ⏱️ 10 min read

“외부 인터넷에 서버를 공개하는 순간, 보안은 선택이 아니라 아키텍처의 생존 조건이 됩니다.”
Lap 03에서 Keyless ADC 미디어 파이프라인을 정복한 뒤, 드디어 도메인을 연결하고 사이트를 오픈한 지 채 10분도 지나지 않았을 때였습니다. 전 세계 IP 대역에서 관리 페이지와 설정 파일을 찔러보는 자동화 스캔 요청이 로그에 찍히기 시작했습니다. 특히 서버리스 환경에서는 악성 봇 트래픽을 앞단에서 차단하지 못하면, 유휴 상태의 컨테이너가 계속 깨어나며 무료 할당량까지 소모하게 됩니다.
이번 글에서는 제가 도메인 연결 직후 겪었던 :8080 내부 포트 노출 및 무한 리다이렉트 문제(ADR-004)를 해결한 과정과, Cloudflare WAF 엣지 방화벽 및 컨테이너 내부 PHP 5중 보안 설정(ADR-006)을 정리해 보겠습니다.


1. 첫 배포 직후 마주친 문제: :8080 포트 노출과 자동화 공격 봇

Cloud Run 컨테이너와 오리건(us-west1)의 MariaDB VM 연결을 마치고, 브라우저 주소창에 도메인(https://wealth-podium.com/wp-admin)을 입력했을 때였습니다. 관리자 로그인 창이 뜨는 대신 연결 타임아웃 에러가 발생했습니다.

주소창을 다시 보니 URL이 이상하게 바뀌어 있었습니다.

https://wealth-podium.com:8080/wp-admin/

컨테이너 내부에서만 쓰여야 할 8080 포트가 도메인 뒤에 그대로 노출되면서, 브라우저가 열리지도 않은 8080 포트로 접속을 시도하다가 멈춰버린 것입니다.

게다가 Cloud Run 로그를 열어보니 또 다른 문제가 보였습니다. 사이트를 막 띄웠는데도 해외 IP 대역에서 /xmlrpc.php, /.env, /wp-json/wp/v2/users, /wp-login.php를 향해 초당 수십 건의 취약점 스캔 요청을 보내고 있었습니다. 요즘의 자동화 공격 봇들은 취약한 워드프레스 플러그인이나 열려 있는 PHP 함수를 찾아내어 순식간에 웹쉘을 업로드합니다.

이 두 가지 문제를 해결하기 위해 먼저 리버스 프록시 헤더 구성을 바로잡고, 이어서 엣지와 컨테이너 양쪽에 다층 방어벽을 세웠습니다.


2. 리버스 프록시 트러블슈팅: :8080 포트 유출과 301 리다이렉트 루프 (ADR-004)

💥 왜 /wp-admin 접속 시 :8080 포트가 따라붙었을까?

방문자의 요청이 컨테이너에 도달하기까지의 네트워크 경로를 따라가 보면 원인을 쉽게 이해할 수 있습니다.

[방문자 브라우저] 
       │ (HTTPS 443)
       ▼
[Cloudflare Edge CDN / WAF]
       │ (HTTPS 443)
       ▼
[Google Front End (GFE) Load Balancer]
       │ (HTTP 컨테이너 내부 포트: 8080 전달)
       ▼
[Cloud Run Apache 웹서버 (Listen 8080)]

Cloud Run 컨테이너는 기본적으로 내부 8080 포트로 요청을 받습니다.

문제는 사용자가 주소 끝에 슬래시(/)를 빼고 https://wealth-podium.com/wp-admin으로 접속했을 때 일어납니다.

  1. Apache의 디렉터리 모듈(mod_dir)은 wp-admin이 디렉터리임을 확인하고, 표준에 따라 끝에 슬래시를 붙인 URL로 301 Moved Permanently 리다이렉트를 보냅니다.
  2. 이때 Apache의 기본 설정(UseCanonicalName Off) 상태에서는 자기 자신이 현재 리스닝하고 있는 물리 포트(8080)를 그대로 Location 헤더에 붙여버립니다.
   HTTP/1.1 301 Moved Permanently
   Location: https://wealth-podium.com:8080/wp-admin/
  1. 외부 브라우저는 Cloud Run 내부 포트인 8080에 직접 접근할 수 없으니 접속 실패가 발생합니다.

또한 Cloudflare의 SSL 모드가 Flexible로 되어 있으면, Cloudflare는 오리진에 HTTP(80)로 요청을 보내고 워드프레스는 다시 HTTPS로 리다이렉트시키면서 301 무한 리다이렉트 루프(ERR_TOO_MANY_REDIRECTS)에 빠지게 됩니다. 혹시 Cloudflare와 클라우드를 연동할 때 이 무한 루프 에러를 겪어보신 분이 꽤 많으실 겁니다.


🔧 해결 방법: Apache 정준 설정(remoteip.conf)과 초기 HTTPS 헤더 처리

저는 컨테이너 안의 Apache 설정 파일(config/remoteip.conf)에 아래 세 줄의 핵심 지시어를 추가해 포트 노출 문제를 해결했습니다.

# [config/remoteip.conf]
# 1. 리버스 프록시 뒤에서 실제 방문자 IP 복원
<IfModule remoteip_module>
    RemoteIPHeader X-Forwarded-For
    RemoteIPInternalProxy 10.0.0.0/8
    RemoteIPInternalProxy 172.16.0.0/12
    RemoteIPInternalProxy 192.168.0.0/16
    RemoteIPInternalProxy 169.254.0.0/16
</IfModule>

# 2. Apache가 리다이렉트 URL을 만들 때 내부 포트(:8080) 대신 공인 도메인과 443 포트를 쓰도록 고정
ServerName https://wealth-podium.com:443
UseCanonicalName On
UseCanonicalPhysicalPort Off
  • ServerName https://wealth-podium.com:443: Apache에게 외부에서 접근하는 정식 호스트명과 포트(443)를 알려줍니다.
  • UseCanonicalName On: 리다이렉트 URL을 조합할 때 내부 소켓 정보 대신 위에서 지정한 ServerName을 사용하도록 강제합니다.
  • UseCanonicalPhysicalPort Off: 물리적인 바인딩 포트(8080)가 외부 응답 헤더에 노출되지 않도록 차단합니다.

여기에 더해, 워드프레스가 초기화되기 전 실행되는 엔트리포인트 스크립트(docker/docker-entrypoint-cloudrun.sh)에서 wp-config.php에 Cloudflare HTTPS 감지 코드를 주입했습니다.

# [docker/docker-entrypoint-cloudrun.sh]
export WORDPRESS_CONFIG_EXTRA="
/* Cloudflare & Cloud Run Early Proxy Handling */
if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
    $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_CF_CONNECTING_IP'];
}
if (isset($_SERVER['HTTP_CF_VISITOR']) && strpos($_SERVER['HTTP_CF_VISITOR'], '"scheme":"https"') !== false) {
    $_SERVER['HTTPS'] = 'on';
}
if (!defined('AUTOMATIC_UPDATER_DISABLED')) {
    define('AUTOMATIC_UPDATER_DISABLED', true);
}
if (!defined('DISALLOW_FILE_EDIT')) {
    define('DISALLOW_FILE_EDIT', true);
}
"

Cloudflare가 전달하는 HTTP_CF_VISITOR 헤더에 "scheme":"https"가 포함되어 있으면 즉시 $_SERVER['HTTPS'] = 'on'으로 설정해 주어, 리다이렉트 루프가 발생할 여지를 원천적으로 없앴습니다.


3. 엣지 보안 설계: Cloudflare Full (Strict) SSL과 WAF 방화벽

악성 봇 요청이 Cloud Run 컨테이너까지 들어오게 두면 컨테이너가 불필요하게 기동되어 무료 리소스를 쓰게 됩니다. 따라서 공격 트래픽은 오리진 서버에 닿기 전 Cloudflare 엣지 단계에서 바로 차단하도록 구성했습니다.

① SSL/TLS 모드: Full (Strict) 적용

  • 설정 위치: Cloudflare 대시보드 > SSL/TLS > Overview
  • 선택한 모드: Full (Strict)
  • 설계 근거: us-west1 리전의 Cloud Run 도메인 매핑은 Google이 발급한 정식 SSL 인증서를 제공합니다. 따라서 방문자 ↔ Cloudflare 구간뿐만 아니라 Cloudflare ↔ Cloud Run 구간까지 엄격하게 암호화 검증을 수행하는 Full (Strict) 모드를 적용했습니다.

② Edge Cache 규칙: 정적 파일 캐싱 (cf-cache-status: HIT)

  • 설정 위치: Cloudflare 대시보드 > Caching > Cache Rules
  • 조건 표현식:
  (http.request.uri.path contains "/wp-content/uploads/") or 
  (http.request.uri.path contains "/wp-content/themes/") or 
  (http.request.uri.path contains "/wp-includes/")
  • Edge Cache TTL: Override origin ➔ 1 month
  • 효과: 이미지, 폰트, CSS, JS 등 정적 파일 요청의 95% 이상을 Cloudflare 서울 엣지(ICN)가 직접 응답합니다. Cloud Run 컨테이너는 동적 PHP 페이지 요청만 처리하므로 서버 부하와 비용을 크게 아낄 수 있습니다.

③ 맞춤형 WAF 방화벽 규칙 2종 (Custom WAF Rules)

규칙 이름 차단 조건 (Expression) 처리 동작 적용 목적
Block Exploit Probes (http.request.uri.path eq "/xmlrpc.php") or (http.request.uri.path contains "/.env") or (http.request.uri.path contains "wp-config.php") Block (403) 취약점 공격 표적이 되는 xmlrpc.php 및 설정 파일 탐색 요청을 엣지에서 즉시 차단
Protect WP Login (http.request.uri.path contains "/wp-login.php") Managed Challenge 관리자 로그인 페이지 접근 시 자동화 봇 여부를 먼저 검증해 무차별 대입 공격 방어

4. 컨테이너 내부 5중 심층 방어 (Defense in Depth, ADR-006)

앞단에 WAF를 두었더라도 아키텍처 보안은 항상 심층 방어(Defense in Depth)를 전제로 해야 합니다. 만약 우회 공격이 들어오더라도 컨테이너 내부에서 작동하지 못하도록 PHP와 Apache, 워드프레스 코어에 5단계 잠금 설정을 추가했습니다.

        [외부 자동화 공격 봇]
               │
     ┌─────────▼─────────┐
     │  Cloudflare WAF   │ ➔ 1단계: /xmlrpc.php, /.env 즉시 차단 & 로그인 챌린지
     └─────────┬─────────┘
               │ (통과 시)
     ┌─────────▼─────────┐
     │    Apache 2.4     │ ➔ 2단계: /uploads 내 PHP 실행 차단 & 버전 정보 은폐
     └─────────┬─────────┘
               │
     ┌─────────▼─────────┐
     │   WordPress Core  │ ➔ 3단계: /wp-json 유저 목록 열거 차단 & 파일 편집기 비활성화
     └─────────┬─────────┘
               │
     ┌─────────▼─────────┐
     │    PHP 8.3 Core   │ ➔ 4단계: disable_functions (OS 쉘 명령 실행 함수 비활성화)
     └─────────┬─────────┘
               │
     ┌─────────▼─────────┐
     │  Stateless Container │ ➔ 5단계: 무상태 컨테이너 구조로 악성 파일 영구 잔존 불가
     └───────────────────┘

🛡️ Layer 1: /wp-json/wp/v2/users 계정 열거 차단

공격 봇들이 가장 먼저 찌르는 곳은 워드프레스 기본 REST API인 /wp-json/wp/v2/users입니다. 기본적으로 이 주소에 접속하면 관리자의 로그인 슬러그와 이름이 JSON으로 노출됩니다.

저는 mu-plugins/cloudrun.php에 아래 필터를 넣어 비로그인 상태에서는 사용자 목록 API 자체가 존재하지 않도록(404) 막았습니다.

// [mu-plugins/cloudrun.php]
// 비로그인 요청에 대해 REST API 사용자 조회 엔드포인트 비활성화 (404 반환)
add_filter('rest_endpoints', function ($endpoints) {
    if (!is_user_logged_in()) {
        unset($endpoints['/wp/v2/users']);
        unset($endpoints['/wp/v2/users/(?P<id>[d]+)']);
    }
    return $endpoints;
});

// ?author=1 형태의 작성자 아카이브 스캔 시 메인으로 리다이렉트
add_action('init', function () {
    if (!is_admin() && isset($_REQUEST['author']) && !is_user_logged_in()) {
        wp_safe_redirect(home_url('/'), 301);
        exit;
    }
});

🛡️ Layer 2: PHP 시스템 명령 실행 함수 원천 비활성화

만약 플러그인 취약점으로 인해 악성 PHP 구문이 실행되더라도 운영체제 쉘(Shell)을 제어하지 못하도록, config/custom-php.ini에서 시스템 명령 실행 함수들을 모두 비활성화했습니다.

; [config/custom-php.ini]
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,show_source,pcntl_exec
allow_url_include = Off
display_errors = Off
log_errors = On
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1

🛡️ Layer 3: 업로드 폴더 내 스크립트 실행 차단 & 파일 편집 잠금

이미지로 위장한 웹쉘(.php, .phtml, .phar 등)이 업로드 폴더에 들어가더라도 아파치 웹서버가 이를 실행하지 못하도록 config/remoteip.conf에서 차단했습니다.

# [config/remoteip.conf]
<Directory "/var/www/html/wp-content/uploads">
    <FilesMatch ".(?i:php[0-9]?|phtml|phar|pl|py|cgi|sh)$">
        Require all denied
    </FilesMatch>
</Directory>

또한 관리자 화면의 테마/플러그인 코드 편집 기능을 끄기 위해 define('DISALLOW_FILE_EDIT', true);도 함께 적용했습니다.

🛡️ Layer 4: 서버 및 버전 정보 은폐

공격자가 서버의 정확한 PHP나 워드프레스 버전을 파악해 타깃 공격을 하지 못하도록 버전 노출 헤더와 기본 안내 파일 접근을 모두 막았습니다.

# [config/remoteip.conf]
<FilesMatch "^(xmlrpc.php|wp-config-sample.php|readme.html|license.txt)$">
    Require all denied
</FilesMatch>

Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header unset X-Powered-By
Header unset X-Pingback

5. 보안 점검 요약 및 다음 글(Lap 5) 예고

적용을 마친 뒤 각 공격 경로별로 직접 요청을 보내 검증한 결과입니다.

점검 항목 공격 의도 적용한 방어 설정 검증 결과
:8080 포트 유출 내부 포트 노출 및 접속 오류 Apache UseCanonicalName On + CF-Visitor 공인 도메인(443) 정상 리다이렉트
XML-RPC 악용 공격 DDoS 증폭 및 무차별 대입 Cloudflare WAF + Apache FilesMatch 엣지에서 즉시 403 Forbidden 차단
REST API 계정 열거 관리자 아이디 스캔 mu-plugins/cloudrun.php (rest_endpoints) 404 rest_no_route 반환
OS 원격 명령 실행 (RCE) 리버스 쉘 및 악성 프로세스 실행 PHP disable_functions PHP 엔진 수준에서 함수 호출 거부
업로드 폴더 웹쉘 실행 악성 스크립트 직접 호출 Apache uploads 실행 거부 + 무상태 컨테이너 403 Forbidden 차단
버전 정보 스캔 취약 버전 타깃 정찰 expose_php Off + readme.html 차단 버전 정보 미노출 확인

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *