Let's Encrypt 와 ACME을 이용한 TLS 인증서 자동발급

김성현·2021년 12월 16일
1
post-thumbnail

오늘도 여기저기 레딧이나 기술 블로그를 돌아다니다가 Let's Encrypt라는 걸 발견했다.
내용을 들어보니 무료 CA 라고 한다.

처음에는 무료라길래 대체 뭔가 했는데 위 링크를 눌러보면 알겠지만 하나같이 참여하는 면면이 무시무시하다.

Chrome(그러니까 구글), Meta(그러니까 페이스북), Cisco(그 OSI 7계층), AWS(그 아마존), 깃허브도 있으니 마소까지도 참여한다 봐야겠지, 그러니 공룡 IT 기업은 죄다 참여하는 것으로 보인다. 빅테크중 안보이는데는 Apple 밖에 없는 것 같다.

이정도면 지구방위대라 칭할 만 하니 일단 이거저거 알아보기 시작했다. 이 기업들 목록 보는 순간 이게 뭔지는 중요하지도 않았다. 뭔진 몰라도 저녀석은 확실히 알아봐야 한다는 확신이 드는 순간이였다.

그럼 이게 대체 뭐하는 녀석인가?

우선 하나만 확실히 하자면 Let's Encrypt 자체는 자기 스스로도 말하기를 그저 평범한 CA에 불과하다. 하지만 이녀석의 진가는 certbot, 혹은 acme를 이용한 인증서 발급 자동화에 있다.

certbot과 ACME

우선 ACME는 프로토콜(IETF, RFC8555)이다. 해당 글에 따르면 ACME(Automatic Certificate Management Environment)PKIX(그러니까, X509)를 사용하는 CA와 사용자 사이의 인증 절차를 자동화하기 위한 프로토콜 이라고 한다.
이것도 본문의 요약을 줄인건데 좀 복잡한 것 같다.
그냥 짧게 말하면 자동으로 x509인증서를 발급받는 프로토콜이다.

certbot은 소개에 따르면 전체 인터넷 암호화를 위해 노력하는 EFF의 활동 산물로 ACME프로토콜을 이용해 인증서 발급을 자동화하는 cli 클라이언트 라고 한다.
이거도 길어서 한마디로 줄여보겠다. ACME를 이용해 인증서를 받는 cli 앱이다.

CA 에서 x509 형식의 인증서를 자동으로 발급받기 위해 ACME라는 프로토콜을 정의하고,
이 프로토콜을 이용하는 certbot 이라는 cli 앱을 만들었다.

그러면 Let's Encrypt에는 어떤 장점이 있을까?

우리가 만약 TLS를 사용하려면 인증서가 있어야 한다. 이때 인증서는 일정 주기마다 돈을 내고 인증서를 다시 발급받아야 하는데 이걸 해본 사람은 알겠지만 귀찮고 돈이 어딘가로 새나가는 것 같기도 하고... 여하튼 뭔가 불편하다.
실제 사용 가능한 인증서는 돈도 들고 귀찮기에 우리는 학습 목적일 때에는 openssl을 이용해 자가 서명 인증서를 만든다.
하지만 이런 인증서는 안전하지 않은 인증서이다. 인증서는 일종의 트리 구조를 취하는데 가장 상위 인증서는 Root CA 라고 전 세계에서 검증받은 무척 대단한 단체만 공식적으로 사용할 수 있다. 당연히 우리가 만든 최상위 인증서는 전 세계에서 검증받은 인증서가 아니므로 안전하지 않은 인증서이다.(실제 인증서에 문제가 있는게 아니다. 그저 이 인증서를 발급한 사람이 믿음직한 사람인지 알 수 없을 뿐)
이런 인증서는 잘 작동하지만, 실제 사용해도 되는 인증서는 아니다. 실제로도 크롬이나 최신 브라우저로 해당 서버에 접속하면 아래와 같은 에러 메시지를 볼 것이다.

이 메시지는 인증서가 의심스러울때 나오는 오류이다.
다만 개인적으로 설명에 약간의 문제가 있다고 생각하는데 '이 사이트는 보안 연결(HTTPS)가 사용되지 않았습니다'라고 말하는데 실제로 HTTPS는 사용되었다. 다만 인증서에 문제가 있을 수 있다는 것이다. 실제로 해당 접속과 관련된 패킷들을 뜯어보면 죄다 암호화가 되서 내용을 읽을 수 없다.

아마 이런 식으로 설명한 건 HTTPS에 대해 기술적으로 잘 모르는 사람들한테 이런식으로 설명하면 "그럼 문제없는거 아냐?"라고 생각하는 것을 방지하기 위해 아예 보안연결 자체가 되지 않았다 라고 설명하는 것이라고 추측된다.

하지만 Let's Encrypt에서 발급하는 인증서는 openssl로 만든 자가 서명 인증서와는 달리, 진짜로 유효한 CA 이다. 즉 인증서로서 문제 없는, 실제 업무에 사용해도 전혀 문제없는 안전한 인증서이다.

실제 인증서를 구매하려고 하면 상당히 비싼데 Let's Encrypt는 이런 비싼 인증서를 무료로 우리한테 제공하는 것이다.

그러면 지금까지 Let's Encrypt가 대충 뭔지는 이해했으니 실제로 어떤 과정을 통해 인증서를 받아보자.

우선 위에 미리 얘기하자면 나는 ACME 프로토콜만 이용하고 certbot은 이용하지 않을 것이다.
certbot을 이용하는 방식은 차후에 추가로 해 볼 생각이다.

실제로 써 보자!

우선 Let's Encrypt를 이용하려면 IP가 아닌 도메인을 가지고 있어야 한다.
아마 이 글을 보러 온 사람중 도메인을 모르는 분은 없을 것 같지만 혹시 모르니 이야기를 해 보면 도메인은 IP를 사람이 알아볼 수 있는 문자 형태로 매칭시킨, 일종의 별칭이다.
예를 들면 naver.com에 접속할때 우리는 주소창에 https://www.naver.com/이렇게 작성하지만 실제 요청은 223.130.200.104쪽으로 간다.

위의 그림은 Wireshark와 WSL 환경에서 dig를 이용한 화면이다.
우측에 보면 naver.com의 ANSWER SECTION223.130.200.104라고 적혀 있는데, 실제 좌측에 캡쳐된 패킷을 읽어보면 해당 주소로 요청을 보낸 것을 볼 수 있다.

DNS에 대한 내용은 나중에 DoH(DNS over HTTPS)를 쓸 때 같이 써 보려고 한다.

주의할 사항이 있는데 위의 캡쳐를 실습해 보려 한다면 구글같은 엄청 큰 IT 업계 주소를 대상으로 하지 마라. 최근 HTTP3 프로토콜을 도입했다고 들었는데 QUIC이라는 녀석 때문인지 예상과는 다른 IP 주소로 자꾸 명령이 날아가는 것을 확인했다. 이 부분은 나중에 HTTP3에 대해 공부해 보며 확실히 알아볼 예정이다.

아무튼 도메인은 IP를 사람이 알아볼 수 있는 이름으로 매핑된 결과물인데 원래 TLS에서 인증서는 도메인이 없어도 작동하는데 전혀 문제가 없지만(물론 도메인이 있는 쪽이 좋다, 관련 보안 설정들도 존재하고) Let's Encrypt에서는 도메인을 가진 경우에만 해당 도메인에만 유효한 인증서를 발급해 준다. 이유는 뭐 보안적인 이러저러한 이야기가 있겠지만 중요한 건 도메인이 없으면 Let's Encrypt를 사용 불가능하다는 사실만 알면 된다.

그러니 일단 DNS를 써서 도메인을 만들고 ACME를 이용해 따로 인증서를 준비하지 않아도 TLS을 이용 가능한 서버를 만들어 보자.
나는 iptime DDNS 라는 iptime 공유기를 쓰면 기본적으로 지원하는 도메인을 이용했다.

우선 혹시나 순서대로 따라갈 사람들이 있을까봐 미리 이야기하자면 iptime DDNS는 안된다. 다른 DNS를 찾아보시라.

Go 언어, crypto/x/acme

Go 언어는 x 패키지로 acme를 지원하고 별도의 어플리케이션이나 라이브러리 없이 자체적으로 ACME를 사용 가능하다.
x 패키지 답게 세부적인 부분이 거친 것 같기는 한데 그래도 사용에는 전혀 지장이 없었다.
무엇보다 crypto/x/acme/autocert 패키지를 이용하면 기존 tls.Listen을 이용해 Let's Encrypt 인증서를 이용할 수 있다.

밑의 코드는 패키지에서 자체적으로 제공하는 기본 예제이다.

원본 예제 https://github.com/golang/crypto/blob/master/acme/autocert/example_test.go

func ExampleManager() {
	m := &autocert.Manager{
		Cache:      autocert.DirCache("secret-dir"),
		Prompt:     autocert.AcceptTOS,
		HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
	}
	s := &http.Server{
		Addr:      ":https",
		TLSConfig: m.TLSConfig(),
	}
	s.ListenAndServeTLS("", "")
}

위 코드에서 주목해야 할 부분은 autocert.Manager구조체인데 여기에 적절한 값을 세팅하면 이에 맞는 인증서를 자동으로 acme프로토콜을 통해가져와 별도로 인증서 발급절차 없이 인증서를 사용할 수 있다.

그런데 위의 코드는 사용 편의성을 위해 많은 것을 숨긴 코드이다. 위 코드를 따라하면 편하기야 하겠지만 내부 동작은 이해하기 힘들다.
그러니 ListenAndServeTLS, TLSConfig 같은 간편한 설정을 위한 코드들은 집어던지고 직접 설정을 다뤄보자.

func ExampleManager() {
	m := &autocert.Manager{
		Cache:      autocert.DirCache("인증서를 저장할 위치"),
		Prompt:     autocert.AcceptTOS,
		HostPolicy: autocert.HostWhitelist("내 도메인"),
	}
	listener, err := tls.Listen("tcp", ":https", &tls.Config{
		GetCertificate: m.GetCertificate,
		NextProtos:     []string{"h2", "http/1.1", acme.ALPNProto},
	})
	if err != nil {
		panic(err)
	}
	defer listener.Close()
	s := &http.Server{
		Handler: http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
			rw.Write([]byte("Hello, World"))
			rw.WriteHeader(200)
		}),
	}
	s.Serve(listener)
}

좀 더 길어지긴 했지만 별로 어려운 내용은 없었다. 그래도 일부 부분들을 보면서 알아낸 것들이 있으니 정리해보자.

    ...
    &tls.Config{
        GetCertificate: m.GetCertificate,
        NextProtos:     []string{"h2", "http/1.1", acme.ALPNProto},
	}
    ...

여기서 tls.Config.GetCertificate는 tls의 Client Hello메시지를 받고 서버가 Server Hello메시지를 보내기 전 Client Hello 메시지에 따라 인증서를 선택할 필요가 있거나 추가적인 작업이 필요할 때 설정하는 핸들러 함수이다.
우리는 평소에 이 필드를 nil로 지정해 두는데 이 경우 tlstls.Config.Certificates 필드에서 가장 적합한 인증서를 고른다.

여기서는 autocert.Manager.GetCertificate를 넣었는데 이 메서드는 autocert.Manager내부에 설정된 값과 Client Hello 메시지를 이용해 자동으로 acme프로토콜을 통해 적합한 인증서를 가져와서 제공하는 메서드이다.(물론 캐싱도 하고)

tls.Config.NextProtosTLS의 ALPN 확장에 대한 내용이다.

    []string{"h2", "http/1.1", acme.ALPNProto}

사실 내부적으로 세부 동작들을 이야기하면 끝도 없다. 그러니 간단히 이야기하자면 HTTP/2를 지원하고(h2), HTTP/1.1를 지원하고, acme(acme-tls/1)를 지원하겠다는 의미이다.
해당 필드들만 설정하면 나머지는 내부적으로 정의된 대로 소켓을 여기저기 잘 굴려 준다.
세부적인 코드들이 어떻게 흐르는지는 너무 길어서 차후 기회가 된다면 다시 써 보도록 하겠다.

이제 코드를 다 작성했으니 이제 실행해 보자.

? 뭔데

iptime ddns, 그럼 그렇지.

iptime 공유기들은 자체적인 ddns 서비스를 제공한다. *.iptime.org형태의 주소들은 해당 기능을 이용해 만들어지는 도메인이다.
이 방법이 내게 사용 가능한 방법 중 무료이고 가장 편리한 방법이였다. 그래서 써 봤고. 실패했다.
위 가장 기본적인 예제조차도 실패하고 이런 메시지를 보냈다.

그럼 왜 iptime ddns로 만들어진 도메인은 사용 불가능한지 알아보자.

CAA

DNS는 도메인에 IP를 연결하는 서버 라고 알고 있을 것이다. 그런데 DNS는 좀더 많은 정보를 제공한다. 그중 오늘 문제가 된 부분은 CAA라는 부분이다.
CAA는 Certification Authority Authorization의 약자인데 한국어로 번역하면 인증기관 허가, 즉 해당 도메인을 인증할 수 있는 인증기관을 인증한다 라는 의미이다

이게 뭔소린가 싶을텐데 도메인을 등록할 때는 나는 ~~에서만 인증받을 거니까 다른 인증기관은 여기에 인증서 만들지 마! 라는 의미이다.

보다 구체적으로 예시를 들어보자 myweb.com이라는 사이트가 있다. 해당 사이트는 ABC라는 CA에서 인증서를 발급받았고 myweb.com도메인에 CAA 로 ABC를 등록했다.
이 경우 DEF라는 CA에서 추가로 myweb.com에 대한 인증서를 발급받는다면 해당 인증서는 인증과정에서 유효하지 않다고 경고받을 것이다.
왜냐하면 myweb.comABC에서 발급된 인증서만을 사용한다고 기록되어 있기 때문이다.

iptime ddns는 이 CAA 필드가 문제가 된다.
iptime ddns는 어떤 기관의 인증서도 발급이 안되게 설정이 되어 있다.

위의 그림이 iptime이 사용 가능한 CAA이름에 대한 내용이다.
issue, issuewold가 모두 ";"라 설정되어 있는데 아마 아게 뭔지 모르는 분들도 이렇게 설정되어 있으면 아무 인증기관도 사용 불가능할거라고 예상할 수 있을 거라 생각한다.

이 CAA필드가 아예 없다면 문제없을텐데 하필이면 iptime.org에는 하필이면 이 필드가 제공된다. 하필이면 어떤 CA도 쓸수 없게. 그래서 혹시나 iptime DDNS에 CAA를 설정할 수 있나 찾아봤는데 없는 것 같다.
다들 나처럼 시간낭비 하지 말고 그냥 속편히 DNS 하나 사라.

Route 53, 도메인 구매

Route 53은 AWS에서 제공하는 DNS 겸 도메인 제공 서비스다. 여기서 도메인을 구매하면 AWS의 dns 서버를 이용할 수 있고 AWS의 끝내주는 인프라를 이용 가능하기에 여기서 도메인을 구매했다.

나는 여기서 .link 형태의 도메인을 구매했다. 이 TLD를 택한 이유는 그냥 할인해서다. 원래 가격은 잘 모르겠고 할인가는 5000원이다.

무료 도메인도 구글에 검색하면 나온다. 하지만 1년에 5000원 밖에 안하는데 학습할때 이돈을 아끼느니 그냥 사는게 속편할 것 같다. 또 AWS를 쓰면서 이에 익숙해지는 것도 좋다고 생각한다. 만약 돈이 아깝다면 구글에 검색하시라. 다른 분들이 이에 관한 글을 많이 써 놓았다.

Route 53, dns 세팅

Route 53에서 도메인을 구매했다면 DNS서버에 해당 도메인이 어디로 연결될지를 써 넣어야 한다.
나는 해당 부분을 이렇게 설정했다.

위에 그림처럼 CCA 필드와 CNAME을 이용해서 내 도메인을 설정했다.
www.내 도메인.link으로 접속하면 자동으로 iptime의 ddns로 연결되게 설정했다.

? Route 53으로 도메인을 쓰는데 왜 iptime ddns도 쓰냐? 라고 생각할 수도 있는데 나는 고정 IP를 가진게 아니라서 아주 간혹가다 IP가 바뀐다.(거의 그럴일 없지만 진짜 가끔 바뀔때가 있다.)

이때 iptime ddns는 자동으로 내 변경된 ip를 감지하고 *.iptime.org형태의 도메인에 바뀐 IP를 적용해준다.(공유기에서 자동으로 iptime에 내 ip를 보내준다.)

만약 iptime ddns를 떼면 내 호스트 PC든 아니면 특수한 뭔가를 쓰던 해서 IP가 변경될 때마다 AWS에 "내 IP가 ~로 바뀌었어요" 라고 계속 알려줘야 하는데 관련 스크립트를 짜는게 어려운 건 아니지만... 솔직히 귀찮다. 편한게 최고다. DNS에 CNAME 필드를 설정하면 자동으로 리다이렉트 시켜주는데 굳이 복잡하게 뭘 할 필요가 있나.

그렇기에 가장 편한 수단인 iptime ddns를 이용했다.
공유기에 따라서는 이런 중개수단 없이 바로 아마존으로 ip 정보를 쏴주는 공유기도 있다고 하는데 지금 내가 쓰는 공유기는 자취방에 기본으로 달려있던 802.11 ac도 지원안하는 유물이라 그런 기능은 바라지도 않는다.

그리고 마침내 코드를 돌려서 성공적으로 ACME를 적용한 웹서버를 돌리는데 성공했다.

이런걸 할때마다 느끼는게 원리 자체는 쉬운데 실제 적용하려면 이런저런 제약이라던가, 배우는데 돈이 들어간다던가 해서 곤란하다.

Let's Encrypt는 CA의 혁명이다!

확실히 학습용으로 만든 서버에 빨간딱지가 안붙어 있으니 마음이 편안하다. 지금까지 이 빨간 딱지를 떼려고 인증서를 살까? 했는데 비싸기도 하고 절차도 까다로워 포기했던 적이 있다.

그런데 이걸 사용하니 그냥 자동으로 인증서가 따라오네?
심지어 실무에 써도 되는 녀석들이라고?
근데 무료?

와우 그럼 써야지.

그럼 어째서 이런 좋은 서비스를 무료로 제공하는 걸까?
솔직히 그 이유는 잘 모르겠다. 단순히 비영리 단체가 주도해서 진행한다면 신념에 의해 하는 것일거라고 생각되지만 저런 빅테크 기업이 한두개도 아니고 다들 사이좋게 같이 후원한다?
이걸 보고 그냥 쟤들이 '인증서를 못받는 가여운 자들을 어여삐 여기사' 라고 생각하는건 말도 안되니 직접 그 이유를 몇개 생각해 보았다.

이게 맞다는 게 아니고 그냥 개인적인 생각이다. 그냥 재미로 보면 한다.

  • 보안 문제가 너무 치명적이다. 점점 인터넷에 보안은 핵심이 되어가는데 HTTP는 패킷을 전혀 암호화하지 않아 수많은 문제를 일으켰고, 이런식으로 보안문제가 계속되면 당연히 IT 기업들 입장에서는 좋을 게 없을 테니 선제적으로 조치를 취했을 수도 있을 것 같다.

  • HTTP/2 부터는 TLS가 필수라고 한다. 그런데 인증서 때문에 HTTP/2를 이용하지 않는 경우가 있다면 HTTP/2부터 적용된 트래픽을 감소시키기 위한 많은 신기술 역시도 쓸 수 없다.(HPACK이라던가 Push라던가... 이건 망했지만) 이 트래픽이란 녀석은 빅테크 기업들도 써야 하는데 HTTP/2를 적용하지 않는 서버가 있다면(혹은 못하는) 위의 빅테크 기업들도 같이 써야하는 트래픽에 부담을 줄것이다. 그래서 HTTP/2같은 신기술을 사용하도록 유도하기 위해 큰 허들이 되는 CA 부분을 손본 것이라는 예상이다. 과거에 구글이 webp, webm같은 포멧을 비싼 돈 들여 개발하고 무료로 푸는 이유에서 해당 기술이 널리 쓰여 구글에서 발생하는 트래픽을 줄이면 결과적으로 기업에 이익이 되기에 해당 포멧들을 무료로 푼다는 이야기를 들은 적이 있다. 이런 이유의 연장선상에서 Let's Encrypt가 나온 게 아닐까?

여하튼 이번 기회에 TLS와 서버 보안에 대한 내용을 많이 공부하게 되었다.

이글을 읽은 다른 프로그래머 분들도 Let's Encrypt에 대해 들어보기만 하고 쓴 적이 없다면 한번 이번 기회에 자기가 주로 사용하는 서버에 이걸 사용해 보는게 어떨까?

서버 코드는 https://gist.github.com/iamGreedy/acme-autocert.go 여기에 있습니다.

profile
수준 높은 기술 포스트를 위해서 노력중...

0개의 댓글