세계 최초로 cert-manager 버그를 발견하고 해결하기

이상유

안녕하세요, 데브시스터즈 진저랩 인프라셀에서 데브옵스 엔지니어로 일하고 있는 이상유입니다.

여러분은 Let’s Encrypt를 알고 계시나요? 자세히 아는 것이 아니라도, SSL/TLS 인증서를 무료로 발급해 주는 서비스라고 많은 분들께서 한 번쯤은 들어 보았을 것이라고 생각합니다.

데브시스터즈에서는 쿠버네티스 환경에서 통신 암호화를 위해 Let’s Encrypt 인증서를 사용하고 있는데요, 최근 흥미로운 이슈를 발견하여 오픈소스 기여까지 하게 된 사례가 있어 이 글을 통해 이야기를 풀어보고자 합니다.

네가 왜 거기서 나와?

지난 2월 13일, 설 연휴를 보내고 돌아온 인프라셀에 한 가지 요청이 들어옵니다. 개발 서버에 접속이 되지 않는데, 확인을 부탁드린다는 요청이었습니다.

연휴 직후였기 때문에 사람이 개입한 변경사항도 존재하지 않던 시점이었기에, 무엇이 문제일까 확인하던 도중 아래와 같은 화면을 보게 되었습니다.

DST Root CA X3이 포함된 Chain을 사용하고 있는 Cert Chain 스크린샷
DST Root CA X3이 포함된 Chain을 사용하고 있는 Cert Chain 스크린샷

2년 전인 2021년 9월 말 인증서가 만료된 DST Root CA X3가 서명한 인증서가 서빙되고 있었는데, 저희는 후술할 접속 불가 이슈 때문에 명시적으로 해당 체인을 사용하지 않도록 설정해 둔 상태였습니다.

그렇다면 왜 2년 전에 만료된 CA 인증서를 Trust Anchor으로 하는 인증 체인이 지금, 그것도 명시적으로 해당 체인을 사용하지 않도록 설정한 곳에 나타난 걸까요? 그 이야기를 하기 위해서는 전후 맥락을 알아보아야 합니다.

Let’s Encrypt의 Chain of Trust

2024년 2월 13일 현재, Let’s Encrypt의 Chain of Trust는 다음과 같습니다.

02 lets encrypt chain of trust
Ref: Let's Encrypt Chain of Trust

ECDSA key를 사용하는 인증서(파란색)들은 이 내용을 풀어나가는데 큰 관계가 없기 때문에, 여기에서는 RSA key를 사용하는 인증서(주황색)에 조금 더 주목해보겠습니다. 아래와 같이, 2가지의 Chain of Trust가 만들어지는 것을 알 수 있습니다.

  1. DST Root CA X3ISRG Root X1Let's Encrypt R3Client Certificate
  2. ISRG Root X1Let's Encrypt R3Client Certificate

특기할 만한 점은 바로, 앞서 만료되었다고 이야기했던 DST Root CA X3이 아직도 Chain에 들어 있다는 점입니다. 만료된 인증서라면 두 체인 모두에서 Trust Anchor는 ISRG Root X1 일 것인데, 왜 굳이 제거하지 않는 것일까요?

무시할 수 없는 과거

DST Root CA X3가 지금까지 체인에 포함되는지를 알기 위해서는 Let’s Encrypt의 역사를 조금 살펴보아야 합니다.

2014년 11월 대중에 Let’s Encrypt가 처음으로 공개되었지만, 이 시점의 Let’s Encrypt는 아직 Trusted CA가 아니었습니다. 때문에 당시 이미 널리 신뢰받고 있던 IdenTrust로부터 Cross-Sign을 받아 인증서를 발급하는 한편, 자신들의 CA를 만들어 Trusted CA에 등록하는 절차를 밟아나갔습니다.

그렇게 시간이 지나 2020년 말, IdenTrust로부터 서명받을 때 사용된 DST Root CA X3인증서가 만료되기까지 1년 정도가 남은 시점이 되었습니다. 이 당시 Let’s Encrypt의 Root CA 인증서, ISRG Root X1은 주요 소프트웨어에서 Trusted CA가 되어 있었습니다.

하지만 그게 모든 기기에서 신뢰한다는 의미는 아니었습니다. Trusted CA가 소프트웨어 업데이트로만 갱신되는 경우, 업데이트 자체가 아예 발생하지 않았을 경우에는 아직 ISRG Root X1이 신뢰받고 있지 않을 가능성이 있습니다.

그리고 안타깝게도 위와 같은 사례로 인해 영향을 받는 경우는 적지 않았습니다. 안드로이드 7.1 미만에서 해당 문제가 발생하는데, 2020년 12월 기준 영향을 받는 사용자는 전체 안드로이드 사용자의 2/3 수준이었습니다. 무시할 수 없는 정도의 기기에서 문제가 발생하는 것이었기에, Let’s Encrypt 커뮤니티에서는 이를 해결할 수 있는 창의적인 방법을 고안해 내었습니다.

Root CA 인증서와 Trust Anchor

우리는 흔히 인증 체인의 가장 위에 있는 것을 Root CA 인증서라고 부르고, 이것 자체가 인증을 해 준다고 오해합니다. 하지만 실제로는 조금 다릅니다.

실제로 인증 체인을 검증하는 데에는 Trust Anchor를 사용합니다. 이것은 일반적으로는 Root CA의 public key와 해당 Key의 Identity의 조합입니다. 우리가 Root CA의 인증서를 사용하는 것은, 이것이 Trust Anchor로서 필요한 모든 데이터를 다 가지고 있는 포맷이기 때문입니다. 그렇기에, 인증서 파일을 불러와서 Trust Anchor를 형성할 때 그 인증서의 만료일 검증을 하는 것은 구현체의 자율입니다. 인증서가 만료되었다는 것은 해당 Public Key와 Identity의 pair에 대한 서명의 유효기한이 다했다는 것이지, 우리가 필요로 하는 Public Key와 Identity가 더 이상 유효하지 않다는 의미는 아니니까요.

때문에 Trust Anchor으로 사용되는 CA 인증서의 만료일을 검증하는 구현체도 있고, 아닌 구현체도 있는데, 안드로이드의 경우에는 이를 검증하지 않습니다. 적어도 안드로이드에 한해서는, DST Root CA X3 라는 Trust Anchor는 업데이트 등으로 인증서 자체가 제거되지 않는 이상 영구히 유효한 것입니다.

이러한 창의적인 방법을 사용해서, 만료되는 DST Root CA X3ISRG Root X1에 서명하여 인증 체인을 구성한 것이 위 (1)번 체인입니다. 이 cross-sign은 DST Root CA X3이 만료되는 21년 9월부터 3년간, 즉 2024년 9월까지 유효하도록 설정되었습니다.

반대로 CA 인증서의 만료일을 검사하는 경우에는 동작이 조금 다릅니다. 이들의 경우에는 DST Root CA X3은 더 이상 유효한 Trust Anchor가 아니지만, 그 하위에 ISRG Root X1 이 있고 이는 Trust Anchor에 포함되어 있습니다. 때문에 일반적인 경우 (1)번 체인의 인증서를 내려보내도 (2)번 체인처럼 동작하여 정상적으로 신뢰 체인을 구성합니다. 일반적인 경우라면요.

문제는 일반적이지 않은 경우가 있다는 것입니다. 앞에서 접속 불가 이슈가 있었다는 이야기를 기억하시나요? 구버전 boringSSL의 경우 기본 설정상 (1)번 체인을 받았을 때 DST Root CA X3이 valid하지 않다면 무조건 신뢰 체인 구성에 실패합니다. 이 동작을 다른 구현체와 유사하게 설정하는 커밋이 있지만, boringSSL과 같은 라이브러리는 보통 직접 사용하는 경우가 드물죠. 저희같은 경우에는 사용중인 라이브러리에서 boringSSL 구버전을 사용하여 문제가 발생하였습니다. 다행히도, 이 케이스에서는 가장 상위 계층의 인증서가 DST Root CA X3에 의해 서명되지 않도록 하여 문제를 해결할 수 있습니다.

문제로 돌아와서

이러한 접속 불가 히스토리가 있었기 때문에, 저희는 cert-manager에서 인증서의 인증 체인을 명시적으로 (2)번 체인인preferredChain: ISRG Root X1으로 설정하여 사용하고 있었습니다.

그런데 지금 보이는 동작은 이 preferredChain 설정값이 무시되고 DST Root CA X3 을 포함하는 (1)번 체인을 사용하는 동작입니다.

테스트 결과, preferredChain 설정값에 따라 다음과 같이 동작이 달라지는 것을 확인하였습니다.

  • preferredChain이 아예 설정되지 않는다면 ISRG Root CA X1 인증서 체인을 사용
  • preferredChain: DST Root CA X3이라면 정상적으로 DST Root CA X3 인증서 체인을 사용
  • preferredChain: ISRG Root X1이라면 DST Root CA X3 인증서 체인을 사용

명시적으로 preferredChain: ISRG Root X1을 지정하는데도 DST Root CA X3 체인을 사용하는데, preferredChain 을 설정하지 않는다고 늘 ISRG Root CA X1체인을 반환할 것이라고 확신할 수 없다고 판단하였습니다. 그렇다면 왜 이런 문제가 발생하는지 찾아봐야겠지요.

안녕, DST Root CA X3

이 섹션의 내용에 대해 더 자세하게 알고 싶으시다면, Let’s Encrypt의 공식 블로그를 참고해 주세요.

앞서 이야기했던 Cross-sign의 유효기한은 2024년 9월 30일까지입니다. 원래 2021년 9월까지 서명했던 것을 하위 호환성 문제로 인해 3년 연장했던 것이고, 이제는 문제가 되는 기기의 점유율이 많이 줄어 (안드로이드 점유율 중 6% 수준) 더 이상 이 cross sign의 필요성이 크지 않습니다.

cross sign 인증서를 매번 통신을 수립할 때마다 서버에서 클라이언트에 내려주는 것도 불필요한 오버헤드이기 때문에, 더 이상 cross-sign을 하지 않고 ISRG Root X1을 사용하는 것이 목표인데, 그렇다고 한순간에 이걸 발급하지 않아버리면 혼란이 올 가능성이 높겠죠.

그래서 Let’s Encrypt에서는 단계적인 DST Root CA X3 deprecate 계획을 수립해 두었는데, 그 첫 번째 페이즈가 지난 2월 8일 적용되었습니다. 구체적으로, 기존에는 DST Root CA X3이 Default chain이었고, ISRG Root X1이 Alternate Chain 인증서로 제공되었는데, 이를 2월 8일부터 ISRG Root X1을 Default Chain으로 변경하고 DST Root CA X3 를 Alternate으로 제공하도록 변경되었습니다.

시기적으로 이 변경사항이 문제를 발생시킨 원인으로 가장 의심됩니다. 하지만 Default Chain과 Alternate Chain이 무엇이길래 지금 겪는 문제를 발생시키는지 조금 더 깊이 확인해 볼 필요가 있습니다.

Automated Certificate Management Environment

Default Chain과 Alternate Chain이 어떻게 내려오는지 알려면 인증서 발급 과정이 실제로 어떻게 이루어지는지를 보아야 합니다. Let’s Encrypt와 같은 자동 인증서 발급 서비스는 ACME(Automated Certificate Management Environment) API를 사용하는데요, 이는 RFC 8555로 정의되어 있습니다.

해당 문서 7.4.2번 섹션을 보면, ACME 서버에서는 동일한 end-entity 인증서(=Let’s Encrypt에서의 Subscriber Certificate)를 포함하여 서로 다른 인증 체인을 사용하는 alternate 인증서를 지정할 수 있다고 되어 있습니다. 이 중 어떤 인증서를 사용할 것인지는 각 클라이언트의 재량입니다.

실제로 해당 방식으로 통신이 이루어지는지 확인하기 위해, Let’s Encrypt 커뮤니티에서 가장 폭넓게 사용되는 Certbot을 사용하여 새 인증서를 발급하여 봅니다.

Certbot은 기본적으로 디버그 로그를 남기는데, 이 안에서 ACME 서버와 통신하는 HTTP request와 response를 볼 수 있습니다.

로그를 확인하면 다음과 같습니다.

ISRG Root X1을 최상위 인증서로 사용하는 체인
ISRG Root X1을 최상위 인증서로 사용하는 체인

ISRG Root X1을 최상위 인증서의 Issuer Common Name으로 하는 인증서 체인이 하나 내려오고, Link 헤더를 통해 alternate 인증서가 내려오는 것을 확인할 수 있습니다

이어지는 로그에서 Alternate 인증서를 불러오는 것을 확인할 수 있습니다.

DST Root CA X3을 최상위 인증서로 사용하는 체인
DST Root CA X3을 최상위 인증서로 사용하는 체인

다음과 같이 최상위 인증서의 Issuer Common NameDST Root CA X3인 인증서 체인이 내려옵니다.

여기까지의 분석을 통해, Let’s Encrypt에서는 의도한 대로 인증서를 발급하고 있음을 확인할 수 있었습니다. 그렇다면 이제는 이 인증서를 받아와 실제로 secret을 만들어주는 클라이언트를 살펴볼 때입니다.

구현별로 서로 다른 휴리스틱

ACME 클라이언트는 Certbot이나 cert-manager 이외에도 acme.sh 등이 존재하고, traefik이나 caddy와 같은 HTTP reverse proxy 소프트웨어에서 자체적으로 제공하고 있기도 합니다.

여기에서는 certbot과 지금 인프라셀에서 사용중인 cert-manager를 서로 비교하여 보겠습니다.

Certbot에서 Preferred Chain을 고르는 로직은 다음과 같습니다.

05 preferred chain logic in certbot
Ref: Github Certbot Repository

각 인증서 체인에서, 가장 마지막 (인증 체인에서 root와 가장 가까운) 인증서를 골라 해당 인증서 체인에서 Issuer의 Common Name을 추출, 이를 입력받은 값과 비교하는 것을 확인할 수 있습니다.

반면 cert-manager는 구현이 조금 다른데요,

06 preferred chain logic in cert manager
Ref: Github cert-manager Repository

cert-manager의 경우 default chain을 제외한 alt chain 중에서, 각 체인의 모든 인증서를 순회하며 Issuer CN을 추출, 이를 입력받은 값과 비교하는 것을 확인할 수 있습니다.

로직을 분석함으로서 두 가지 디자인 선택이 cert-manager에서 잘못된 인증서 체인을 선택하도록 하는 문제를 만들었다는 것을 알 수 있었습니다.

우선 Alternate Chain을 선택할 때 Default Chain은 탐색 대상에서 제외한다입니다. 2월 8일 이전에는 Default Chain이 DST Root CA X3이었기 때문에 preferredChainISRG Root X1인 경우 정상적으로 의도한 인증서 체인이 발급되었습니다. 하지만 지금은 Default Chain이 ISRG Root X1이기에, 이 체인의 인증서가 발급되기를 기대하였으나 preferredChain을 지정할 경우 이 체인은 아예 탐색 대상이 되지 않습니다.

다음으로, 모든 중간 인증서의 Issuer CN을 Alternate Chain 탐색 대상으로 삼는다 입니다. 지금 Alternate Chain인 DST Root CA X3 chain에도 ISRG Root X1을 Issuer으로 가지는 인증서가 존재합니다. 때문에 preferredChainISRG Root X1으로 설정한 경우 해당 인증서가 조건을 충족시켜 DST Root CA X3 체인의 인증서를 반환하게 됩니다.

결론적으로,

  1. Let’s Encrypt에서 하위 호환을 위해 제공하던 인증 체인을 deprecate하였고
  2. 해당 변경사항이 cert-manager에서 버그가 발생할 수 있는 조건을 트리거하였고
  3. 해당 버그가 발생해도 일반적으로는 문제가 없지만, 하필이면 Trust Anchor 인증서의 유효기간을 검증하고 무조건 topmost 인증서를 Trust Anchor을 통해 인증하려고 하는 구현체를 사용하였기에

발견하게 된 버그라고 할 수 있겠습니다.

첫 오픈소스 기여

보통 제가 문제를 찾았을 때 즈음에는 누군가 이슈를 만들어 두던데, 이번 케이스에서는 아니었습니다. 문제를 확인했을 당시 Let’s Encrypt 커뮤니티 포럼이나 cert-manager 레포 중 어느 곳에도 리포팅이 되지 않은 상태였는데, 아마도 굉장히 특정한 상황에만 서비스 장애로 이어지는 문제였기에 영향을 받은 사람이 그리 많지 않았던 것으로 추정중입니다.

문제 리포팅을 하려고 보니, 이미 코드 분석도 완료한 시점에서 그리 어려운 변경사항이 아니리라고 판단되었습니다. 그렇다면 굳이 이슈 만들어두고 수정될 때까지 기다리는 것보다, 내가 PR을 올리는 게 낫지 않을까? 하는 생각에, 바로 PR을 올려 보았습니다.

07 pr to cert manager
개인 계정이어서 프로필을 살짝 가렸습니다. 원본이 궁금하시다면 여기를 참고해 주세요.

리뷰를 기다리며 저는 제가 코드 컨벤션 같은 것을 잘 지켰는지 등을 걱정했는데요, 이는 기우로 끝났습니다.

오히려 cert-manager의 메인테이너 분들은 사용자들이 기존 buggy한 구현에 의존하고 있을 가능성과 그로 인한 호환성 문제를 더 주의깊게 고려하시더라고요. 저는 생각지도 못한 부분이었고, 이런 고민을 한다는 것이 굉장히 인상깊었습니다.

여담으로, 이 PR을 올리고 하루 뒤 동일한 이슈가 cert-manager 레포에 리포팅되고, 5일정도 지나서 Let’s Encrypt 커뮤니티에도 사례가 공유되었습니다. 저희가 문제를 좀 더 빠르게 경험한 것은 맞는 것 같습니다.

Wrap-Up

  • cert-manager 사용 중, 설정값과 실제 발급된 인증서의 인증 체인이 다른 문제를 발견하여 분석하여 보았습니다.
  • 분석 결과 cert-manager에 존재하였으나 지금까지는 발현되지 않은 버그가 최근 Let’s Encrypt의 Default Certificate Chain 변경에 의해 발현되는 것을 확인하였고, 이를 ACME 스펙, 실제 통신 내용, 그리고 코드 확인으로 검증하였습니다.
  • 분석한 결과를 바탕으로, cert-manager에 PR을 제출하여 머지되었습니다.

커뮤니티 리포팅된 사례 중 하나는 해당 버그로 장애가 발생했다고도 적혀 있었는데, 저희는 운 좋게 개발 환경에서 이슈를 발견하여 서비스 영향이 없었다는 점이 정말 다행이었습니다.

문제를 조사하면서 평소에 개인적으로도, 업무적으로도 많이 사용하는 Let’s Encrypt가 어떻게 동작하는지 조금 더 깊이있게 볼 수 있었던 것이 특히 기억에 남습니다.

긴 글 읽어주셔서 감사합니다.

데브시스터즈는 최고의 인재를 찾고 있습니다.

데브시스터즈에서는 능력있는 SRE/DevOps Engineer를 찾고 있습니다.
자세한 내용은 채용 사이트를 확인해주세요!
DevOpsSREInfra

© 2024 Devsisters Corp. All Rights Reserved.