약 25분

CT 로그에서 내 도메인의 모르는 인증서 찾기

이 글의 순서 8개 절
  1. CT 로그에 남는 것
  2. crt.sh로 찾기
  3. Cert Spotter API
  4. 우리 것인지 가리기
  5. 정상인데 낯선 발급
  6. 무단 발급이 의심되면
  7. 감시 걸어 두기
  8. 참조

example.com의 서버가 지금 내주는 인증서는 한 장이다. 2026-10-15에 Cert Spotter로 같은 이름의 CT 기록을 찾으면 아직 만료되지 않은 인증서가 13건 나온다. 발급 CA는 SSL.com과 Sectigo 두 곳이고, Sectigo 쪽은 DV와 OV가 섞여 있다.

이 13건은 대부분 설명이 된다. 이 도메인은 Cloudflare를 쓰고, Cloudflare는 평소에 쓰지 않는 예비 인증서를 다른 CA에서 받아 둔다. 내 도메인이라면 사정이 다를 수 있다. 아무도 설명하지 못하는 한 장이 목록에 섞여 있을 수 있다. 다른 팀이 외부 서비스에 서브도메인을 붙이며 받은 인증서일 수도 있고, DNS 계정을 빼앗긴 흔적일 수도 있다.

CT 로그에 남는 것

CT(Certificate Transparency)는 공인 CA가 발급한 인증서를 누구나 볼 수 있는 로그에 남기는 체계다. 2013-06에 RFC 6962로 나왔고, 2021-12에 이를 대체하는 RFC 9162(CT 2.0)가 나왔다. 둘 다 Experimental이다. 로그에서는 발급 CA, SAN(인증서에 든 이름 목록), 유효기간, 일련번호, 공개키를 볼 수 있다.

로그는 항목을 추가만 할 수 있는 머클 트리다. 누구나 읽을 수 있고, 기록을 몰래 지우거나 바꾸면 다른 관찰자가 알아챌 수 있게 설계됐다. 발급은 보통 이렇게 흐른다(RFC 6962 §3).

  1. CA가 최종 인증서와 같은 내용에 ‘독약(poison)’ 확장을 넣은 precertificate를 만들어 로그에 낸다. 이 확장(OID 1.3.6.1.4.1.11129.2.4.3)은 critical이라 일반 클라이언트는 precertificate를 인증서로 받아들이지 않는다.
  2. 로그가 ’이 항목을 기록하겠다’는 서명된 약속인 SCT(Signed Certificate Timestamp)를 돌려준다.
  3. CA가 SCT 목록을 확장(OID 1.3.6.1.4.1.11129.2.4.2)으로 넣어 최종 인증서를 발급한다.
  4. 로그는 정해진 최대 지연(MMD) 안에 그 항목을 공개 트리에 넣는다. Chrome 로그 정책은 새로 신청하는 로그에 MMD를 RFC 6962 로그 4시간, static-ct-api 로그 1분 이하로 요구한다.

RFC 9162가 나왔어도 Chrome 로그 정책이 로그에 요구하는 API는 RFC 6962 또는 C2SP의 static-ct-api(v1.1.0)다. 2026-10-14 판 Chrome 로그 목록에는 운영자 11곳의 로그가 올라 있다. RFC 6962 로그가 26개(그중 Usable 21), static-ct-api(타일) 로그가 59개(Usable 22, Qualified 37)다.

브라우저가 요구하는 SCT

Chrome, Apple 플랫폼, Firefox는 SCT가 모자란 공인 인증서를 받아들이지 않는다.

브라우저 적용 인증서에 넣은 SCT 기준
Chrome 2018-04-30 이후 발급분 수명 180일 이하 2개, 초과 3개(서로 다른 로그). 그중 2개 이상은 서로 다른 운영자
Apple 플랫폼 공인 TLS 서버 인증서 180일 이하 2개, 181~398일 3개. 운영자당 인정 개수에 상한(1개·2개), RFC 6962 로그의 SCT 1개 이상
Firefox 135(2025-02-04)부터 데스크톱 180일 이하 2개, 그 외 3개. 2개 이상은 서로 다른 운영자

기준을 못 채운 공인 인증서는 Chrome에서 net::ERR_CERTIFICATE_TRANSPARENCY_REQUIRED로 막힌다. 그러니 브라우저에서 동작하는 공인 인증서는 로그에 남는다. 몰래 받은 인증서도 쓰려면 로그를 거쳐야 한다.

실제 인증서에서 SCT를 보면 이렇다. example.com이 2026-10-15에 내준 인증서다.

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -text | grep -A4 'Signed Certificate Timestamp'
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : 94:4E:43:87:FA:EC:C1:EF:81:F3:19:24:26:A8:18:65:
                                01:C7:D3:5F:38:02:01:3F:72:67:7D:55:37:2E:19:D8
                    Timestamp : Sep 26 22:59:12.866 2026 GMT
--
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : CB:38:F7:15:89:7C:84:A1:44:5F:5B:C1:DD:FB:C9:6E:
                                F2:9A:59:CD:47:0A:69:05:85:B0:CB:14:C3:14:58:E7
                    Timestamp : Sep 26 22:59:12.894 2026 GMT

두 Log ID를 Chrome 로그 목록(log_list.json)에서 찾으면 Cloudflare ’Nimbus2026’과 DigiCert ’Sphinx2026h2’다. 수명 90일 인증서라 SCT 2개, 운영자 2곳으로 Chrome 기준을 채운다. macOS 기본 openssl(LibreSSL 3.3.6)은 이 부분을 풀지 못하고 OID와 깨진 바이트만 보여 준다. Homebrew 등으로 설치한 OpenSSL을 쓴다.

CT가 못 하는 것

CT는 발급을 막지 않는다. 이미 발급된 것을 보이게 할 뿐이다. 발급 전에 CA를 제한하는 것은 CAA이고, CAA 레코드 가이드에 따로 적었다.

사설 CA도 대상이 아니다. Chrome의 CT 정책은 공인 CA에만 적용되고, 사내 CA에는 엔터프라이즈 정책이 따로 있다.

DV 인증서에는 요청자가 남지 않는다. 어느 ACME 계정, 어느 주문자가 받았는지 인증서에 없다. OV와 EV는 주체에 조직 이름이 들어간다. 아래 예의 Sectigo OV 인증서는 O=Internet Corporation For Assigned Names and Numbers다.

반대로 인증서에 넣은 이름은 모두 공개된다. Chrome 문서도 CT로 인증서의 도메인이 공개된다는 점을 밝히고, 이름을 덜 드러내는 방법으로 와일드카드 인증서를 든다. vpn-test.example.com 같은 내부 이름을 공인 인증서에 넣으면 그 이름도 누구나 보는 목록에 오른다. 와일드카드와 SAN의 차이는 와일드카드 vs SAN 인증서에 있다.

crt.sh로 찾기

웹에서는 https://crt.sh/?q=example.com이다. 하위 도메인까지 보려면 %.example.com을 넣는다.

매개변수 뜻
exclude=expired 만료된 인증서 제외
deduplicate=Y precertificate와 최종 인증서 쌍을 하나로
output=json JSON으로 받기
spkisha256=<해시> 공개키(SPKI)의 SHA-256으로 검색

2026-10-15에 JSON으로 받은 결과다.

curl -s 'https://crt.sh/?q=example.com&exclude=expired&output=json' \
  | jq -r '.[] | [.id, .not_before, .serial_number[0:8], (.issuer_name | sub(".*CN="; ""))] | @tsv' \
  | column -t -s "$(printf '\t')"
29581277802  2026-09-26T22:49:11  01eee6aa  Cloudflare TLS Issuing ECC CA 3
29581274041  2026-09-26T22:49:11  01eee6aa  Cloudflare TLS Issuing ECC CA 3
29598205284  2026-09-26T22:34:50  5173da68  Cloudflare TLS Issuing ECC CA 3
29557945233  2026-09-24T00:00:00  2caeeaf0  Sectigo Public Server Authentication CA DV E36
28361964045  2026-07-29T22:10:08  0624d0ab  Cloudflare TLS Issuing ECC CA 3
…
22625582522  2025-11-20T00:00:00  009de105  Sectigo Public Server Authentication CA OV R36
8506962125   2023-01-27T01:21:18  1ac1e693  Issuer Not Found
10570508844  2021-08-31T15:55:23  1000      AS207960 Test CA

20줄이 나왔다. 아래는 모두 같은 날 직접 겪은 일이다.

crt.sh는 느리고 실패할 때가 있다. 공용 무료 서비스다. 이날 조회 중 HTTP 502가 여러 번 났다. 스크립트에는 시간 제한과 재시도를 둔다.

최신 기록은 늦게 들어온다. crt.sh의 Monitored Logs 페이지(2026-10-14 14:39 UTC 값)에서 Cloudflare ‘Nimbus2026’ 로그는 Backlog(아직 읽지 못한 항목) 547,758,318건, Latest Entry Age 45일이었다. Google ’Argon2026h2’는 51일, DigiCert ’Sphinx2026h2’는 43일이었다. 실제로 같은 시각 Cert Spotter에 있던 example.com 발급 3건(2026-09-17 Sectigo, 2026-09-26 SSL.com RSA 2건)이 crt.sh 결과에는 없었다.

CT 밖에서 모은 인증서도 섞인다. 마지막 줄 AS207960 Test CA의 상세 페이지는 종류가 Intermediate certificate이고, Certificate Transparency 칸이 ’No entries found’다. 그 위 줄은 crt.sh가 발급자를 찾지 못한 인증서다(Issuer Not Found).

상세 페이지 https://crt.sh/?id=<ID>에는 쓸 것이 많다. 어느 로그에 언제 올라갔는지, CRL 등 폐기 상태, CA에 문제를 알리는 링크(Report a problem with this certificate to the CA), SHA-256 지문이 있다.

crt.sh와 Cert Spotter는 수집하는 로그와 처리 지연이 달라 결과가 다를 수 있다. crt.sh에 없다고 발급이 없었던 것은 아니니, 최근 기록은 Cert Spotter로도 확인한다.

같은 인증서가 두 줄씩 보이면

위 결과의 첫 두 줄은 일련번호가 같다. 하나는 precertificate, 하나는 최종 인증서다. 상세 페이지의 Summary 칸에 종류가 적혀 있다.

crt.sh ID Summary 일련번호
29581277802 Precertificate 01eee6aabb521d5e…
29581274041 Leaf certificate 01eee6aabb521d5e…

crt.sh는 둘을 따로 보여 준다. 고급 검색의 “Deduplicate (pre)certificate pairs?”(URL로는 deduplicate=Y)를 켜면 합친다. Cert Spotter는 처음부터 하나의 ’발급(issuance)’으로 묶는다. 같은 발급의 두 기록은 tbs_sha256이 같다.

precertificate만 보여도 발급된 것으로 대한다. Mozilla 루트 정책(3.1, 5.4절)은 precertificate를 로그에 올리는 것을 최종 인증서를 발급하겠다는 구속력 있는 의사로 본다. 최종 인증서가 실제로 없어도 CA는 ‘있다고 간주되는’ 인증서를 폐기할 수 있어야 한다.

Cert Spotter API

SSLMate의 Cert Spotter는 CT 검색 API를 제공한다. 같은 날의 결과다.

curl -s 'https://api.certspotter.com/v1/issuances?domain=example.com&expand=dns_names&expand=issuer' \
  | jq -r '.[] | [.id, .not_before[0:16], .issuer.friendly_name, (.issuer.name | sub(".*CN="; "")), (.dns_names | join(","))] | @tsv' \
  | column -t -s "$(printf '\t')"
12809115180  2025-11-20T00:00  Sectigo  Sectigo Public Server Authentication CA OV R36  example.com,example.edu,example.net,example.org
…
17232708246  2026-09-17T00:00  Sectigo  Sectigo Public Server Authentication CA DV R36  *.example.com,example.com
17383412688  2026-09-24T00:00  Sectigo  Sectigo Public Server Authentication CA DV E36  *.example.com,example.com
17433287100  2026-09-26T22:34  SSL.com  Cloudflare TLS Issuing RSA CA 3                 *.example.com,example.com
17433287993  2026-09-26T22:34  SSL.com  Cloudflare TLS Issuing ECC CA 3                 *.example.com,example.com
17433445379  2026-09-26T22:48  SSL.com  Cloudflare TLS Issuing RSA CA 3                 *.example.com,example.com
17433446883  2026-09-26T22:49  SSL.com  Cloudflare TLS Issuing ECC CA 3                 *.example.com,example.com

모두 13건이다. 주요 매개변수는 이렇다.

매개변수 뜻
include_subdomains=true 하위 도메인 전체 (기본값 false)
match_wildcards=true www.example.com을 찾을 때 *.example.com 인증서도
expand=… 기본 응답에 없는 필드. dns_names, issuer, issuer.caa_domains, revocation, problem_reporting, cert_der 등
after=<id> 다음 페이지. 직전 결과의 마지막 id를 넘긴다

만료되지 않은 발급만 돌려주고, precertificate와 최종 인증서는 한 발급으로 묶는다. issuer.friendly_name은 CA 조직 이름이다. 문서도 인증서의 Issuer DN보다 이 값을 쓰라고 권한다. issuer.caa_domains에는 그 CA의 CAA 값이 온다(SSL.com은 ssl.com). 폐기 여부(revoked)도 온다. 이 목록에서는 2025-11-20 발급 Sectigo OV 인증서가 true였다.

키 없이 부르는 것은 시간당 소량, 개인·평가 용도로만 허용된다. 계정을 만들면 무료 Small 플랜 키로 시간당 이름 하나 조회 100회, 하위 도메인 포함 조회 10회를 쓸 수 있다(2026-10-15 SSLMate 안내 기준). 키는 -u "키:"처럼 Basic 인증의 사용자 이름으로 넘긴다.

우리 것인지 가리기

CT는 발급됐다는 사실만 알려 준다. 누가 왜 받았는지는 운영자가 가려야 하고, 쓰는 CA 목록과 키 목록이 있으면 그 판단이 빨라진다.

발급 CA는 조직 단위로

먼저 “우리가 쓰는 CA” 목록이 있어야 한다. ACME로 받는 Let’s Encrypt, 구매하는 인증서의 CA, 클라우드나 CDN이 대신 받는 CA까지 적는다. 이 목록은 CAA에 적을 목록과 같다.

비교는 조직 단위로 한다. 중간 인증서 이름(CN)은 자주 바뀐다. Let’s Encrypt는 중간 인증서 4개(YE1, YE2, YR1, YR2)를 번갈아 쓰고, 예비 중간 인증서(YE3, YR3)로 “예고 없이 언제든” 발급을 시작할 수 있다고 적어 둔다. CN으로 비교하면 이것이 다 새 발급자로 보인다.

이름이 오해를 부르기도 한다. 위 결과의 Cloudflare TLS Issuing ECC CA 3은 Issuer DN이 O=SSL Corporation이고 Cert Spotter의 friendly_name도 SSL.com이다. CA는 SSL.com이다.

SAN에 함께 든 이름

인증서 하나에 어떤 이름이 함께 들어 있는지를 보면 누가 받았는지 짐작할 수 있다.

SAN 모양 짐작
*.example.com + example.com CDN·호스팅의 범용 인증서(위 Cloudflare 예)
서브도메인 하나만 그 이름을 외부 서비스에 연결한 팀
다른 도메인 여러 개를 함께 같은 소유자가 묶어 산 인증서(위 Sectigo OV는 example.com, .net, .org, .edu)
처음 보는 호스트 이름 테스트·내부 서버, 또는 누군가 새로 만든 이름

짐작일 뿐이니 발급 시각과 공개키로 확인한다.

발급 시각

자동 갱신 기록과 맞춰 본다. Certbot이라면 /var/log/letsencrypt/의 로그, 배포 파이프라인이라면 그 실행 시각이다. 시각은 notBefore보다 SCT 시각이 정확하다. example.com이 내준 인증서는 notBefore가 22:49:11, SCT가 22:59:12(UTC)다. Sectigo DV E36 인증서는 notBefore가 2026-09-24 00:00:00으로 날짜만 찍혀 있고, SCT는 09:48:14(UTC)다.

몇 분 간격으로 여러 장이 나오기도 한다. 2026-09-26에는 SSL.com이 22:34에 RSA·ECC 인증서를 한 장씩, 22:48~22:49에 다시 한 장씩 발급했다. Cloudflare FAQ에 따르면 유료 존의 Universal 인증서는 RSA·ECDSA 두 가지로 발급된다. 키 종류가 둘이면 발급 기록도 둘이다.

공개키

가장 확실한 기준이다. 서버가 쓰는 키의 공개키 해시와 CT 기록의 공개키 해시를 비교한다.

# 서버가 내주는 인증서의 공개키(SPKI) SHA-256
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256

# 개인키 파일에서 바로 구할 때 (Let's Encrypt 폐기 문서의 방식)
openssl pkey -in privkey.pem -pubout -outform DER | openssl sha256
SHA2-256(stdin)= b68c8c1a989386ea15693ca0bba41e09c9ee9b63ca2649e718eb076a51fb0735

이 값을 Cert Spotter의 pubkey_sha256과 비교하거나 crt.sh에 spkisha256=로 검색한다.

curl -s 'https://api.certspotter.com/v1/issuances?domain=example.com' \
  | jq -r '.[] | select(.pubkey_sha256 == "b68c8c1a989386ea15693ca0bba41e09c9ee9b63ca2649e718eb076a51fb0735") | [.id, .not_before] | @tsv'
17433446883	2026-09-26T22:49:11Z

13건 중 서버가 지금 쓰는 키는 이 한 건이다. 나머지 12건은 서버 키와 다르다. 우리가 가진 어떤 키와도 맞지 않는 인증서는 우리 서버에 설치된 적이 없다. 반대로 공개키가 우리 키와 같은데 발급한 기억이 없다면 그 키를 다른 누가 쓰고 있다는 뜻이다. 키 유출로 보고 아래 ’구멍 막기’대로 대응한다.

판단표

관찰 가능한 원인 할 일
쓰는 CA, 갱신 시각과 맞음, 공개키가 서버 키와 같음 정상 갱신 없음
쓰는 CA, 공개키가 우리 어느 서버와도 다름 다른 서버·팀이 같은 CA로 따로 발급 담당 확인, 인벤토리에 추가
CDN이 쓰는 CA, *.도메인+apex, 서버엔 안 보임 CDN 예비 인증서 CDN 문서와 대조
처음 보는 CA, 서브도메인 하나, 외부 서비스로 CNAME 다른 팀의 외부 서비스 연결 연결한 팀 확인, CAA 목록 갱신
처음 보는 CA, 설명되는 곳이 없음 무단 발급 의심 아래 ‘무단 발급이 의심되면’

정상인데 낯선 발급

CDN의 예비 인증서

Cloudflare 문서에 따르면 Cloudflare가 권한 DNS를 맡은 도메인에는 표준 Universal 인증서마다 예비(backup) 인증서를 하나 더 발급한다. 예비 인증서는 개인키도 CA도 주 인증서와 다르다. CA는 Google Trust Services, Let’s Encrypt, Sectigo, SSL.com 중에서 고른다. 평소에는 배포하지 않고, 주 인증서가 폐기되거나 키가 유출되면 자동으로 배포한다. Free, Pro, Business 요금제는 끌 수 없고 Enterprise만 지원 요청으로 뺄 수 있다. Cloudflare의 CA 표에서 Sectigo는 예비 인증서에만 쓰고 수명은 90일이다.

example.com의 Sectigo DV 인증서 두 장(2026-09-17 R36, 2026-09-24 E36)은 이름이 *.example.com과 example.com이고 수명이 90일이 안 되며, 서버는 SSL.com 인증서를 내준다. CT 기록만으로 단정할 수는 없지만 이 설명과 맞는다. 신고하기 전에 쓰는 CDN의 문서부터 대조한다.

CAA로 이 발급만 골라 막기도 어렵다. Cloudflare를 DNS로 쓰면 Cloudflare가 자기 CA용 CAA를 대신 추가한다. FAQ는 특정 CA를 적은 CAA를 넣어도 Universal과 Advanced 인증서에 Cloudflare가 쓰는 CA는 바뀌지 않는다고 적는다. 다른 CDN이나 클라우드도 비슷한 구조일 수 있으니 쓰는 곳의 문서를 확인한다. 클라우드가 대신 받는 인증서 전반은 클라우드 관리형 인증서 가이드에 있다.

certdesk 진단 툴은 CT 로그에 서버보다 새 인증서가 있는데 서버가 아직 옛 인증서를 내주면 ’서버 미반영’으로 알려 준다. 이때 CDN이 다른 CA로 함께 받아 두는 예비 인증서는 빼고 판단한다.

다른 팀이 외부 서비스에 붙인 이름

서브도메인을 외부 서비스에 CNAME으로 연결하면 그 서비스가 인증서를 받는다. 예를 들어 GitHub Pages는 사용자 지정 도메인을 설정하면 DNS 확인을 거쳐 Let’s Encrypt 인증서를 요청한다. 문서 사이트, 랜딩 페이지, 고객 지원 도구를 붙일 때도 같은 일이 생길 수 있다. 인프라 팀이 모르는 CA가 CT에 나타나는 가장 평범한 이유다.

이때 할 일은 신고보다 확인이다. 그 이름의 CNAME이 어디를 가리키는지 보고, 연결한 팀을 찾아 인벤토리와 CAA 목록에 반영한다. CAA에 그 CA를 빠뜨리면 다음 갱신이 막힌다.

무단 발급이 의심되면

그 이름이 지금 가리키는 곳

dig +short CNAME shop.example.com
dig +short A shop.example.com
echo | openssl s_client -connect shop.example.com:443 -servername shop.example.com 2>/dev/null \
  | openssl x509 -noout -issuer -serial -dates

# ACME DNS 검증에 쓰일 수 있는 레코드가 남아 있는지
dig +short TXT _acme-challenge.shop.example.com
dig +short CNAME _acme-challenge.shop.example.com

이름이 외부 서비스를 가리키고 그 서비스가 문제의 인증서를 내준다면, 그 서비스에 이 이름을 연결한 사람이 발급자다. 팀 안에 그런 사람이 없다면 버려진 연결을 남이 차지한 경우를 의심한다.

_acme-challenge를 CNAME으로 다른 존에 위임해 두었다면 그 존을 쥔 쪽도 DNS-01로 발급할 수 있다. Let’s Encrypt 문서도 CNAME이나 NS로 챌린지 응답을 다른 존에 위임할 수 있다고 적는다.

CA에 신고하고 폐기 요청

모든 공인 CA는 인증서 문제를 신고받는 창구를 둔다. CA/B Forum Baseline Requirements(2.3.1)의 규정이다. BR 4.9.3에 따라 CA는 키 유출, 오용, 사기 등을 신고하는 방법을 온라인과 CPS 1.5.2절에 공개하고, 24시간 365일 신고를 받아야 한다. 신고(Certificate Problem Report)를 받으면 24시간 안에 조사해 가입자와 신고자 모두에게 예비 결과를 알려야 한다(4.9.5). 인증서에 든 이름의 도메인 검증을 믿을 수 없다는 증거를 얻으면 24시간 안에 폐기해야 한다(4.9.1.1). CAA를 잘못 확인했거나 CAA가 허용하지 않은 발급도 여기에 든다.

신고처는 CA의 CPS 1.5.2절에 있고, crt.sh 상세 페이지의 “Report a problem with this certificate to the CA” 링크로도 찾아갈 수 있다. Cert Spotter에 expand=problem_reporting을 붙이면 발급 CA의 안내가 함께 온다.

curl -s 'https://api.certspotter.com/v1/issuances?domain=example.com&expand=issuer&expand=problem_reporting' \
  | jq -r '.[] | select(.id == "17383412688") | .problem_reporting' | head -2
To revoke one or more certificates issued by Sectigo for which you (i) are the Subscriber or (ii) control the domain or (iii) have in your possession the private key, you may use our automated Revocation Portal here:
  · https://secure.sectigo.com/products/RevocationPortal

같은 질의에서 SSL.com 발급분은 https://www.ssl.com/revoke/가 나온다. 신고에는 crt.sh ID나 SHA-256 지문, 일련번호, 도메인 소유자가 승인하지 않았다는 점, 도메인을 통제한다는 근거(요청받은 TXT 레코드를 넣을 수 있다는 등)를 적는다.

Let’s Encrypt 인증서는 직접 폐기할 수 있다. 문서가 안내하는 방법은 가진 것에 따라 다르다.

가진 것 방법
발급한 계정 certbot revoke --cert-path <인증서>
그 인증서의 개인키 certbot revoke --cert-path <인증서> --key-path <개인키> --reason keyCompromise
도메인 통제권만 다른 계정으로 인증서의 모든 이름을 검증받은 뒤 certbot revoke --cert-path <인증서>

도메인 통제권만 있다면 인증서 파일은 crt.sh에서 받는다. 검증만 받는 방법으로 문서는 존재하지 않는 이름을 섞어 발급을 요청하는 방식을 든다. 존재하지 않는 이름 때문에 발급은 실패하지만 나머지 이름의 검증은 남는다.

certbot certonly --manual --preferred-challenges=dns \
  -d shop.example.com -d nonexistent.shop.example.com

구멍 막기

누군가 도메인 검증을 통과했다면 그 경로를 찾아 막는다.

DNS 업체와 등록 대행사 계정부터 본다. 2단계 인증, 최근 로그인과 변경 기록, 발급해 둔 API 토큰을 확인한다. DNS-01 자동화용 토큰이 웹서버에 있다면 범위를 그 존과 TXT로 좁힌다. Let’s Encrypt도 전체 DNS API 권한을 웹서버에 두면 서버 침해의 피해가 커진다고 경고한다.

버려진 CNAME도 찾는다. 해지한 외부 서비스를 아직 가리키는 이름은 남이 그 서비스에서 차지할 수 있다. MDN은 이를 서브도메인 탈취로 설명하고, 해지할 때 DNS 레코드부터 지우라고 권한다. Cert Spotter README도 버려진 서브도메인을 차지한 공격자가 받은 인증서를 탐지 대상으로 든다.

호스팅과 CDN 계정도 그 이름을 연결할 수 있는 곳이면 모두 본다.

발급한 기억이 없는데 CT의 공개키가 우리 키와 같다면 개인키가 샌 것으로 본다. 발급 요청(CSR)에는 그 개인키의 서명이 필요하기 때문이다. 새 키로 재발급하고 옛 인증서는 keyCompromise로 폐기한다.

CAA로 다시 못 받게

쓰는 CA만 issue에 남기고, ACME를 쓰면 accounturi로 계정까지 묶는다. CAA는 이미 발급된 인증서에는 영향이 없고 다음 발급부터 막는다. 상위 도메인 탐색, 와일드카드, CA별 값, 2027-03-15부터 의무가 되는 accounturi 처리는 CAA 레코드 가이드에 있다.

감시 걸어 두기

조회는 사고가 난 뒤에 하는 일이다. 새 발급이 로그에 올라오면 알려 주는 감시를 걸어 둔다. 어려운 건 무엇을 알릴지다. example.com 한 이름에서만 2026-09 한 달에 발급이 6건 나왔다. 인증서 수명이 2027-03-15 100일, 2029-03-15 47일로 줄면(47일 시대) 정상 갱신 기록은 더 늘어난다. 모든 발급을 즉시 알리면 정말 봐야 할 한 건이 묻힌다.

그래서 처음 보는 CA의 발급은 바로 알리고, 쓰던 CA의 갱신은 모아서 본다.

certdesk 만료 감시 구독

만료 감시 구독에 도메인을 등록하면 매일 점검할 때 CT 로그도 함께 본다. 처음 보는 CA의 인증서가 올라오면 그날 바로 메일로 알리고, Slack, Discord, Telegram 채널을 연결했다면 거기에도 보낸다. 쓰던 CA의 갱신은 월요일 주간 요약에 모은다.

등록한 날은 그때까지의 발급 CA를 기준으로 잡고 알리지 않는다. CA는 조직 단위로 본다. Let’s Encrypt의 중간 인증서가 바뀌어도 새 CA로 치지 않는다. 보는 범위는 등록한 이름이 들어간 인증서이고, 그 이름을 덮는 와일드카드도 포함한다. 하위 도메인 전체를 훑으려면 아래 도구를 함께 쓴다. 도메인 5개까지 무료이고, 만료와 보안 등급 알림도 같은 구독으로 받는다.

Cert Spotter 오픈소스

SSLMate가 공개한 certspotter는 데이터베이스 없이 로그를 직접 읽는 감시 도구다. Chrome과 Apple 로그 목록에서 Qualified, Usable, ReadOnly 상태인 로그를 보고, RFC 6962 로그와 static-ct-api 로그를 모두 읽는다.

go install software.sslmate.com/src/certspotter/cmd/certspotter@latest
mkdir -p ~/.certspotter
echo '.example.com' > ~/.certspotter/watchlist          # 앞에 점: 도메인과 하위 전체
echo 'ops@example.com' > ~/.certspotter/email_recipients  # sendmail 이 동작해야 한다

certspotter는 데몬으로 계속 띄워 둔다. -start_at_end를 주면 로그를 처음부터 읽지 않아 대역폭을 크게 아끼지만, 시작 전에 올라간 인증서는 알리지 않는다. 알림 대신 스크립트를 돌리려면 ~/.certspotter/hooks.d에 실행 파일을 둔다. 알고 있는 정상 인증서는 certspotter-authorize로 등록해 알림에서 뺀다.

직접 짜는 스크립트

Cert Spotter API로 마지막으로 본 발급 id를 기억해 두고, 그 뒤 발급 중 허용 목록에 없는 CA만 출력한다. 첫 실행은 기준선만 잡는다.

#!/bin/bash
# ct-watch.sh — CT 로그의 새 발급 중 허용 목록에 없는 CA 를 출력한다 (출력이 있으면 exit 1)
# 사용법: CERTSPOTTER_API_KEY=... ./ct-watch.sh example.com   (cron 으로 1시간에 한 번)
d=$1
allow="Let's Encrypt|SSL.com"            # 쓰는 CA — Cert Spotter 의 friendly_name, | 로 구분
st="${CT_WATCH_DIR:-$HOME/.ct-watch}/$d"; mkdir -p "${st%/*}"
last=$(cat "$st" 2>/dev/null); after=$last; found=0
while :; do
  page=$(curl -fsS ${CERTSPOTTER_API_KEY:+-u "$CERTSPOTTER_API_KEY:"} \
    "https://api.certspotter.com/v1/issuances?domain=$d&include_subdomains=true&expand=dns_names&expand=issuer${after:+&after=$after}") || exit 2
  [ "$(jq length <<<"$page")" -eq 0 ] && break
  if [ -n "$last" ]; then                  # 첫 실행은 기준선만 잡는다
    hits=$(jq -r --arg a "$allow" '.[] | select((.issuer.friendly_name // "?") | test("^(" + $a + ")$") | not)
      | "\(.not_before) \(.issuer.friendly_name // .issuer.name) \(.dns_names | join(","))"' <<<"$page")
    [ -n "$hits" ] && { echo "$hits"; found=1; }
  fi
  after=$(jq -r '.[-1].id' <<<"$page")
done
[ -n "$after" ] && echo "$after" > "$st"
exit $found

2026-10-15에 example.com으로 시험했다. 첫 실행은 출력 없이 마지막 id(17433446883)만 저장했다. 기준선을 9월 이전(17000000000)으로 되돌리고 다시 돌리면 허용 목록(Let’s Encrypt, SSL.com)에 없는 두 건이 나오고 종료 코드는 1이다.

2026-09-17T00:00:00Z Sectigo *.example.com,example.com
2026-09-24T00:00:00Z Sectigo *.example.com,example.com

종료 코드는 0(새 CA 없음), 1(있음), 2(조회 실패)다. 조회 실패를 ’이상 없음’으로 넘기지 않는다. CDN 예비 인증서처럼 정상인 CA가 있으면 허용 목록에 넣는다. 위 결과의 Sectigo 두 건이 그런 경우다.

include_subdomains=true 조회는 무료 키 기준 시간당 10회다. 페이지를 넘길 때도 요청을 따로 보내므로 도메인이 많으면 실행 간격을 늘린다.

지금 서버가 내주는 인증서와 CAA는 진단 툴에서 함께 본다.

참조