Let's Encrypt: "unable to get issuer certificate" (오류 2), 강제 재발급 및 속도 제한

Discourse 설치 또는 재구축 시 유효한 Let’s Encrypt 인증서가 다음과 같은 오류로 거부될 수 있습니다:

C=US, O=Let's Encrypt, CN=YR2
error 2 at 1 depth lookup: unable to get issuer certificate
error fullchain.cer: verification failed

저는 RSA 발급이 성공한 직후 이를 즉시 재현했습니다. 실패한 확인 작업은 불필요한 --force 요청을 트리거하여 HTTP 429 / rateLimited / “too many certificates” 오류로 이어질 수 있습니다. 제 설치 환경에서는 이후 ECDSA 발급이 차단되어 HTTPS가 시작되지 않았습니다.

영향을 받는 체인에서, 인증서가 여전히 유효한 경우에도 각 재구축 또는 컨테이너 재시작 시 RSA 및 ECDSA 인증서의 불필요한 강제 재발급이 트리거될 수 있습니다. 성공적인 재발급은 Let’s Encrypt 할당량을 소모하므로, 반복적인 재구축 또는 재시작은 속도 제한에 빠르게 도달할 수 있습니다.

이미 설치된 인증서로 HTTPS가 계속 작동하는 동안에는 이를 인지하지 못할 수 있습니다. 동일한 호스트네임으로 인스턴스를 삭제하고 다시 설치해도 해당 할당량이 초기화되지 않습니다. 새 설치에 필요한 인증서가 누락된 경우, 속도 제한된 발급으로 인해 HTTPS 시작이 방지될 수 있습니다.

수정 방법

templates/web.letsencrypt.ssl.template.yml 파일의 cert_exists() 함수 내에서 다음을 교체하십시오:

- openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
+ openssl verify -untrusted ca.cer fullchain.cer

함수의 나머지 부분은 변경하지 마십시오. 수정된 템플릿은 /usr/local/bin/letsencrypt를 업데이트하기 위해 다음 컨테이너 재구축에 포함되어야 합니다.

이미 속도 제한에 걸린 경우, 패치는 할당량을 초기화하지 않습니다. 수정 사항을 적용하고 추가 발급을 시도하기 전에 retry after 시간을 준수하십시오.

PR: FIX: preserve the Let's Encrypt issuer chain in cert_exists - Pull Request #1136 - discourse/discourse_docker - GitHub

체인 검증 문제 확인

컨테이너 내부의 영향받은 인증서 디렉터리(/shared/letsencrypt/<hostname> 또는 <hostname>_ecc)에서 다음을 비교하십시오:

openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
openssl verify -untrusted ca.cer fullchain.cer

재현된 경우, 첫 번째 명령은 오류 2로 실패하고 두 번째 명령은 다음을 반환합니다:

fullchain.cer: OK

원인 및 검증

openssl x509 -in ca.cer는 발급자 번들에서 첫 번째 인증서만 읽습니다. 관찰된 YR2 체인의 경우, ISRG Root X1에 도달하는 데 필요한 크로스 서명된 Root YR이 누락됩니다.

-untrusted ca.cer는 신뢰 앵커를 시스템 저장소에 유지하면서 전체 발급자 번들을 제공하여 2021년 수정 사항(#576)의 의도를 보존합니다.

저는 컨테이너 내부에서 실제 RSA 및 ECDSA 체인을 검증했습니다. 패치를 적용한 전체 재구축 시 기존 인증서 두 개가 모두 재사용되었습니다: 두 개의 Skip 메시지가 표시되고, 두 체인이 모두 설치되었으며, nginx -t가 통과했습니다.

관련 보고서

411097
413078

PR: FIX: preserve the Let's Encrypt issuer chain in cert_exists - Pull Request #1136 - discourse/discourse_docker - GitHub

6개의 좋아요

평가할 수 있도록 올바른 인증서와 체인을 여기에 올려 주세요.

3개의 좋아요

이 문제를 조사해 주셔서 감사합니다 @supermathie. 영향을 받은 인스턴스에서 fullchain.cer와 ca.cer 파일을 첨부했습니다.
fullchain.cer.txt (5.9 KB)
ca.cer.txt (3.8 KB)

아래 OpenSSL 출력 내용을 보면, 원래 검증 명령은 오류 2로 실패하고, 동일한 인증서와 체인을 사용하여 -untrusted 옵션을 추가했을 때 성공하는 것을 확인할 수 있습니다.

openssl version
openssl verify -CAfile <(openssl x509 -in ca.cer) fullchain.cer
openssl verify -show_chain -untrusted ca.cer fullchain.cer

OpenSSL 3.5.7 9 Jun 2026 (Library: OpenSSL 3.5.7 9 Jun 2026)
C=US, O=Let's Encrypt, CN=YR2
error 2 at 1 depth lookup: unable to get issuer certificate
error fullchain.cer: verification failed
fullchain.cer: OK
Chain:
depth=0: CN=hubprod.surikai.fr (untrusted)
depth=1: C=US, O=Let's Encrypt, CN=YR2 (untrusted)
depth=2: C=US, O=ISRG, CN=Root YR (untrusted)
depth=3: C=US, O=Internet Security Research Group, CN=ISRG Root X1

이 문제는 두 개의 서로 다른 환경에서 발생했습니다. PR을 열었을 때부터 개발 인스턴스에서 수정 사항을 적용하여 실행해 왔으며, 예상대로 정상적으로 작동하고 있습니다.

2개의 좋아요

참고로 ACME 클라이언트는 ${LETSENCRYPT_DIR}에 다음과 같은 관련 파일을 생성합니다:

  • ${DISCOURSE_HOSTNAME}.cer: 발급된 인증서를 포함합니다.
  • ca.cer: 인증서 체인을 포함하며, 다소 혼동하기 쉬운 이름입니다.
  • fullchain.cer: 위의 두 파일을 결합한 것입니다.

가장 정확한 명령어는 다음과 같습니다:

openssl verify -untrusted ca.cer ${DISCOURSE_HOSTNAME}.cer

현재 명령어는 엉망입니다(시스템 루트로 체인이 유효한지 실제로 테스트하지 않으므로 정확하지도 않고), 다른 방식들은 부수 효과나 가정에 의존합니다. PR을 업데이트하겠습니다.

2개의 좋아요

상당히 일리 있는 말씀입니다. fullchain.cer에서 OpenSSL이 첫 번째 인증서만 읽는 데 의존하는 것보다 리프 인증서를 직접 검증하는 방식이 더 깔끔하네요.

저희 PR #1136은 폐기하고, 작성하신 PR을 채택하겠습니다.

필요하다면 새로운 PR을 테스트해 드릴 수 있습니다.

1개의 좋아요

병합 완료 :partying_face:

문제를 명확히 파악하고 수정을 제안해 준 @jmx에게 감사드립니다. 좋은 제안이었어요. 다만 제가 조금 더 작업하고 싶었거든요.

3개의 좋아요

이 주제는 14시간 후 자동으로 닫혔습니다. 더 이상 답변을 남길 수 없습니다.