LabHub
배우기 러닝패스 코스

인증서는 갱신됐는데 브라우저는 옛것을 보여줬다 · 리다이렉트 한 줄이 갱신을 막았다 · 이론

발급은 되는데 갱신에서만 터지는 설정

LabHub 에서 이어서 보기

한 줄 요약

ACME 는 "이 도메인을 내가 통제한다" 를 HTTP 나 DNS 로 증명하면 CA 가 인증서를 내주는 프로토콜이고, cert-manager 는 그 증명과 갱신을 클러스터 안에서 대신한다. 발급은 잘 되는데 갱신에서만 터지는 설정 오류가 있고, 홈랩에서 실제로 겪었다.

왜 이게 필요했나

인증서를 사람이 갱신하던 시절에는 만료 사고가 잦았다. 짧은 유효기간과 자동 갱신이 답이었고, 그러려면 CA 가 도메인 통제권을 사람 없이 확인할 방법이 필요했다. [RFC 8555](https://datatracker.ietf.org/doc/html/rfc8555)(ACME)가 그 규약이다. 문제는 자동화가 성공한 것처럼 보이는 실패를 만든다는 점이다. 첫 발급은 잘 됐다. 그 뒤 게이트웨이 설정을 하나 바꿨다. 아무 증상이 없다 — 인증서는 아직 두 달 남았으니까. 갱신 시점에야 챌린지가 실패하고, 그때는 왜 실패하는지 아무도 그 설정 변경을 떠올리지 못한다.

어떻게 동작하나

ACME 클라이언트는 계정 키로 CA 에 주문(order)을 내고, CA 는 도메인마다 챌린지를 준다. HTTP-01 은 http://<도메인>/.well-known/acme-challenge/<token> 을 GET 했을 때 본문이 키 인가(key authorization)token || '.' || base64url(Thumbprint(accountKey)) — 와 같아야 한다. RFC 는 이 요청을 TCP 80 으로 보내라고 정하고, 검증 서버는 리다이렉트를 따라가도 된다(SHOULD)고 적는다. Let's Encrypt 의 [설명](https://letsencrypt.org/docs/challenge-types/)에 따르면 실제 구현은 최대 10단계까지 리다이렉트를 따라가되 http:·https: 의 80·443 포트로만 가며, HTTPS 로 튕겼을 때 인증서는 검증하지 않는다. DNS-01 은 _acme-challenge.<도메인> 의 TXT 레코드에 계정 키에서 유도한 값을 넣는 방식이고, 와일드카드 인증서는 DNS-01 로만 받을 수 있다.

GET /.well-known/acme-challenge/zk3r9Qm...   Host: www.lab.internal200 OKzk3r9Qm....Q7pVw2x...          ← token.thumbprint, 이것이 본문 전체

[cert-manager](https://cert-manager.io/docs/usage/certificate/) 는 이 절차를 컨트롤러로 만든 것이다. Issuer/ClusterIssuer 가 CA 와의 계약(계정 키·솔버)을 갖고, Certificate 리소스가 원하는 인증서를 선언한다 — secretName(결과가 저장될 Secret), dnsNames, issuerRef(다른 네임스페이스에서도 쓰려면 kind: ClusterIssuer), duration(기본 90일), renewBefore. 문서대로 durationrenewBefore 는 Go 의 시간 문자열이라 h·m·s 만 쓸 수 있고 d(일)는 안 되며, durationrenewBefore 보다 커야 한다. renewBefore 를 적지 않으면 발급된 인증서 수명의 2/3 지점에서 갱신하고, 실제 수명이 요청보다 짧게 나올 수 있으므로 절대값보다 renewBeforePercentage 를 권한다. HTTP-01 솔버는 챌린지 동안 임시 Ingress 나 HTTPRoute 를 만들어 챌린지 경로를 솔버 파드로 보내는데, [문서](https://cert-manager.io/docs/configuration/acme/http01/)는 Gateway API 를 쓸 때 그 라우트가 80 포트 리스너를 가진 Gateway 에 붙어야 한다고 적는다.

현장에서 만나는 모습

홈랩에서 겪은 사례다. 게이트웨이(Cilium Gateway)에 HTTP→HTTPS 리다이렉트를 켰다. 사이트는 잘 돌았다. 그런데 이 리다이렉트는 /.well-known/acme-challenge/ 요청까지 HTTPS 로 튕겼고, HTTPS 리스너에는 솔버 라우트가 없어 그 끝은 404 였다. 발급은 이미 끝나 있었으니 증상이 없었고, 만료 30일 전 갱신에서야 조용히 실패할 상태였다. "솔버 라우트가 경로가 더 구체적이니 이기지 않을까" 를 실제로 재 봤다 — 솔버를 흉내 낸 Exact 경로 HTTPRoute 를 띄워 그 경로를 쳤더니 리다이렉트가 이겼다(Cilium 1.20.1 에서의 실측). 경로 구체성은 우선하지 않았다. 고치는 법은 리다이렉트를 게이트웨이가 아니라 앱에서 하되 챌린지 경로는 예외로 두는 것이었고, 상시 확인은 한 줄이다 — curl -sI http://<도메인>/.well-known/acme-challenge/x | head -1 이 301 이면 갱신이 막힌 것이고 404 나 200 이면 정상이다. 파일이 없어서 404 인 것은 괜찮다. 리다이렉트만 아니면 된다.

한 가지 더. 이 설정으로 갱신을 한 번도 겪지 않은 상태였으므로, 만료를 기다리지 않고 Certificate 의 status 에 Issuing 조건을 세워 갱신을 강제로 일으켰다. 몇 초 뒤 새 CertificateRequest 가 만들어졌고, 판정은 openssl s_client 로 받은 인증서의 날짜가 바뀐 것으로 했다. 자동화는 실제 발급 이력이 있어야 믿을 수 있다.

다음 실습에서 할 것

파드 안에 HTTP 와 HTTPS 리스너를 가진 엣지(/opt/app/edge.py)와 HTTP-01 검증 서버(/opt/app/acme_va.py)를 띄운다. 키 인가 파일을 두고 검증을 VALID 로 통과시킨 뒤, 리다이렉트를 켜서 301 → 404 로 INVALID 가 나는 사고를 재현하고, 챌린지 경로만 예외로 두어 리다이렉트를 유지한 채 다시 VALID 를 받는다. 갱신이 막혔는지 보는 probe.sh 를 만들어 채점기의 서버로 양방향 검사를 받고, cert-manager Certificate 매니페스트를 duration·renewBefore·ClusterIssuer 로 작성한다.