이 글은 Cloudflare 프록시와 인증서 시리즈의 2번째 편이다.
EP.01에서 다룬 "연결이 두 개로 쪼개진다"는 구조를 바탕으로, 두 번째 연결의 인증서 검증을 다룬다.
들어가며
오리진에 인증서를 설치하지 않았는데 브라우저에는 자물쇠가 떴다. 반대로 인증서를 설치하고 Full (strict)까지 맞췄는데 오리진에 직접 붙으면 경고가 떴다.
두 현상 모두 EP.01의 결론으로 절반은 설명된다. Proxied에서는 연결이 브라우저 ↔ 엣지, 엣지 ↔ 오리진 두 개로 나뉜다. 브라우저의 자물쇠는 첫 번째 연결만 말해준다.
나머지 절반은 두 번째 연결에서 누가, 어떤 기준으로 오리진 인증서를 검증하느냐다. 이 글은 그 기준인 SSL/TLS 모드와 인증서 신뢰 체계를 정리한다. 처음엔 "인증서 종류별로 뭐가 되고 안 되는지"만 표로 만들려 했다. 그런데 정리하다 보니 핵심은 인증서의 "등급"이 아니라 신뢰하는 주체였다.
인증서가 보장하는 것은 암호화가 아니라 신원이다
TLS 인증서는 "이 공개키의 주인이 이 도메인이다"라는 증명서다. 암호화 자체는 키 교환으로 이뤄지고, 인증서는 상대가 진짜인지 확인하는 데 쓰인다.
두 개념을 분리해야 이후 내용이 헷갈리지 않는다.
| 개념 | 하는 일 | 무엇이 결정하나 |
|---|---|---|
| 암호화(Encryption) | 제3자가 내용을 못 읽게 한다 | TLS 버전, 암호 스위트, 키 교환(ECDHE 등) |
| 인증(Authentication) | 연결한 상대가 진짜 그 서버인지 확인한다 | 인증서와 그 발급자의 신뢰 여부 |
자체 서명 인증서로도 TLS 1.3 + AES-256-GCM 연결은 똑같이 맺어진다. 다만 상대가 진짜인지는 증명되지 않는다. 중간에 끼어든 공격자가 자기 자체 서명 인증서를 내밀어도 구별할 수 없다는 뜻이다.
클라이언트가 인증서를 검증할 때 확인하는 항목은 다음과 같다.
- 지금이 유효기간(
notBefore~notAfter) 안인가 - 접속한 호스트명이 인증서의 SAN(Subject Alternative Name)에 있는가
- 서명과 인증서 체인이 올바른가
- 체인을 따라 올라가면 내가 신뢰하는 루트 CA에 닿는가
1~3번은 인증서 자체의 문제다. 4번은 검증하는 쪽이 무엇을 신뢰하느냐에 달렸다. 이 글은 대부분 4번 이야기다.
신뢰는 신뢰 저장소가 정한다
클라이언트는 인증서 체인을 따라 올라가다 자기 신뢰 저장소(trust store)에 있는 루트 CA에 닿으면 인증서를 신뢰한다. 신뢰 저장소가 다르면 같은 인증서도 결과가 달라진다.
flowchart TD
subgraph Browser["브라우저/OS 신뢰 저장소"]
R1["공인 루트 CA<br/>ISRG Root X1, GTS Root R1 등"]
end
subgraph CF["Cloudflare 신뢰 저장소"]
R2["공인 루트 CA (cfssl_trust)"]
R3["Cloudflare Origin CA 루트"]
end
R1 --> I1["중간 CA"] --> L1["Let's Encrypt·유료 공인 인증서"]
R2 --> I1
R3 --> L2["Origin CA 인증서"]
S["자체 서명 인증서<br/>(체인 없음)"]
그림을 보면 세 가지가 보인다.
- 공인 CA 인증서는 양쪽 저장소 모두에 루트가 있다. 브라우저도, Cloudflare도 신뢰한다.
- Origin CA 인증서의 루트는 Cloudflare 저장소에만 있다. Cloudflare는 신뢰하고 브라우저는 신뢰하지 않는다.
- 자체 서명 인증서는 어느 저장소에도 연결되지 않는다. 누구도 기본으로는 신뢰하지 않는다.
그래서 인증서의 신뢰도는 "높다/낮다"가 아니라 "누구에게 유효하냐"로 물어야 한다. Origin CA 인증서는 잘못된 인증서가 아니라, 신뢰하도록 설계된 대상이 Cloudflare뿐인 인증서다.
Cloudflare SSL/TLS 모드 네 가지
SSL/TLS 모드는 엣지 ↔ 오리진 연결의 프로토콜과 검증 수준을 정한다. EP.01에서 봤듯 Proxied 레코드에만 적용된다.
| 모드 | 방문자 ↔ 엣지 | 엣지 ↔ 오리진 | 오리진 인증서 검증 |
|---|---|---|---|
| Off | HTTP (HTTPS 요청을 HTTP로 리디렉션) | HTTP | 없음 |
| Flexible | HTTPS | HTTP | 없음 |
| Full | HTTPS | HTTPS | 전혀 안 함 |
| Full (strict) | HTTPS | HTTPS | 만료·발급자·호스트명 검증 |
모드별로 기억할 점은 다음과 같다.
- Off: 방문자가 HTTPS로 들어와도 HTTP로 돌려보낸다. Cloudflare도 권장하지 않는다.
- Flexible: 오리진에 인증서가 필요 없다. 대신 엣지 ↔ 오리진은 평문이다. 그리고 443 포트에서만 동작한다. 8443 같은 다른 HTTPS 포트는 Full로 처리된다.
- Full: 공식 문서 표현 그대로 인증서를 "어떤 식으로도 검증하지 않는다". 만료, 자체 서명, 호스트명 불일치 모두 통과한다. 암호화는 되지만 인증은 없다.
- Full (strict): 인증서가 만료되지 않았고, 공인 CA 또는 Cloudflare Origin CA가 발급했고, CN/SAN이 호스트명과 일치해야 한다. 안 맞으면 526이다.
이 외에 Enterprise 전용인 Strict (SSL-Only Origin Pull)이 있다. 방문자가 HTTP로 와도 오리진에는 항상 HTTPS로 붙는 모드다.
2026년 현재 새로 추가한 zone은 기본값이 Automatic SSL/TLS다. Cloudflare가 오리진을 스캔해서 지원 가능한 가장 안전한 모드를 고르고, 오리진 인증서가 만료돼도 모드를 낮추지 않는다. 대시보드에서 Custom SSL/TLS로 바꿔야 위 네 모드를 직접 고를 수 있다.
오리진 인증서 네 가지 비교
오리진에 둘 수 있는 선택지는 인증서 없음, 자체 서명, Origin CA, 공인 CA 네 가지다. 각각을 "누가 신뢰하나" 기준으로 비교하면 다음과 같다.
| 오리진 인증서 | 브라우저 기본 신뢰 | Full (strict) 통과 | 비용 | 유효기간 |
|---|---|---|---|---|
| 없음 (HTTP만) | 해당 없음 | 불가 | - | - |
| 자체 서명 | 불신 | 불가 (526) | 무료 | 직접 정함 |
| Cloudflare Origin CA | 불신 | 통과 | 무료 | 최대 15년 |
| Let's Encrypt (무료 공인) | 신뢰 | 통과 | 무료 | 90일 이하 |
| 유료 공인 CA (DV·OV·EV) | 신뢰 | 통과 | 유료 | 최대 200일 |
| 사설 CA 발급 | 루트를 배포한 클라이언트만 | 기본 불가 | 운영 비용 | 직접 정함 |
표만으로는 부족한 부분을 덧붙인다.
- Origin CA는 Cloudflare 대시보드 SSL/TLS → Origin Server에서 바로 발급한다. 15년짜리라 편하지만, Cloudflare가 만료 알림을 보내지 않는다는 제약이 공식 문서에 적혀 있다. 15년 뒤의 나를 위해 만료일을 따로 기록해 둔다.
- 공인 인증서 유효기간은 CA/Browser Forum 규정(SC-081)에 따라 2026년 3월 15일부터 최대 200일로 줄었다. 2027년 3월 15일에는 100일, 2029년 3월 15일에는 47일로 더 줄어든다. 유료로 사도 1년짜리는 더 이상 없다. 무료든 유료든 자동 갱신을 걸어둬야 한다.
- 자체 서명과 사설 CA는 다르다. 자체 서명은 인증서가 자기 자신을 서명한 것이고, 사설 CA는 내가 운영하는 루트가 서버 인증서를 서명한 것이다. 사설 CA는 루트 하나만 클라이언트에 심으면 그 CA가 발급한 모든 인증서를 신뢰시킬 수 있다.
프록시 × 모드 × 인증서 조합표
앞의 내용을 조합하면 결과를 예측할 수 있다. 아래 표는 Cloudflare 공식 문서의 모드별 조건을 기준으로 정리한 예상 동작이다. 오리진 인증서의 호스트명과 유효기간은 정상이라고 가정했다.
| 오리진 상태 | DNS Only | Proxied + Flexible | Proxied + Full | Proxied + Full (strict) |
|---|---|---|---|---|
| HTTP만 제공 | HTTPS 불가 | 정상* | 525 또는 521 | 525 또는 521 |
| 자체 서명 | 브라우저 경고 | 정상* | 정상 (검증 없음) | 526 |
| Origin CA | 브라우저 경고 | 정상* | 정상 (검증 없음) | 정상 |
| 공인 CA | 정상 | 정상* | 정상 (검증 없음) | 정상 |
* Flexible은 오리진 인증서를 아예 쓰지 않는다. 오리진이 80 포트에서 HTTP를 정상 응답해야 하고, 오리진이 HTTP → HTTPS 리디렉션을 하면 루프가 생긴다.
HTTP만 제공하는 오리진에서 Full 계열이 실패할 때 코드가 갈리는 이유는 443 포트 상태다. 443이 열려 있는데 TLS를 못 하면 핸드셰이크 실패라 525, 443이 아예 닫혀 있으면 연결 거부라 521이 나올 수 있다. 이 구분은 오리진 설정에 따라 달라지므로 추정이다.
표에서 눈여겨볼 칸은 두 개다.
- Origin CA 행: Full (strict)는 정상인데 DNS Only는 경고다. EP.01 삽질 기록 1번의 정체가 이것이다.
- Full 열: 전부 "정상"인데 아무것도 검증하지 않는다. 겉보기에 가장 문제없는 열이 실제로는 인증이 빠진 열이다.
openssl과 curl로 오리진 인증서 직접 확인하기
Proxied 상태에서 도메인으로 접속하면 엣지 인증서만 보인다. 오리진 인증서를 보려면 오리진 IP를 직접 지정해야 한다.
엣지 인증서는 도메인으로 바로 확인한다.
openssl s_client -connect example.com:443 -servername example.com \
-verify_return_error </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject -enddate
오리진 인증서는 IP로 붙되 SNI는 도메인으로 보낸다. -servername을 빠뜨리면 오리진이 기본 인증서를 내밀어서 엉뚱한 결과를 본다.
# 203.0.113.10은 문서용 예시 IP다. 본인이 관리하는 오리진 IP로 바꾼다.
openssl s_client -connect 203.0.113.10:443 -servername example.com \
-showcerts </dev/null
curl --resolve는 DNS 결과만 바꿔치고 호스트명 검증은 그대로 수행한다. 브라우저가 DNS Only 상태에서 겪을 일을 그대로 재현하는 셈이다.
curl -Iv --resolve example.com:443:203.0.113.10 https://example.com/
직접 확인해 보려고 로컬에 openssl s_server를 띄우고 인증서를 바꿔 끼워 가며 실행했다. Origin CA 자리에는 직접 만든 루트 CA로 서명한 인증서를 넣었다. 루트가 시스템 신뢰 저장소에 없다는 조건이 Origin CA와 같기 때문이다. 환경은 macOS, OpenSSL 3.6.4, curl 8.7.1이다.
| 오리진 인증서 | openssl Verify return code | curl 결과 |
|---|---|---|
| 자체 서명 | 18 (self-signed certificate) | curl: (60) SSL certificate problem: self signed certificate |
| 루트가 저장소에 없는 CA (Origin CA와 같은 조건) | 21 (unable to verify the first certificate) | curl: (60) SSL certificate problem: unable to get local issuer certificate |
공인 CA (hyochan.site 엣지) | 0 (ok) | 정상 응답 |
같은 원인인데 openssl과 curl의 메시지가 다르다. 서버가 leaf 인증서 하나만 보내면 OpenSSL 3는 체인의 첫 단계에서 멈추고 21을 낸다. curl은 "발급자를 로컬에서 못 찾았다"고 말한다. 둘 다 앞의 신뢰 저장소 그림 그대로다. 로컬 저장소에 루트가 없으니 발급자를 찾지 못한다. 메시지 문구는 TLS 라이브러리 버전에 따라 조금씩 다르다(macOS 기본 curl은 LibreSSL이라 self signed처럼 하이픈이 빠진다). Cloudflare가 배포하는 Origin CA 루트를 -CAfile로 넘기면 0 (ok)로 바뀐다. 실험에서도 루트를 -CAfile(curl은 --cacert)로 넘기자마자 0 (ok)와 HTTP 200이 나왔다. 같은 인증서가 신뢰 저장소에 따라 다르게 판정되는 걸 눈으로 확인할 수 있다.
openssl s_client -connect 203.0.113.10:443 -servername example.com \
-CAfile origin_ca_rsa_root.pem </dev/null 2>/dev/null | grep 'Verify return code'
525·526과 리디렉션 루프
Cloudflare HTTPS 오류의 대부분은 525, 526, ERR_TOO_MANY_REDIRECTS 셋 중 하나다. 셋은 실패 지점이 다르다.
| 오류 | 실패 지점 | 발생 모드 | 주요 원인 |
|---|---|---|---|
| 525 SSL Handshake Failed | 엣지 ↔ 오리진 TLS 핸드셰이크 | Full, Full (strict) | 오리진 인증서 미설치, SNI 미지원, 암호 스위트 불일치 |
| 526 Invalid SSL Certificate | 핸드셰이크 후 인증서 검증 | Full (strict) | 만료, 자체 서명, 호스트명 불일치, 중간 인증서 누락, 폐기 |
| ERR_TOO_MANY_REDIRECTS | HTTP 리디렉션 반복 | 주로 Flexible | 오리진의 HTTP → HTTPS 강제 리디렉션 |
리디렉션 루프는 Flexible의 구조에서 나온다.
sequenceDiagram
participant B as Browser
participant C as Cloudflare
participant O as Origin
B->>C: https://example.com
C->>O: http://example.com (Flexible이라 HTTP)
O-->>C: 301 → https://example.com
C-->>B: 301 → https://example.com
B->>C: https://example.com
Note over B,O: 같은 요청이 계속 반복된다
오리진은 HTTP로 들어온 요청을 HTTPS로 보내는데, Cloudflare는 다시 HTTP로 오리진에 붙는다. 해결법은 오리진 리디렉션을 지우고 Cloudflare의 Always Use HTTPS로 엣지에서 리디렉션하거나, 오리진에 인증서를 넣고 Full (strict)로 올리는 것이다. 나라면 후자를 고른다.
526에서 한 가지 함정이 더 있다. 중간 인증서 누락이다. 브라우저는 캐시해 둔 중간 인증서로 체인을 알아서 채우는 경우가 있어서 직접 접속하면 멀쩡해 보인다. Cloudflare는 오리진이 보낸 체인만으로 검증하므로 526이 난다. nginx라면 ssl_certificate에 cert.pem이 아니라 fullchain.pem을 지정했는지 먼저 본다.
삽질 기록
인증서 오류가 나면 모드를 낮춰서 증상부터 없애고 싶어진다.
526이 뜨면 Full로 내리고, 525가 뜨면 Flexible로 내리면 일단 사이트는 열린다. 나도 처음엔 그렇게 넘어갔다. 그런데 이건 검증을 끈 것이지 문제를 고친 게 아니다.
- 526 → Full: 오리진 인증서 검증이 사라진다. 엣지와 오리진 사이에서 다른 인증서를 내미는 상대도 통과한다.
- 525 → Flexible: 엣지 ↔ 오리진이 평문이 된다. 로그인 폼의 비밀번호가 그 구간을 그대로 흐른다.
방문자 브라우저에는 두 경우 모두 자물쇠가 정상으로 뜬다. 그래서 문제가 있는 줄도 모르고 지나간다. 문제를 고치는 순서는 다음과 같이 잡았다.
- 위의
openssl s_client -servername으로 오리진이 실제로 내미는 인증서를 본다 - 만료일, SAN, 체인(
-showcerts로 중간 인증서까지 오는지)을 확인한다 - 인증서를 고치거나 Origin CA로 교체한 뒤 Full (strict)로 되돌린다
한 가지 더, 526 원인 목록에 폐기(revoked)가 있다. Origin CA 인증서를 키 유출 등으로 폐기하면 되돌릴 수 없다. 폐기하기 전에 새 인증서를 먼저 오리진에 걸어두지 않으면 그 순간부터 526이다.
운영 환경별 권장 구성
어떤 구성을 고를지는 "트래픽이 반드시 Cloudflare를 지나는가"에 달렸다.
| 상황 | 오리진 인증서 | 모드 | 추가 조치 |
|---|---|---|---|
| Cloudflare 경유만 허용 | Origin CA | Full (strict) | 방화벽에서 Cloudflare IP만 허용, Authenticated Origin Pulls |
| 직접 접속도 필요 / CDN 교체 가능성 | Let's Encrypt 등 공인 CA | Full (strict) | ACME 자동 갱신과 갱신 실패 모니터링 |
| 내부 서비스 / 개발 환경 | 사설 CA (여러 개면) 또는 자체 서명 (하나면) | 해당 없음 또는 Tunnel | 사설 CA 루트의 안전한 배포·관리 |
Cloudflare 경유만 허용하는 구성에서는 Origin CA가 가장 편하다. 15년 유효라 갱신 부담이 없다. 대신 EP.01 삽질 기록 2번처럼 오리진이 직접 접속을 받으면 의미가 반감된다. 방화벽에서 Cloudflare IP 대역만 허용한다.
다만 Cloudflare IP 허용은 "Cloudflare를 지나온 요청"만 보장한다. 다른 사람의 Cloudflare 계정을 거친 요청도 같은 IP에서 온다. 내 zone에서 온 요청만 받으려면 Authenticated Origin Pulls(mTLS로 Cloudflare가 클라이언트 인증서를 제시)를 함께 쓴다. 이 기능은 Off·Flexible에서는 동작하지 않는다. 아예 공인 IP를 닫고 싶다면 Cloudflare 딥다이브 EP.02에서 다룬 Tunnel을 쓴다.
직접 접속이 필요하면 공인 인증서를 쓴다. 프록시를 켜든 끄든, 나중에 다른 CDN으로 옮기든 그대로 동작한다. 갱신 주기가 짧으니 certbot이나 acme.sh의 갱신 실패를 알림으로 받아야 한다.
내부 서비스는 서비스가 여러 개면 사설 CA가 낫다. 서비스마다 자체 서명 인증서를 클라이언트에 심는 것보다 루트 하나를 심는 편이 관리가 쉽다.
신뢰도와 보안 수준은 같은 말이 아니다
"무료 인증서는 덜 안전하다", "자체 서명은 암호화가 약하다"는 둘 다 틀린 말이다. 인증서 신뢰도와 TLS 보안 수준은 서로 다른 축이다.
인증서를 평가할 때는 세 질문을 따로 던진다.
| 질문 | 확인할 것 | 예시 |
|---|---|---|
| 누가 이 인증서를 신뢰하는가 | 검증 주체의 신뢰 저장소 | 브라우저 / Cloudflare / 사설 CA를 심은 내부 클라이언트 |
| 무엇을 검증했는가 | 인증서 유형 | DV는 도메인 통제권, OV·EV는 조직 실체까지 |
| 연결 자체는 안전한가 | 서버 TLS 설정 | TLS 1.2 이상, 안전한 암호 스위트, 전방향 안전성 |
Let's Encrypt와 유료 DV 인증서를 같은 nginx 설정에 걸면 암호화 강도는 완전히 같다. 유료 인증서의 차이는 OV·EV의 조직 검증, 기술 지원, 보증 같은 부가 서비스에 있다. 그리고 공인 인증서는 "이 도메인의 통제권자"를 증명할 뿐, 그 사이트가 피싱이 아니라는 보증은 하지 않는다.
반대로 Origin CA는 브라우저가 신뢰하지 않지만 Cloudflare ↔ 오리진 구간에서는 공인 인증서와 동등하게 검증된다. 검증 주체가 Cloudflare뿐이라는 점만 다르다.
정리
브라우저의 자물쇠는 HTTPS 연결이 성공했다는 뜻이지, 모든 구간에서 서버 신원이 검증됐다는 뜻이 아니다.
| 구성 | 브라우저 ↔ 엣지 | 엣지 ↔ 오리진 | 한 줄 평가 |
|---|---|---|---|
| DNS Only | (엣지 없음) 오리진 인증서를 브라우저가 직접 검증 | - | 공인 CA 인증서 필수 |
| Proxied + Flexible | 암호화·인증 | 평문 | 인증서를 못 쓸 때의 임시방편 |
| Proxied + Full | 암호화·인증 | 암호화만 | 인증이 빠진 상태 |
| Proxied + Full (strict) | 암호화·인증 | 암호화·인증 | 기본값으로 삼을 구성 |
| 인증서 | 신뢰하는 주체 | 어울리는 곳 |
|---|---|---|
| 자체 서명 | 직접 심은 클라이언트 | 일회성 테스트 |
| 사설 CA | 루트를 배포한 클라이언트 | 내부 서비스 |
| Origin CA | Cloudflare | Cloudflare 경유 전용 오리진 |
| 공인 CA (무료·유료) | 브라우저와 Cloudflare 모두 | 직접 접속이 필요한 오리진 |
시리즈는 여기서 끝이다. 처음에 헷갈렸던 "주황 구름을 껐더니 경고가 떴다"와 "인증서가 없는데 자물쇠가 떴다"는 결국 같은 질문이었다. 어느 구간에서, 누가 인증서를 검증하느냐다.
참고자료
- Cloudflare Docs — Encryption modes
- Cloudflare Docs — Flexible
- Cloudflare Docs — Full
- Cloudflare Docs — Full (strict)
- Cloudflare Docs — Off (no encryption)
- Cloudflare Docs — Cloudflare origin CA
- Cloudflare Docs — Error 525
- Cloudflare Docs — Error 526
- Cloudflare Docs — ERR_TOO_MANY_REDIRECTS
- Cloudflare Docs — Authenticated Origin Pulls
- Cloudflare Docs — Encrypt all traffic and keep your site secure (Automatic SSL/TLS)
- cloudflare/cfssl_trust — Cloudflare 공인 CA 신뢰 저장소
- CA/Browser Forum Ballot SC-081 — 인증서 유효기간 단축
- RFC 5280 — X.509 인증서 및 경로 검증