---
title: "[Lap 04] Cloudflare WAF와 컨테이너 PHP 5중 보안 설계"
canonical_url: "https://wealth-podium.com/2026/10/23/track-01-lap-4-cloudflare-waf-security/"
published_at: "2026-10-23T09:00:00+09:00"
modified_at: "2026-11-07T15:44:35+09:00"
author: "Dellwon (Software Developer & System Architect)"
summary: "리버스 프록시 환경에서 발생한 :8080 내부 포트 노출과 무한 리다이렉트를 해결하고, 악성 스캔 봇을 앞단에서 차단하는 Cloudflare WAF 규칙과 컨테이너 내부 PHP 5중 보안 설정을 적용한 과정을 다룹니다."
---
# [Lap 04] Cloudflare WAF와 컨테이너 PHP 5중 보안 설계

> **"외부 인터넷에 서버를 공개하는 순간, 보안은 선택이 아니라 아키텍처의 생존 조건이 됩니다."**  
> [Lap 03](/2026/10/22/track-01-lap-3-keyless-adc-debugging/)에서 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이 이상하게 바뀌어 있었습니다.

```text
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` 포트가 따라붙었을까?

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

```text
[방문자 브라우저] 
       │ (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
   HTTP/1.1 301 Moved Permanently
   Location: https://wealth-podium.com:8080/wp-admin/
   ```
3. 외부 브라우저는 Cloud Run 내부 포트인 8080에 직접 접근할 수 없으니 접속 실패가 발생합니다.

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

---

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

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

```apache
# [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 감지 코드를 주입했습니다.

```bash
# [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`
- **조건 표현식**:
  ```text
  (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단계 잠금 설정을 추가했습니다.

```text
        [외부 자동화 공격 봇]
               │
     ┌─────────▼─────────┐
     │  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`) 막았습니다.

```php
// [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`에서 시스템 명령 실행 함수들을 모두 비활성화했습니다.

```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`에서 차단했습니다.

```apache
# [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나 워드프레스 버전을 파악해 타깃 공격을 하지 못하도록 버전 노출 헤더와 기본 안내 파일 접근을 모두 막았습니다.

```apache
# [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` 차단 | 버전 정보 미노출 확인 |
