Cloudflare Full Strict SSL + Nginx 리버스 프록시 삽질 총정리
Cloudflare Origin Certificate 발급부터 Nginx SSL 종단, PHP-FPM HTTPS 인식 문제까지. 멀티사이트 SSL 구성에서 만난 실전 트러블슈팅 기록. 실전 경험을 바탕으로 정리했어요.
📚 1인 인프라 구축기 시리즈 (2편)
Cloudflare Full Strict SSL + Nginx 리버스 프록시 삽질 총정리
블로그 4개를 Oracle ARM 서버에서 운영하고 있다. Cloudflare를 앞에 두고 Nginx로 리버스 프록시를 하는 구조인데, SSL 설정에서 생각보다 많은 함정을 만났다.
“Full (Strict)” 하나 켜면 끝이겠지 했는데, 전혀 아니었다 💀
이 글은 Cloudflare Origin Certificate 발급부터 Nginx SSL 종단 설정, 그리고 WordPress가 HTTPS를 인식하지 못해 REST API가 죽어버린 사고까지 실제로 겪은 트러블슈팅을 정리한 기록이다.
🔍 증상: SSL 에러의 세 가지 얼굴

⚠️ 증상 1 — 브라우저에서 526 에러
Cloudflare SSL 모드를 “Full (Strict)“로 바꾼 직후, 사이트에 접속하면 Cloudflare 526 에러가 떴다.
Error 526: Invalid SSL Certificate
Cloudflare가 Origin 서버(내 Nginx)의 인증서를 검증하는 단계에서, 자체 서명 인증서(self-signed)는 “Strict” 모드에서 거부된다. “Full”까지는 통과되지만,
“Full (Strict)“에서는 Cloudflare가 발급한 Origin Certificate나 공인 인증서가 필요하다.
⚠️ 증상 2 — 멀티사이트에서 일부만 526
첫 번째 사이트는 SSL이 잘 되는데, 두 번째 사이트만 526 에러가 났다. Nginx에 server 블록을 추가하면서 인증서 경로를 잘못 지정한 것이 원인이었다.
핵심: Origin Certificate는 도메인 단위로 발급된다.
*.jongmolee.com와일드카드 인증서가 있어도jongmolife.com은 커버되지 않는다. 도메인 수만큼 인증서가 필요하다.
⚠️ 증상 3 — WordPress REST API 401 Unauthorized
SSL은 다 해결했는데,
이번엔 WordPress REST API에서 Application Password 인증이 안 됐다.
wp-json/wp/v2/posts에 요청을 보내면 401이 돌아온다.
curl -u "user:xxxx xxxx xxxx xxxx" \
https://jongmolife.com/wp-json/wp/v2/posts
# → 401 Unauthorized
브라우저에서 관리자 로그인은 되는데 API만 안 되는 기묘한 상황이었다. 세 증상이 각각 독립된 문제처럼 보이지만, 사실 모두 SSL 체인의 각 레이어에서 비롯된 거였다.
🧠 원인: SSL 체인의 세 가지 레이어

레이어 1 — Cloudflare ↔ Origin 서버 인증
사용자 브라우저
↓ (HTTPS — Cloudflare 엣지 인증서)
Cloudflare CDN
---
↓ (HTTPS — Origin Certificate 검증)
Nginx (Origin 서버)
↓ (FastCGI)
---
PHP-FPM (WordPress)
SSL 모드별 차이를 정리하면 이렇다.
| SSL 모드 | Cloudflare→Origin | Origin 인증서 요구 |
|---|---|---|
| Off | HTTP | 불필요 |
| Flexible | HTTP | 불필요 (위험!) |
| Full | HTTPS | 자체서명 OK |
| Full (Strict) | HTTPS | Origin Cert 또는 공인 필수 |
“Flexible”은 Cloudflare→Origin 구간이 HTTP라서, 사용자 눈에는 HTTPS 자물쇠가 보이지만 실제로는 평문 통신이다. 보안상 절대 쓰면 안 된다.
주의: Flexible 모드는 브라우저에서 자물쇠 아이콘이 보여서 안전해 보인다. 하지만 Cloudflare와 내 서버 사이는 HTTP로 평문 통신 중이다. 비밀번호, 세션 토큰이 그대로 노출될 수 있다.
레이어 2 — 도메인별 Origin Certificate 분리
Cloudflare Origin Certificate는 도메인 단위로 발급된다.
*.jongmolee.com 와일드카드를 발급하면 해당 도메인과 서브도메인만 커버된다.
jongmolife.com은 별도 발급이 필수다.
certs/
├── origin.pem # *.jongmolee.com (15년)
├── origin-key.pem
---
├── life-origin.pem # *.jongmolife.com (15년)
├── life-origin-key.pem
├── it-origin.pem # *.jongmoit.com (15년)
---
└── it-origin-key.pem
하나라도 빠지면 해당 도메인만 526 에러가 난다. 멀티사이트를 구성할 때 도메인 추가할 때마다 이 파일을 쌍으로 챙겨야 한다.
레이어 3 — PHP-FPM의 HTTPS 인식 실패
이게 가장 찾기 어려운 원인이었다.
Nginx가 SSL 종단을 처리하므로, PHP-FPM 입장에서는 자기에게 들어오는 요청이 HTTP다.
WordPress는 $_SERVER['HTTPS'] 값을 보고 HTTPS 여부를 판단하는데,
이 값이 없으면 HTTP로 간주한다.
Cloudflare (HTTPS) → Nginx (HTTPS → FastCGI) → PHP-FPM (HTTPS 값 없음 = HTTP 인식)
WordPress가 HTTP로 인식하면 Application Passwords 기능이 자동으로 비활성화된다. 보안상 HTTPS에서만 동작하도록 설계되어 있기 때문이다. 결과적으로 REST API 인증이 통째로 죽는다.
핵심: 이 문제의 근본 원인은
fastcgi_param HTTPS on;이wp-login.php전용 location 블록에만 들어 있고, 일반 PHP 처리 블록에는 빠져있었던 것이다. 딱 한 줄 차이인데, 증상은 전혀 달라 보인다.
✅ 해결: 단계별 SSL 구성

Step 1 — Cloudflare Origin Certificate 발급
Cloudflare 대시보드에서 클릭으로도 발급할 수 있지만, API로 자동화하는 게 낫다. CSR(Certificate Signing Request)을 생성하고 Cloudflare API로 서명을 요청한다.
# 1. CSR 생성
openssl req -new -newkey rsa:2048 -nodes \
-keyout /tmp/cf-origin-key.pem -out /tmp/cf-origin.csr \
-subj "/CN=jongmolife.com/O=Personal/C=KR"
# 2. Cloudflare API로 Origin Certificate 발급
CF_TOKEN=$(security find-generic-password \
-s "cloudflare-api" -a "jongmolee.com" -w)
curl -s -X POST "https://api.cloudflare.com/client/v4/certificates" \
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
---
-d '{
"hostnames": ["jongmolife.com", "*.jongmolife.com"],
"requested_validity": 5475,
---
"request_type": "origin-rsa",
"csr": "'"$(cat /tmp/cf-origin.csr)"'"
}'
requested_validity: 5475는 15년이다.
Origin Certificate의 최대 유효기간으로, 한 번 발급하면 2041년까지 쓸 수 있다 🚀
팁: Origin Certificate는 Cloudflare 프록시를 통해서만 유효하다. 브라우저가 직접 접속하면 “신뢰할 수 없는 인증서” 경고가 뜬다. 반드시 Cloudflare DNS에서 프록시(주황색 구름 아이콘)를 켜야 한다.
Step 2 — Nginx SSL 서버 블록 설정
각 도메인별로 별도의 server 블록을 만들고, 해당 도메인의 Origin Certificate를 연결한다.
# ── HTTP → HTTPS 리다이렉트 ──
server {
listen 80;
server_name jongmolife.com www.jongmolife.com;
---
return 301 https://$host$request_uri;
}
# ── HTTPS ──
server {
listen 443 ssl;
server_name jongmolife.com www.jongmolife.com;
ssl_certificate /etc/nginx/certs/life-origin.pem;
ssl_certificate_key /etc/nginx/certs/life-origin-key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
root /var/www/html-life;
index index.php;
# FPM 컨테이너 내부 경로 주의!
set $fpm_root /var/www/html;
location ~ \.php$ {
fastcgi_pass wordpress-life:9000;
fastcgi_param SCRIPT_FILENAME $fpm_root$fastcgi_script_name;
---
include fastcgi_params;
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
fastcgi_param HTTPS on; # ← 이게 핵심!
---
}
}
주의: 멀티사이트 Docker 구성에서 Nginx의
root와 FPM 내부 경로가 다르다. Nginx에서는/var/www/html-life로 마운트하지만, FPM 컨테이너 안에서는/var/www/html이 고정이다.$fpm_root변수로 분리하지 않으면 “File not found” 에러가 끊임없이 난다.
Step 3 — 모든 PHP 블록에 HTTPS 파라미터 추가
원래 fastcgi_param HTTPS on;이 wp-login.php 전용 블록에만 있었다.
이를 모든 .php 처리 블록에 추가해야 한다.
❌ Before — HTTPS 파라미터가 login 블록에만 있는 경우
location = /wp-login.php {
fastcgi_param HTTPS on; # 여기만 있었음
# ...
---
}
location ~ \.php$ {
# HTTPS 파라미터 없음 → WordPress가 HTTP로 인식
# Application Passwords 기능 자동 비활성화 → REST API 401
---
# ...
}
wp-login.php 외 모든 PHP 요청(REST API 포함)에서 HTTPS 환경 변수가 없으니,
WordPress는 HTTP로 간주한다.
✅ After — 모든 PHP 블록에 추가
location = /wp-login.php {
fastcgi_param HTTPS on;
# ...
---
}
location ~ \.php$ {
fastcgi_param HTTPS on; # 모든 PHP 요청에 적용 ← 이 줄이 핵심
# ...
---
}
딱 한 줄 추가인데 REST API 401이 바로 해소됐다.
Step 4 — mu-plugin으로 Application Passwords 강제 활성화
HTTPS 파라미터를 추가해도, 캐시나 세션 문제로 즉시 반영되지 않을 수 있다. mu-plugin(Must-Use Plugin)으로 Application Passwords를 강제 활성화하면 안전하다.
<?php
// wp-content/mu-plugins/force-app-passwords.php
add_filter('wp_is_application_passwords_available',
---
'__return_true');
mu-plugin은 관리자 화면에서 비활성화할 수 없다.
wp-content/mu-plugins/ 디렉토리에 파일만 넣으면 자동 로드된다.
보안 관련 필수 설정에 적합한 방식이다.
팁:
fastcgi_param HTTPS on;추가가 근본 해결책이고, mu-plugin은 이중 안전망이다. 둘 다 적용해두면 환경이 바뀌어도 Application Passwords가 예상치 못하게 꺼지는 일이 없다.
Step 5 — Cloudflare SSL 모드를 Full (Strict)로 변경
모든 인증서와 Nginx 설정이 완료된 후에 SSL 모드를 변경한다. 순서가 중요하다. 인증서 없이 Strict를 먼저 켜면 사이트가 즉시 죽는다.
# Zone별로 SSL 모드 설정
for ZONE_ID in "2967d6b..." "f0092c3..." "5d70da0..."; do
curl -s -X PATCH \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/settings/ssl" \
---
-H "Authorization: Bearer $CF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"value":"strict"}'
---
done
Zone ID는 Cloudflare 대시보드 각 도메인의 Overview 탭 우측 하단에서 확인할 수 있다.
🛡️ 예방: SSL 체크리스트

새 도메인 추가 시 체크리스트
새 도메인을 멀티사이트에 추가할 때마다 이 순서를 반드시 따른다. 순서를 지키지 않으면 중간에 사이트가 죽는 구간이 생긴다.
| # | 항목 | 확인 |
|---|---|---|
| 1 | Cloudflare Origin Certificate 발급 (와일드카드) | □ |
| 2 | 서버에 cert + key 파일 배포 | □ |
| 3 | Nginx server 블록 추가 (ssl_certificate 경로 확인) | □ |
| 4 | fastcgi_param HTTPS on; 모든 PHP 블록에 포함 | □ |
| 5 | $fpm_root 변수 설정 (멀티사이트) | □ |
| 6 | nginx -t 문법 검사 통과 | □ |
| 7 | Cloudflare DNS A레코드 (프록시 활성화) | □ |
| 8 | Cloudflare SSL 모드: Full (Strict) 확인 | □ |
| 9 | REST API 인증 테스트 (curl -u) | □ |
인증서 만료 관리
Cloudflare Origin Certificate는 최대 15년이라 실질적으로 만료 걱정이 없다. 그래도 관리 차원에서 인증서 목록을 문서화해두는 게 좋다. Let’s Encrypt의 90일 갱신 사이클에서 해방되는 것만으로도 충분히 가치있다.
| 인증서 | 도메인 | 만료 |
|---|---|---|
| origin.pem | *.jongmolee.com | 2041-02-23 |
| life-origin.pem | *.jongmolife.com | 2041-03-01 |
| it-origin.pem | *.jongmoit.com | 2041-03-05 |
자동 감지 스크립트
Nginx 설정 변경 후 배포 전에 SSL 검증을 자동으로 돌리는 스크립트를 CI에 넣어두면 사고를 예방할 수 있다.
#!/bin/bash
# ssl-check.sh — 배포 전 SSL 검증
DOMAINS=("jongmolee.com" "jongmolife.com" "jongmoit.com")
for domain in "${DOMAINS[@]}"; do
status=$(curl -s -o /dev/null -w "%{http_code}" "https://$domain")
if [ "$status" != "200" ]; then
---
echo "❌ $domain: HTTP $status"
exit 1
fi
---
echo "✅ $domain: OK"
done
📋 정리

| 상황 | 안티패턴 | 권장 패턴 |
|---|---|---|
| SSL 모드 선택 | Flexible (평문 통신 위장) | Full (Strict) + Origin Cert |
| Origin 인증서 | Let’s Encrypt (90일 갱신) | Cloudflare Origin Cert (15년) |
| 멀티도메인 인증서 | 하나로 모든 도메인 커버 시도 | 도메인별 와일드카드 개별 발급 |
| FPM HTTPS 인식 | login 블록에만 HTTPS 파라미터 | 모든 PHP 블록에 fastcgi_param HTTPS on; |
| FPM 경로 | $document_root 공유 | $fpm_root 변수로 컨테이너별 분리 |
| 보안 강제 설정 | functions.php 수정 | mu-plugin (비활성화 불가) |
| 배포 순서 | SSL Strict 먼저 → 인증서 나중 | 인증서 먼저 → SSL Strict 나중 |
Cloudflare + Nginx + Docker 멀티사이트 SSL은 레이어가 세 개(Cloudflare ↔ Nginx ↔ PHP-FPM)라서 어디서 문제가 생겼는지 찾기 어렵다. 각 레이어를 독립적으로 검증하는 습관이 중요하다.
526이면 Cloudflare↔Nginx 구간, 401이면 Nginx↔FPM 구간을 의심하면 된다 ✨
같은 인프라에서 서버를 처음 세팅한 과정이 궁금하다면 Oracle ARM + Docker로 WordPress 4사이트 운영하기도 참고해보자.
참고 자료
📚 1인 인프라 구축기 시리즈 (2편)
- 1. Oracle ARM + Docker로 WordPress 4사이트 운영하기
- 2. Cloudflare Full Strict SSL + Nginx 리버스 프록시 삽질 총정리