[PKI] 오프라인 루트 CRL 배포 누락이 DMZ WAP에서 터지는 이유 — 체인 폐기검증 트러블슈팅
들어가며
2-Tier PKI와 ADFS 팜을 세운 뒤 DMZ에 WAP(Web Application Proxy)를 붙이려는데, WAP에 반입한 ADFS SSL 인증서가 체인 검증(Verify())에서 계속 False가 나왔음. 처음엔 방화벽 문제로 봤는데 아니었고, 원인은 2-Tier CA 구축 때 오프라인 루트 CRL을 배포지점에 안 올린 데 있었음.
오프라인 루트 + DMZ 조합에서 나오는 문제라 기록으로 남김.
환경값은 예시로 치환함. 도메인
lab.local, 내부망10.0.0.x, DMZ10.10.10.x, CA는lab-ROOTCA-CA(오프라인 루트) /lab-ISCA01-CA(발급 CA), federation 이름sts.lab.local.
구성 배경
- 2-Tier PKI: ROOTCA(workgroup, 오프라인,
10.0.0.231) + ISCA01(도메인 조인, 발급 CA,10.0.0.232) - CDP/AIA: ISCA01의 IIS를 HTTP 배포지점으로 사용.
pki.lab.local(CNAME) → ISCA01,/CertEnroll가상 디렉터리 - ADFS 팜:
sts.lab.local, NLB VIP + ADFS01/02 - WAP01: DMZ(
10.10.10.10), workgroup(도메인 미가입), OPNsense로 격리. 방화벽은DMZ → ADFS01:443만 허용
WAP는 백엔드 ADFS와 신뢰를 맺기 전에, 반입한 SSL 인증서의 체인을 검증해야 함. 그런데 여기서 막힘.
$c = Get-ChildItem Cert:\LocalMachine\My\<thumbprint>
$c.Verify()
# False
방화벽부터 의심 — 그러나 아니었음
certutil이 **** OFFLINE ****을 뱉었음. DMZ 격리를 해놨으니 WAP가 내부 PKI(pki.lab.local)에 못 닿는 것으로 판단함. OPNsense 규칙이 DMZ → ADFS01:443만 열려 있으니 CDP(:80)가 차단됐다는 논리였음.
방화벽에 WAP01 → ISCA01:80 규칙을 추가하고 재검증했지만 여전히 False. 그래서 curl로 실제 도달 여부를 직접 확인함:
curl.exe -v http://pki.lab.local/CertEnroll/lab-ISCA01-CA.crl
* IPv4: 10.0.0.232
* Established connection ... from 10.10.10.10 port 60237
< HTTP/1.1 200 OK
< Content-Type: application/pkix-crl
< Content-Length: 874
200 OK. DMZ에서 ISCA01의 CRL이 HTTP로 정상 내려옴. 방화벽·DNS·라우팅은 문제가 아니었음.
참고:
certutil이 OFFLINE을 뱉은 건 네트워크가 막혀서가 아니라, 초기 실패 판정이 WinHTTP URL 캐시에 박혀 재사용됐기 때문.certutil -urlcache * delete로 비우면 그 오탐은 사라짐. 하지만 캐시를 비워도Verify()는 여전히 False였음 — 진짜 원인은 따로 있었다는 뜻.
certutil -verify로 체인 뜯어보기
Verify()는 True/False만 주니 어디서 막히는지 안 보임. 체인 요소별 상태를 보려면 certutil -verify가 정답임.
Export-Certificate -Cert $c -FilePath C:\temp\sts.cer -Type CERT | Out-Null
certutil -verify -urlfetch C:\temp\sts.cer
출력 핵심만 추리면:
CertContext[0][0] Subject: CN=sts.lab.local ← leaf(ADFS 인증서)
Verified "Base CRL (03)" ... http://pki.lab.local/CertEnroll/lab-ISCA01-CA.crl ← 통과
CertContext[0][1] Subject: CN=lab-ISCA01-CA ← 중간(발급 CA)
Element.dwErrorStatus = CERT_TRUST_REVOCATION_STATUS_UNKNOWN (0x40)
Element.dwErrorStatus = CERT_TRUST_IS_OFFLINE_REVOCATION (0x1000000)
CDP: Error retrieving URL ... 0x80070032 ERROR_NOT_SUPPORTED
file:////ROOTCA/CertEnroll/lab-ROOTCA-CA.crl ← 여기서 터짐
CertContext[0][2] Subject: CN=lab-ROOTCA-CA ← 루트(트러스트 앵커)
두 가지가 확정됨.
첫째, leaf는 문제없었음. ADFS 인증서 자체의 폐기 여부는 ISCA01의 HTTP CRL로 이미 검증됨(Verified "Base CRL (03)"). curl로 확인한 그 CRL이 실제로 쓰인 것.
둘째, 막힌 건 중간 인증서(ISCA01)의 폐기검증임. 체인 검증은 leaf 하나만 보는 게 아니라 루트까지 각 단계의 폐기 여부를 확인함:
sts(ADFS)가 폐기됐나? → ISCA01 CRL로 확인 → OK- 그
ISCA01은 폐기됐나? → ROOTCA CRL로 확인 → 실패
실패 이유는 ISCA01 인증서에 박힌 ROOTCA CDP가 http가 아니라 file:////ROOTCA/CertEnroll/... (UNC 공유 경로)라는 데 있음. WAP01은 workgroup·DMZ라 오프라인 ROOTCA의 파일 공유에 접근할 수 없음 → ERROR_NOT_SUPPORTED → 중간 인증서 revocation “unknown” → 전체 체인 CRYPT_E_REVOCATION_OFFLINE.
근본 원인 — ROOTCA CRL이 어디에도 배포 안 됨
file:// CDP가 박힌 것 자체가 신호였음. 2-Tier PKI 구축 때 ROOTCA의 CDP/AIA를 http로 재설정하지 않고 기본값(로컬 file 경로)으로 루트 인증서를 발급했다는 뜻. 게다가 ISCA01의 배포지점을 확인해보니:
# ISCA01에서
dir C:\Windows\System32\CertSrv\CertEnroll\*.crl
lab-ISCA01-CA+.crl (delta)
lab-ISCA01-CA.crl
ISCA01 CRL만 있고 ROOTCA CRL 파일 자체가 없음. 즉 오프라인 루트의 CRL을 발급 CA의 HTTP 배포지점에 복사(publish)하는 단계를 빠뜨린 것.
이게 왜 지금까지 안 걸렸나? 도메인 조인된 내부 머신들은 AD에 게시된 CRL(LDAP)이나 로컬 캐시로 검증을 넘겼기 때문. workgroup·DMZ인 WAP01은 그 경로가 없어서 이 결함이 드러난 것.
해결 — 오프라인 루트 CRL 수동 확보 + 로컬 심기
ROOTCA는 발급을 안 하니 CRL이 자동 갱신되지 않음. 수동으로 뽑아야 함.
1) 오프라인 ROOTCA에서 CRL 생성
# ROOTCA(10.0.0.231) 콘솔에서
certutil -CRL
dir C:\Windows\System32\CertSrv\CertEnroll\*.crl # lab-ROOTCA-CA.crl 생성 확인
2) 오프라인 머신에서 파일 옮기기 (네트워크 없이)
ROOTCA는 오프라인이라 네트워크 복사가 안 됨. CRL은 1~2KB로 작으니 Base64로 텍스트화해 옮김. Hyper-V 확장 세션(Enhanced Session)이면 클립보드 복붙으로 충분함.
# ROOTCA에서 — CRL을 Base64 문자열로
$crl = Get-Content C:\Windows\System32\CertSrv\CertEnroll\lab-ROOTCA-CA.crl -Encoding Byte
[Convert]::ToBase64String($crl) | Set-Clipboard
# WAP01에서 — 붙여넣어 파일 복원
$b64 = 'MIIDE...(전문)...==' # 작은따옴표 필수 (+ / = 보호)
[IO.File]::WriteAllBytes('C:\temp\lab-ROOTCA-CA.crl', [Convert]::FromBase64String($b64))
# 복원 검증 — 진짜 CRL인지
certutil -dump C:\temp\lab-ROOTCA-CA.crl | Select-Object -First 15
# Issuer: CN=lab-ROOTCA-CA, ThisUpdate/NextUpdate 보이면 정상
3) 로컬 CA 저장소에 심고 재검증
certutil -addstore -f CA C:\temp\lab-ROOTCA-CA.crl
certutil -urlcache * delete
$c = Get-ChildItem Cert:\LocalMachine\My\<thumbprint>
$c.Verify()
# True
이제 sts → ISCA01은 HTTP CRL로, ISCA01 → ROOTCA는 로컬에 심은 CRL로 검증됨. DMZ 격리를 유지한 채 네트워크 없이 체인 전체가 통과함.
검증 — WAP 신뢰 등록과 외부 중계
체인이 잡힌 뒤 신뢰 등록과 외부 중계 검증.
# WAP 역할 설치
Install-WindowsFeature Web-Application-Proxy -IncludeManagementTools
# 프록시 신뢰 등록 (workgroup이면 도메인 접두어 필수)
$cred = Get-Credential lab\adm-admin
Install-WebApplicationProxy `
-FederationServiceName "sts.lab.local" `
-CertificateThumbprint "<thumbprint>" `
-FederationServiceTrustCredential $cred
# Status: Success / DeploymentSucceeded
외부 중계 최종 확인 — WAP 경유로 ADFS 메타데이터가 나오는지:
curl.exe -v https://sts.lab.local/federationmetadata/2007-06/federationmetadata.xml
< HTTP/1.1 200 OK
< Content-Type: application/samlmetadata+xml
<EntityDescriptor ... entityID="http://sts.lab.local/adfs/services/trust">...
TLS 핸드셰이크가 에러 없이 끝나고 <EntityDescriptor>가 내려옴. 앞서 심은 ROOTCA CRL이 없었다면 이 단계에서 schannel이 신뢰 실패로 끊겼을 것.
정리 — 핵심 교훈
-
가설은 확인 전에 믿지 말 것. “방화벽이 막는다”는 그럴듯했지만 curl로 확인하니 아니었음.
Verify()의 True/False만 보지 말고certutil -verify로 체인 요소별 상태를 봐야 원인이 드러남. -
체인 폐기검증은 leaf만이 아니라 각 단계를 다 확인함. leaf(ADFS)는 발급 CA의 CRL로, 발급 CA는 루트의 CRL로 검증됨. 중간 어느 한 단계라도 CRL을 못 구하면 전체가
REVOCATION_STATUS_UNKNOWN으로 보류됨. -
오프라인 루트 + DMZ면 루트 CRL 배포가 필수. 오프라인 ROOTCA의 CRL을 발급 CA의 HTTP 배포지점에 publish하지 않으면,
file://CDP는 도메인 미가입·격리 환경에서 탈 수 없음. 이런 환경에서 문제가 먼저 나타남. -
근본 해결은 ROOTCA CDP/AIA를 http로 재설계하는 것. 이번엔 로컬 심기로 우회했지만, 이는 CRL 만료(NextUpdate) 시 다시 깨지는 임시책임. 제대로 하려면
CAPolicy.inf/certutil -setreg로 ROOTCA의 CDP/AIA를 http 경로로 지정하고 루트 인증서를 재발급해 배포지점에 CRL을 올려둬야 함.
격리 자체가 검증을 막은 게 아니라, 원래 있던 배포 누락이 격리 환경에서 드러난 것뿐임. DMZ는 결함을 만든 게 아니라 노출시킨 쪽에 가까움.
Comments