HTTPS 사이트에 더 이상 구식 인증서를 사용하지 않는 이유
(rachelbythebay.com)- 작성자는 ACME 프로토콜의 복잡성과 구현 위험성 때문에 수년간 거부감을 가졌음
- 기존 ACME 클라이언트들은 보안상 위험하거나 난해한 코드가 많아 직접 실행하기 꺼렸음
- 하지만 도메인 등록업체인 Gandi의 품질 저하 및 가격 상승으로 직접 인증서 갱신 도구를 구현하게 됨
- 수많은 시행착오 끝에 Let's Encrypt를 통해 직접 인증서를 발급받는 도구를 성공적으로 완성함
- 글 후반부에서는 ACME 프로토콜의 실제 동작과정 및 JSON, base64, 서명 등의 저수준 구현 디테일을 자세히 설명
Why I no longer have an old-school cert on my https site
배경과 계기
- 2023년 초에는 구식 인증서를 계속 유지하는 이유를 설명했지만, 2025년 현재 이제는 그 방식을 버리게 된 이유를 공유
- ACME 프로토콜에 대한 거부감은 2018년부터 있었으며, 복잡한 웹 기술들과 난해한 인코딩 방식이 큰 장벽이었음
- 대부분의 ACME 클라이언트가 신뢰하기 어려운 코드였고, 루트 권한으로 작동시키기에는 위험하다고 판단했음
- Gandi가 사모펀드에 인수된 이후 품질이 하락하고 가격이 올라, 더는 기존 인증서를 유지할 이유가 사라졌음
자체 구현의 시작
- 기존 도구를 사용하지 않고, 직접 하나씩 작은 유틸리티 함수부터 구현해 나감
- jansson이라는 C용 JSON 라이브러리를 C++에서 사용할 수 있도록 감싸는 작업부터 시작함
- JWK(Key 구조체) 생성을 위한 여러 라이브러리를 검토했으나 대부분 도움이 되지 않았고, 스스로 구현하기로 결정함
- 중간에 몇 번이나 멈추고 다시 시작하는 과정을 반복하며, 점진적으로 작은 구성 요소들을 연결해 감
테스트 환경과 실제 적용
-
Let's Encrypt의 실제 서버를 직접 건드리지 않기 위해, "pebble"이라는 테스트용 ACME 서버를 독립된 환경에서 사용함
-
수많은 실패 끝에, CSR을 입력받아 인증서를 발급하는 초기형 도구를 완성했고,
- Let's Encrypt staging 서버에서 테스트 성공
- 프로덕션 환경에서도 성공
- 실제 웹사이트에도 적용 완료
ACME 프로토콜 상세 설명
- RSA 키를 생성하고 CSR(Certificate Signing Request)을 만들어 CN 및 SAN 포함
- ACME 디렉터리 URL에서 JSON 파싱해 newNonce, newAccount, newOrder 등 엔드포인트 추출
- 개인 키에서 modulus와 public exponent 추출, 이를 웹에 맞는 base64url 인코딩으로 변환
- JWK 생성 후, JSON payload와 함께 RSA SHA256 서명
- HTTP HEAD 요청으로 Nonce 받아온 뒤, 서명된 요청을 POST로 보내 계정 생성
- 응답의
Location
헤더는 실제 리디렉션이 아니라 계정 식별자 URL로 사용됨
ACME 프로토콜의 복잡성
- 단순한 인증서 발급임에도 불구하고,
- SHA256 해시, base64web, JSON 내 JSON 구조, RSA 서명
- HEAD 요청, Location 헤더로 계정 식별, 일회용 Nonce 필요 등
- 아직 인증서 주문, 도메인 소유권 증명(TXT 레코드 등), 인증 완료 등은 다루지도 못한 수준이라고 언급함
- 일부 클라이언트는 publicExponent 인코딩을 잘못 구현해도 동작하는 사례도 있어, 표준의 느슨함도 지적함
결론
- ACME는 너무나도 복잡하고, 직접 구현하는 데 엄청난 시행착오와 노력이 요구되는 시스템임
- 그럼에도 불구하고 구식 인증서를 버리고 완전한 자동화 방식으로의 전환에 성공했음을 공유함
- 이 복잡함이 혹시 누군가의 일자리를 보장하기 위한 구조가 아닐까 하는 농담도 덧붙임
Hacker News 의견
-
나는 Let’s Encrypt SRE/infra 팀의 테크니컬 리드로, 이런 문제들에 대해 많은 고민을 하는 입장임
JSON Web Signature는 정말 까다로운 포맷이고, ACME API도 RESTful함에 매우 진심인 편임
내가 직접 설계했다면 이렇게 만들진 않았을 것임
이런 구조가 만들어진 배경에는 IETF가 IETF 표준을 많이 활용하려던 의도와 위원회식 디자인이 한몫했다고 생각함
JSON, JWS, HTTP용 라이브러리 몇 개만 있어도 훨씬 나아지긴 하지만, 특히 C에서는 그 라이브러리들조차 사용하기 쉽지 않다는 점이 문제임
RFC 언어 자체가 복잡하고 다른 문서를 참조하는 경우도 많아서, 이를 돕기 위한 인터랙티브 클라이언트와 문서를 별도로 작업하고 있음-
JSON Web Signature가 까다로운 포맷이라는 말이 이해가 잘 가지 않음
나는 ASN.1, Kerberos, PKI 등 복잡한 것들을 많이 다루는 입장에서, JWS가 그리 어려운 포맷이라고 생각하지 않음
심지어 직접 코드로 작성한다 해도 S/MIME, CMS, Kerberos 등보다 훨씬 쉽다고 봄
JWS가 어디서 ‘까다로운지’ 더 설명이 필요함
JWT의 문제라면, HTTP 유저 에이전트가 표준적으로 JWT를 어떻게 받거나 요청해야 하는지 잘 정해져 있지 않다는 점이 더 핵심이라고 생각함 -
어떤 사람이 “인증서를 3개 이상 발급받으려면 돈을 내야 한다”는 말을 보았는데, 내가 지난 5년간 써 오면서 청구서를 받은 적도 없고, 이건 오해나 잘못된 정보인 것 같음
-
-
“e=65537” 대신 “e=AQAB”로 처리하는 부분에 대해 이야기하면서, JSON이 숫자를 제대로 다루지 못하는 특성이 원인임을 설명함
만약 매우 큰 숫자인 4723476276172647362476274672164762476438 같은 값을 JSON 파서에 넘길 때, 대부분의 JSON 파서는 조용히 64비트 정수나 float로 잘라버리거나, 운이 좋으면 에러를 내기도 함
Common Lisp 같은 언어라면 잘 처리하겠지만, 실제로는 그런 환경에서 개발하는 사람은 많지 않음
그래서 JSON에 대형 숫자를 안정적으로 전달하려면 base64로 바이트 배열로 변환하는 게 차라리 낫다는 생각임
아무 문제 없이 넘어가는 것처럼 보여도 이것이 다양한 보안 이슈의 근원이 되므로, 프로토콜의 모든 숫자를 이렇게 다루는 것도 타당하다고 봄
다만 이 방식은 JSON의 인간 친화적인 가독성이 사라진다는 단점이 있고, 개인적으로 표준화된 S-Expression이 훨씬 나은 선택이라고 생각함
하지만 세상은 JSON을 택함-
왜 세상이 JSON을 선택했는지 이해하지 못한다면 일부러 무시하는 것이라고 생각함
JSON은 대부분의 데이터에 대해 손쉽게 사람이 직접 작성/편집/읽을 수 있음
반면 Canonical S-Expression은 원소마다 길이 정보를 앞에 붙여야 해서 수작업이 너무 번거로움
S-Expression을 작성하려면 일일이 문자를 세고 접두어도 수정해야 하니 매우 귀찮음
예상과 달리, 이런 손쉬운 수작업 및 수정성이 JSON이 살아남은 이유라고 봄
참고로 루비 JSON 파서는 큰 숫자도 잘 처리함 -
나는 C# 앱에서 JSON serializer가 BigInt를 숫자로 내보냈고, JS에서 그것을 받아서 조용히 잘못 해석하는 버그에 시달린 적이 있음
에러 대신 overflow가 표준 동작인 것을 보면 아직도 놀람
이후로 32비트보다 큰 숫자는 반드시 문자열로 처리하는 습관을 들임 -
{"e":"AQAB"}와 {"e":65537} 비교에는 일리가 있지만, {"e":"65537"}과 비교하면 이 또한 모든 JSON 파서의 처리 결과는 같음
숫자든 문자열이든 명확하게 변환됨
물론 결과가 double로 안 들어갈 만큼 큰 수면 그 자체로 언어나 파서 이슈는 있지만, 표현 방식과는 별개라고 생각함 -
JSON의 문제는 포맷 자체가 아니라, 파서가 원래 JS 타입 매핑을 위해 만들어졌다는 데 있다고 생각함
일부 파서는 잘 처리할 수 있어도, 그렇게 하면 JSON의 이식성이 사라짐
Base64로 변환해도 같은 문제가 생김 (표준과 다르므로)
replacer와 reviver로 커스텀 파싱은 가능하나, 모든 환경에서 이 기능이 보장되는 건 아님
결국 표준 파서로 JSON을 해석한다는 전제가 오류의 근원임
만약 JSON이 아니라 다른 포맷이라고 부른다면 이런 문제가 줄겠지만, 사람들은 여전히 JSON처럼 생기면 그대로 파서에 넣으려 할 것임 -
Go 언어는 json.Number 타입을 통해 손실 없이 숫자를 문자열로 디코딩할 수 있음
거의 내 ‘최애’ 임의 소수(decimal) 타입 중 하나를 소개함 https://github.com/ncruces/decimal/…
반쯤 농담으로, 이 경우 S-Expression이 더 나은 이유를 잘 모르겠음
LISP 중에서도 임의 정밀도 산술 지원 안 하는 경우도 있음
-
-
ACME와 여러 클라이언트에 대해 저자가 비판적 태도를 보인 이유가 의아함
이건 단순히 사용 능력 문제는 아닌 것 같아서, ACME라는 개념 자체나 그 주변 도구 전반에 대한 반감이 있는 듯 추정했음
우리도 2019년부터 LE 기반으로 몇 개 사이트에 적용했었고, 그간 여러 ACME 클라이언트들을 써 봤음
예를 들면 Crypt-LE는 우리 용도에 괜찮았고, Sectigo ACME와 연동하려다 le64로는 부족해서 certbot, lego, posh-acme 등 다양한 것을 써 봄
최종적으로 certbot에 GHA 환경 이슈를 고쳐서 썼고, posh-acme도 좋았음
다시 읽어보니 저자의 날카로운 톤은 ACME나 클라이언트가 아니라 스펙 자체에 대한 것이었음
ACME라는 아이디어는 좋으나, 구현 및 실제 적용은 실망이라는 결론임-
저자와 비슷한 관점이라고 생각함
‘많은 기존 클라이언트들이 위험한 코드고, 내 서버에서 루트 권한을 갖고 실행시키기엔 신뢰할 수 없다’라는 저자의 말을 인용함
보안 민감한 작업에서는 이런 신중한 태도가 타당하다고 생각함 -
원글의 어투를 이해하기 어려웠던 사람에게 맥락을 줄 수 있는 예전 포스팅 링크를 소개함
- “Why I still have an old-school cert on my https site” (2023.01.03) https://rachelbythebay.com/w/2023/01/03/ssl/
- “Another look at the steps for issuing a cert” (2023.01.04) https://rachelbythebay.com/w/2023/01/04/cert/
-
서버에서 이해할 수 없는 무언가를 돌리는 것 자체를 싫어하는 사람도 많고, 나도 그 생각에 공감함
하지만 보안 분야는 고양이와 쥐의 게임이라 계속 변화할 수밖에 없는 성질이고, 결국 따라갈 수밖에 없음
다행히도 ACME는 내 마음대로 클라이언트를 만들 수 있는 자유가 있음
certbot을 반드시 쓸 필요도 없고, TPM처럼 내 자원을 차단하는 구조도 아님
-
-
ACME 클라이언트를 처음부터 구현하려는 경우, RFC들(그리고 관련 JOSE 등 문서)을 직접 읽는 것이 생각보다 쉽다고 경험을 공유함
직접 구현도 해보고, ACME v2 흐름을 이해하는 정리글도 작성해서 공유함 https://www.arnavion.dev/blog/2019-06-01-how-does-acme-v2-work/
공식 RFC를 대체하진 않지만, 이 정리글을 순서도와 방식별 인덱스처럼 참고하면 좋음-
MIT 보안 수업 최종 과제로 ACME 클라이언트 구현을 직접 하기도 함 https://css.csail.mit.edu/6.858/2023/labs/lab5.html
-
굳이 매뉴얼을 일일이 읽지 말고, 영어로 모든 과정을 풀어 설명하는 글을 Hacker News에 올리는 게 더 많은 인터넷 포인트를 얻는 묘한 현실을 풍자함
-
-
웹 인프라 프로토콜 복잡성이 계속 높아지는 점을 지적한 저자가 고맙다는 이야기를 함
이런 표준은 그냥 툴이나 클라이언트를 써야 하는 개발자에게도 부담이 아니지만, ‘규제 장벽’처럼 작용해서 결국 기존 대형 기업만이 인터넷 운영 요건을 맞출 수 있게 만드는 구조화라는 생각임
ACME 하나만으론 넘기 힘든 진입장벽이진 않지만, 결국 누적되어 하나의 벽이 됨- 이런 프로토콜들은 다 오픈소스 구현체가 있고, AI의 발전 덕분에 이런 장벽도 점점 낮아질 것이라는 낙관론을 피력함
-
OpenBSD에는 베이스 OS에 포함되어 있는 매우 간단하고 가벼운 ACME 클라이언트가 있음
기존 대안들이 너무 무겁고 Unix 철학에 어긋나 있기 때문에 새로 만들었다고 들음
저자가 이쪽을 고민하지 않은 것 같아 아쉬움
아마도 타 OS에도 조금만 노력하면 포팅할 수 있을 것임-
이 OpenBSD 클라이언트는 오히려 OpenBSD 철학이 보안이 왜 이렇게 복잡한지를 이해하지 못한 사례라고 생각함
이 클라이언트는 해당 머신에만 설치해서 사용하고, 분리 구조를 통해 각 요소가 서로 영향을 주지 않게 만들어졌음
하지만 ACME 프로토콜 자체는 완전한 분리 구조(air-gapping)가 가능해서, 웹서버와 인증서 요청기, DNS 서버가 서로 다른 환경이어도 무방함
OpenBSD 통합 클라이언트를 안 쓸 경우, 더 복잡할지 몰라도 보안 설계 원칙상 이쪽이 더 우수하다고 봄
‘OpenBSD만 설치하면 끝’은 단지 손쉬운 방법일 뿐임 -
uacme (https://github.com/ndilieto/uacme)도 소개함
가벼운 C 코드로, LE 파이썬 클라이언트에서 배터리 문제로 계속 고생한 후 대체재로 안정적으로 사용함 -
OpenBSD ACME 클라이언트를 직접 쓰고 있는데, 아주 잘 동작한다는 경험 공유
-
-
“4096비트 RSA 개인키를 만들라”는 권장은 오히려 방문자 속도 저하 문제만 만들고, 실질 보안은 2048비트 수준임
2048비트 리프(leaf) 인증서를 쓰는 게 더 낫다고 강조함-
4096 비트면 패시브 캡처/미래 복호화에 더 강하지 않냐는 질문을 던짐
중간 인증서 보안도 비동기 공격에 영향을 주는지 궁금함 -
웹호스트가 RSA 키만 지원해서 일부러 4096비트 RSA를 써서 빨리 EC키를 지원하라고 유도함
-
-
이런 작업을 직접 해보면 실력이 늘긴 하지만, 저자 글의 어조는 프로토콜이나 Let’s Encrypt 구축 과정에 대해 짜증을 내는 것처럼 보임
lightweight ACME 라이브러리(https://github.com/jmccl/acme-lw 등)로도 충분히 자동화할 수 있는데, 왜 이렇게까지 힘들게 하나 궁금함- SSL은 정말 ‘핫하고 고착된 혼돈의 집합’이라고 단언함
플랫/비트필드 문제는 모두 ASN.1/X.509의 역사적 유산 때문인데, 수학적 복잡성이 심각하고, 모든 라이브러리와 SW가 80년대 기술 한계에 묶여있음
LetsEncrypt 도입 때나 HTTP/2 등장 때 이 혼돈을 정리할 마지막 기회가 있었지만, 현실적으로 ACME CA는 쉘 스크립트·OpenSSL·술로 구성만 해도 되고, 기존 SW와의 호환도 문제라 도약 못 했음
- SSL은 정말 ‘핫하고 고착된 혼돈의 집합’이라고 단언함
-
점점 HTTPS로의 전환 압박이 커지는 경험을 나눔
예를 들어 WhatsApp에서 HTTP 링크는 이제 열 수 없게 됨-
프록시와 캐싱을 쓰면 트래픽 부담을 줄일 수 있고, 작은 서버엔 좋은 방법임을 제안함
-
ACME가 아무리 복잡하더라도 TLS 미지원보다는 훨씬 낫다는 점을 강조함
-
-
“RSA 키, SHA256 다이제스트, RSA 서명, 실제로는 base64가 아닌 base64, 문자열 연결, JSON 내부의 JSON, Location 헤더를 301 리다이렉션 대신 식별자로 사용, 단일 헤더 값을 위해 HEAD 요청, 모든 요청을 위해 nonce 용도의 별도 요청 필요 등 요소가 겹침”
“아직도 인증서 오더 생성, 권한·챌린지 처리, 키 썸프린트, TXT 레코드 구성 등 더 복잡한 단계가 남아있음”
정말 믿기 힘들 정도의 복잡성이고, 정리 내용을 공유해줘서 고맙다는 응원의 메시지를 전함