CAA 레코드가 하는 일

CAA(Certification Authority Authorization) 레코드는 DNS 레코드(타입 257)로, 어떤 인증 기관(CA)이 그 이름의 인증서를 발급할 수 있는지 적어 둡니다. 2017년 9월부터 모든 공인 CA는 발급 전에 이 레코드를 조회하고, 자신이 목록에 없으면 발급을 거부해야 합니다.

구조는 이게 전부이고, 하지 못하는 일도 알아 둬야 합니다.

  • 확인하는 주체는 발급 시점의 CA입니다. 브라우저는 CAA를 읽지 않습니다. 이미 발급된 인증서는 DNS에 무엇을 넣든 계속 동작합니다.
  • 공개 규정을 따르는 CA에만 효력이 있습니다. 해킹당한 CA나, 내 기기에 직접 설치한 사설 CA에는 아무 효과가 없습니다.
  • CAA 레코드가 없으면 어느 공인 CA든 요청자가 검증을 통과하는 한 발급할 수 있습니다. 대부분의 도메인이 이 상태이고 잘못된 것이 아닙니다. CAA는 목록을 좁힐 뿐, 검증을 더해 주지 않습니다.

그래서 CAA는 한 가지 실패를 싸게 막아 줍니다. 내가 쓰지도 않는 CA에서, 더 약한 검증 경로나 실수를 통해 누군가 내 이름의 인증서를 받는 일입니다. 넣어 둘 가치가 있습니다. 동시에 갱신이 갑자기 CAA record for example.com prevents issuance로 실패하는 원인이기도 하고, 이 글을 찾는 분들 대부분이 겪는 상황이 이쪽입니다.

방문자 입장에서는 CAA 때문에 브라우저 오류가 나는 일은 없습니다. 사이트에서 인증서 경고가 뜬다면 원인은 만료, 이름 불일치, 신뢰할 수 없는 체인 같은 다른 곳에 있습니다.

레코드 문법

CAA 레코드는 플래그 바이트, 태그, 따옴표로 감싼 값 세 부분으로 이루어집니다. 존 파일 형식으로는 이렇습니다.

example.com.  3600  IN  CAA  0 issue "letsencrypt.org"
example.com.  3600  IN  CAA  0 issuewild ";"
example.com.  3600  IN  CAA  0 iodef "mailto:security@example.com"

DNS 업체 관리 화면에서는 보통 같은 세 값을 칸마다 따로 넣습니다. 플래그 0, 태그 issue, 값 letsencrypt.org.

RFC 8659가 정의하는 태그는 다음과 같습니다.

태그의미
issue이 CA는 이 이름의 인증서를 발급할 수 있음. CA마다 레코드 하나씩, 여러 개면 그중 어느 CA든 발급 가능
issuewild같은 뜻이지만 와일드카드 인증서에만 해당. issuewild가 하나라도 있으면 와일드카드 요청에는 issue 대신 이것만 적용되고, 없으면 와일드카드도 issue를 따름
iodefCA가 거부한 요청을 보고할 곳(mailto: 또는 https: URL). CA에게 보고 의무가 없고 보내지 않는 곳도 많으니 선택 사항으로 생각하세요

값 ";"(CA 식별자를 비워 둔 것)은 “어떤 CA도 안 됨”이라는 뜻입니다. 0 issue ";"는 그 이름의 발급을 전부 막습니다. 파킹 도메인이나 메일 전용 도메인처럼 인증서가 있을 일이 없는 도메인에 알맞은 설정입니다.

플래그는 거의 항상 0입니다. 정의된 플래그는 128(“issuer critical”) 하나뿐이며, CA가 이 레코드의 태그를 이해하지 못하면 발급하지 말라는 뜻입니다. 모든 CA가 반드시 지켜야 하는 태그에만 쓰는 것이고, 알 수 없는 태그에 128을 걸면 모든 곳에서 발급이 막힙니다. 128이 왜 필요한지 확실히 알 때가 아니면 0으로 두세요.

S/MIME(메일 서명) 인증서용 issuemail 태그도 따로 있습니다. 웹사이트용 TLS 인증서에는 영향을 주지 않습니다.

CA 식별자

issue 값은 CA가 CAA 식별자로 공개한 도메인 이름입니다. 내 CA가 문서로 밝힌 값을 쓰세요. 자주 쓰는 것은 이렇습니다.

CA식별자
Let’s Encryptletsencrypt.org
Google Trust Servicespki.goog
AWS Certificate Manageramazon.com (AWS는 amazontrust.com 등 관련 값 몇 가지도 받음)
DigiCertdigicert.com
Sectigosectigo.com

인증서를 산 곳의 이름으로 식별자를 짐작하지 마세요. 인증서 판매 대행사, 호스팅 업체, CDN은 내가 고르지 않은 CA로 발급하는 경우가 많습니다. 지금 인증서를 실제로 누가 발급했는지 SSL 점검이나 다음 명령으로 확인합니다.

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer

인증서가 따로 있는 모든 호스트명에 대해 같은 확인을 하세요. 제3자가 대신 관리하는 것도 포함입니다. CDN, CNAME으로 붙인 고객센터·상태 페이지, 메일 호스팅 등입니다. 이 발급자들이 모두 issue에 있어야 다음 갱신이 실패하지 않습니다. 지난 1년간 내 이름으로 인증서를 발급한 CA를 빠짐없이 보려면 Certificate Transparency 로그에서 도메인을 검색하는 것이 가장 확실합니다.

바로 쓰는 예시

Let’s Encrypt만 허용(와일드카드 포함):

example.com.  CAA  0 issue "letsencrypt.org"

Let’s Encrypt와 Google Trust Services 허용, 와일드카드는 누구에게도 불허:

example.com.  CAA  0 issue "letsencrypt.org"
example.com.  CAA  0 issue "pki.goog"
example.com.  CAA  0 issuewild ";"

일반 인증서는 Let’s Encrypt, 와일드카드는 DigiCert만:

example.com.  CAA  0 issue "letsencrypt.org"
example.com.  CAA  0 issuewild "digicert.com"

이 경우 Let’s Encrypt에 와일드카드를 요청하면 실패합니다. issuewild가 있으면 와일드카드에는 더 이상 issue가 적용되지 않기 때문입니다.

웹에 쓰지 않는 도메인이라 인증서 발급을 아예 막을 때:

example.org.  CAA  0 issue ";"

내 ACME 계정으로만 발급되게 묶기

RFC 8657은 issue에 파라미터 두 개를 더합니다. accounturi는 발급을 특정 ACME 계정으로 제한하고, validationmethods는 도메인 검증 방식을 제한합니다.

example.com.  CAA  0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890; validationmethods=dns-01"

이 레코드가 있으면 웹 서버는 장악했지만 내 ACME 계정은 없는 사람, 혹은 다른 경로로 HTTP-01 챌린지를 통과할 수 있는 사람도 Let’s Encrypt에서 인증서를 받지 못합니다. Let’s Encrypt는 두 파라미터를 모두 지원합니다. 다른 CA에서 쓰려면 그 CA 문서를 먼저 확인하세요. certbot에서는 certbot show_account가 계정 URL을 보여 줍니다. 계정을 새로 만들면 레코드도 고쳐야 한다는 점을 잊지 마세요. 안 그러면 갱신이 멈춥니다.

CA가 레코드를 찾는 방법: 트리 거슬러 오르기

CA는 요청한 이름만 보지 않습니다. 그 이름이나 그 위에서 가장 가까운 CAA 레코드를 찾습니다.

  1. 요청한 이름, 예를 들어 app.eu.example.com의 CAA를 조회합니다.
  2. 거기에 CAA가 없으면 맨 왼쪽 라벨을 떼고 eu.example.com을 조회합니다.
  3. 이어서 example.com, 그런 식으로 최상위 도메인까지 올라갑니다.
  4. CAA 레코드가 처음 나타나는 이름의 것이 적용됩니다. 그보다 위는 무시됩니다.

여기서 두 가지가 자주 발목을 잡습니다.

example.com의 레코드는 자기 CAA가 없는 모든 서브도메인에 적용됩니다. 루트를 Let’s Encrypt로 잠그면 shop.example.com, mail.example.com 등 그 아래 모든 이름도 함께 잠깁니다.

레코드는 합쳐지지 않습니다. example.com이 letsencrypt.org를 허용하는데 shop.example.com에 0 issue "digicert.com"을 넣으면, shop.example.com은 DigiCert만 허용합니다. CA가 shop.example.com에서 오르기를 멈추기 때문에 그 이름에서는 Let’s Encrypt가 거부됩니다. 서브도메인에 레코드를 둔다면 그 서브도메인에 필요한 CA를 전부 적어야 합니다.

CA가 보는 것을 직접 보려면 트리를 거슬러 올라가 보세요.

name=app.eu.example.com
while [ -n "$name" ]; do
  echo "== $name"; dig +short CAA "$name"
  case "$name" in *.*) name=${name#*.} ;; *) name="" ;; esac
done
== app.eu.example.com
== eu.example.com
== example.com
0 issue "letsencrypt.org"
0 issuewild ";"
== com

여기서는 app.eu.example.com에 example.com의 레코드가 적용됩니다. DNS 조회는 어떤 이름이든 우리 네트워크 밖에서 CAA 레코드를 보여 주므로 단계마다 교차 확인하기 좋습니다.

CNAME이 있으면 답이 달라진다

요청한 이름이 CNAME이면, 그 이름의 CAA 조회도 다른 DNS 조회처럼 CNAME을 따라갑니다. 예를 들어

status.example.com.  CNAME  example.statuspage-provider.net.

이라면 CA는 example.statuspage-provider.net의 CAA를 받습니다(있다면). 서비스 업체가 거기에 CAA를 두었다면 그 레코드가 내 status.example.com에 적용되고, 내 레코드는 그 이름에 대해 조회조차 되지 않습니다.

대상에 CAA가 없으면 CA는 업체 쪽이 아니라 내 트리를 example.com부터 거슬러 오릅니다. RFC 8659에서는 원래 이름을 따라 오릅니다. (예전 RFC 6844는 대상 쪽 상위 도메인도 거슬러 올랐는데, 이 부분이 빠졌습니다.)

실무에서 의미하는 것은 이렇습니다.

  • CNAME으로 붙인 외부 서비스(고객센터, 상태 페이지, 문서 호스팅, 쇼핑몰)는 대개 내 사용자 도메인의 인증서를 서비스가 직접 발급합니다. 대상에 CAA가 없으면 내 루트 레코드가 그 발급의 허용 여부를 정합니다. 루트를 CA 하나로 잠그면 몇 달 뒤 그 서비스의 갱신이 실패하고, 보통 아무도 그것을 DNS 변경과 연결 짓지 못합니다. CAA를 좁히기 전에 외부 서비스로 가는 CNAME을 모두 나열하고 각자 어떤 CA를 쓰는지 확인하세요.
  • CNAME인 이름에는 CAA 레코드를 추가할 수 없습니다. DNS는 CNAME 옆에 다른 레코드를 허용하지 않습니다. 그 이름의 CAA를 직접 정하려면 CNAME을 없애야 하는데, SaaS라면 보통 불가능합니다.

“CAA record prevents issuance” 해결

Let’s Encrypt는 이 오류를 urn:ietf:params:acme:error:caa 유형으로, CAA record for example.com prevents issuance 같은 문구와 함께 보고합니다. 다른 CA는 표현이 다르지만 절차는 같습니다.

메시지 속 이름을 보세요. 요청한 이름의 상위 도메인인 경우가 많고, 거기가 오르기를 멈춘 지점입니다.

  1. 트리를 거슬러 오릅니다. 위의 반복문을 주문에 들어간 정확한 이름(와일드카드는 *.를 뗀 이름)부터 돌립니다. 레코드가 처음 나오는 단계가 적용되는 곳입니다.
  2. CNAME을 확인합니다. 그 이름이 CNAME이면 내가 보고 있는 것이 대상의 CAA일 수 있습니다.
  3. 식별자와 실제 요청 CA를 비교합니다. 여러 CA를 지원하는 ACME 클라이언트는 버전에 따라 기본 CA가 바뀌기도 했고, 호스팅 관리 화면이 알리지 않고 CA를 바꾸기도 합니다. DNS를 고치기 전에 클라이언트가 실제로 어느 CA와 통신하는지(설정이나 로그의 ACME 디렉터리 URL) 확인하세요.
  4. 와일드카드 요청이라면, 그 단계에 issuewild가 하나라도 있으면 그것만으로 결정됩니다. 예전 잠금 설정에서 남은 issuewild ";"가 대표적인 원인입니다.
  5. accounturi나 validationmethods가 있다면 요청이 그 계정에서, 그 방식으로 와야 합니다. 새 서버에서 새 ACME 계정으로 요청하면 여기서 실패합니다.
  6. 레코드를 고친 뒤 기다립니다. 리졸버는 TTL 동안 예전 응답을 들고 있습니다. CA 규정상 CA는 CAA 확인 결과를 레코드 TTL과 8시간 중 긴 쪽까지 쓸 수 있으므로, 5분 뒤 재시도해도 예전 레코드를 볼 수 있습니다. 계획된 CAA 변경이라면 하루 전에 TTL을 낮춰 두세요.

비슷해 보이는 다른 실패도 있습니다. CAA 조회 자체가 실패하는 경우입니다. 권한 네임서버가 CAA 질의에 시간 초과나 SERVFAIL을 내면(예전 DNS 장비나 로드밸런서 중에 모르는 레코드 타입에 이렇게 응답하는 것이 있었습니다), CA는 보통 이를 “레코드 없음”으로 볼 수 없어 발급을 거부합니다. Let’s Encrypt는 이 경우 CAA 거부가 아니라 CAA 확인 중 DNS 오류로 보고합니다. 내 네임서버에 직접 물어보세요.

dig CAA example.com @ns1.your-dns-provider.net

정상이라면 레코드가 있든 없든 NOERROR입니다. SERVFAIL이나 시간 초과라면 레코드가 아니라 DNS 서버가 문제입니다.

관리형 인증서와 CDN

나 대신 인증서를 발급하는 플랫폼은 그 CA가 내 CAA 목록에 있어야 합니다.

  • AWS Certificate Manager 인증서(CloudFront, 로드밸런서)는 Amazon 식별자 중 하나가 필요합니다.
  • Cloudflare 엣지 인증서는 여러 CA가 발급하며, 현재 Let’s Encrypt와 Google Trust Services가 포함됩니다. Cloudflare 영역에 CAA 레코드를 추가하면 Cloudflare가 Universal SSL에 쓰는 CA 항목을 함께 추가합니다. 저장한 뒤 레코드 목록에서 실제로 무엇이 들어갔는지 확인하고, 직접 관리하는 인증서로 옮긴 것이 아니라면 그 항목은 남겨 두세요.
  • CNAME으로 붙인 SaaS는 앞에서 설명한 그대로입니다.

위험은 특정 CA에 묶이는 것입니다. 장애 중에 다른 CA로 재발급하는 CDN이나 CA를 바꾸는 플랫폼은, 예전 CA 하나만 적힌 CAA에 막힙니다. CAA를 쓰지 말라는 이유는 아니지만, 플랫폼이 CA 두 곳을 문서에 밝혔다면 두 곳을 모두 적고 갱신을 지켜볼 이유는 됩니다.

무리 없는 도입 순서

  1. 현재 발급자를 파악합니다. 호스트명마다 SSL 점검을 하고, Certificate Transparency 로그에서 도메인을 검색합니다.
  2. 외부 서비스로 가는 CNAME을 모두 나열하고 각각 어떤 CA를 쓰는지 확인합니다.
  3. 하루 전에 루트의 TTL을 낮춥니다(300초면 충분합니다).
  4. 찾아낸 모든 CA의 issue를 게시합니다. 와일드카드를 다르게 제한하고 싶을 때만 issuewild를, 원하면 iodef를 넣습니다.
  5. 시험 발급이나 갱신을 돌리고(certbot renew --dry-run은 Let’s Encrypt 스테이징 CA를 쓰며, 스테이징도 CAA를 똑같이 확인합니다) 트리를 한 번 더 거슬러 올라가 봅니다.
  6. TTL을 다시 올립니다.

와일드카드 쪽 이야기는 와일드카드 인증서에, 이런 변경 뒤 갱신이 조용히 실패하지 않게 하는 방법은 만료 체크리스트에 정리해 두었습니다.

내 도메인에서 바로 확인

무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.