날씨 정보를 불러오는 중...

Let's Encrypt로 무료 HTTPS 적용하기 - 홈서버 인증서를 Traefik이 알아서 발급·갱신하는 구조

2026-10-14 06:45 · IT기술 · 조회 25
banner

Let's Encrypt로 무료 HTTPS 적용하기 - 홈서버 인증서를 Traefik이 알아서 발급·갱신하는 구조

사이트에 https를 붙이려면 인증서가 필요하다. 예전에는 연 몇 만 원짜리 인증서를 사서 파일을 서버에 올리고, 만료 전에 직접 갈아 끼웠다. Let's Encrypt가 나온 뒤로는 무료이고, 발급부터 갱신까지 전부 자동화할 수 있다. 이 블로그도 Let's Encrypt 인증서로 돌아가고, 내가 인증서 파일을 만진 적은 한 번도 없다. 이 글에서는 개념부터 내 k3s에 실제로 붙어 있는 설정과 확인 방법까지 정리한다.

Let's Encrypt란

비영리 단체 ISRG가 운영하는 공개 인증 기관(CA)이다. 무료이고, 주요 브라우저와 운영체제가 기본으로 신뢰하므로 방문자 화면에 경고가 뜨지 않는다. 비용만 없을 뿐 보안 수준이 낮은 인증서가 아니다. 암호화 강도는 유료 DV(도메인 검증) 인증서와 같다.

  • 검증 수준은 DV(도메인 소유 확인)만 제공한다. 회사명이 표시되는 OV·EV 인증서는 발급하지 않는다.
  • 유효기간은 90일이다. 짧은 이유는 자동 갱신을 전제로 하고, 키가 유출돼도 피해 기간을 줄이기 위해서다.
  • 발급과 갱신은 ACME라는 프로토콜로 자동 처리한다.

동작 원리: ACME와 도메인 검증

CA는 "이 도메인이 정말 네 것이냐"를 확인해야 인증서를 준다. 서버가 CA에게 과제를 받아 풀면 그 증거로 소유를 인정한다. 과제 방식은 세 가지다.

  • HTTP-01: 80번 포트의 /.well-known/acme-challenge/ 경로에 CA가 준 토큰을 올려 둔다. 가장 흔한 방식이다. 와일드카드는 불가능하다.
  • TLS-ALPN-01: 443번 포트의 TLS 핸드셰이크 안에서 검증한다. 80번 포트가 필요 없다. 내가 쓰는 방식이다.
  • DNS-01: 도메인의 TXT 레코드에 토큰을 넣는다. 서버가 외부에 열려 있지 않아도 되고, 와일드카드(*.example.com)는 이 방식으로만 받을 수 있다. 대신 DNS 제공자 API 연동이 필요하다.

흐름은 단순하다. 서버가 CA에 계정을 만들고, 도메인에 대한 인증서를 요청하고, 과제를 풀고, 인증서를 받아서 저장한다. 이 모든 걸 클라이언트(certbot, acme.sh, Traefik, Caddy 등)가 대신 해 준다.

나는 Traefik 내장 ACME를 쓴다

k3s에는 Traefik이 기본으로 들어 있고, Traefik이 Let's Encrypt 클라이언트 기능을 내장하고 있다. 그래서 cert-manager나 certbot을 따로 설치하지 않았다. 지금 운영 중인 Traefik의 실행 인자는 이렇다.

--entryPoints.web.address=:8000/tcp
--entryPoints.websecure.address=:8443/tcp
--entryPoints.websecure.http.tls=true
--certificatesresolvers.letsencrypt.acme.tlschallenge=true
--certificatesresolvers.letsencrypt.acme.email=(내 이메일)
--certificatesresolvers.letsencrypt.acme.storage=/data/acme.json

의미는 다음과 같다.

  • certificatesresolvers.letsencrypt: "letsencrypt"라는 이름의 인증서 발급기를 만든다. 이름은 내가 정한 것이고, 아래 IngressRoute에서 이 이름으로 부른다.
  • tlschallenge=true: TLS-ALPN-01 방식을 쓴다. 443번만 외부에서 도달하면 된다.
  • email: 만료 경고 메일을 받을 주소다. 자동 갱신이 실패했을 때 마지막 안전망이 된다.
  • storage=/data/acme.json: 계정 키와 발급받은 인증서를 이 파일에 저장한다.

서비스에는 한 줄만 붙인다

각 서비스의 IngressRoute에서 발급기 이름만 지정하면 끝이다. 설정은 이렇다(도메인은 예시).

spec:
  entryPoints:
    - websecure
  routes:
    - kind: Rule
      match: Host(`example.com`)
      services:
        - name: mini-portal
          port: 5000
  tls:
    certResolver: letsencrypt

Traefik은 라우터의 Host 규칙에서 도메인을 읽어 인증서를 발급받는다. 이후에는 같은 도메인을 요청하는 모든 라우터가 그 인증서를 공유한다. 새 서비스를 올릴 때도 certResolver: letsencrypt 한 줄이면 https가 붙는다.

함정: http 리다이렉트는 별도 IngressRoute로

tls: 블록이 있는 IngressRoute는 entryPoints에 web(80번)을 같이 적어도 http 라우터를 조용히 지워 버린다. 그래서 http에서 https로 보내는 리다이렉트는 tls 블록이 없는 IngressRoute를 하나 더 만들어야 한다. 나는 서비스마다 -http-redirect라는 이름의 짝을 둔다.

kind: IngressRoute
metadata:
  name: mini-portal-http-redirect
spec:
  entryPoints:
    - web
  routes:
    - kind: Rule
      match: Host(`example.com`)
      middlewares:
        - name: redirect-to-https
      services:
        - name: mini-portal
          port: 5000

발급된 인증서 목록 확인하기

Traefik이 발급한 인증서는 acme.json에 base64로 들어 있다. 이 파일을 열어 도메인별 만료일을 뽑으면, 지금 어떤 인증서가 언제까지 유효한지 한눈에 볼 수 있다.

kubectl -n kube-system exec deploy/traefik -- cat /data/acme.json | python3 -c "
import json,sys,base64,subprocess
d=json.load(sys.stdin)
for r,v in d.items():
  for c in v.get('Certificates') or []:
    pem=base64.b64decode(c['certificate'])
    o=subprocess.run(['openssl','x509','-noout','-enddate'],input=pem,capture_output=True).stdout.decode().strip()
    print(c['domain']['main'], o)
"
example.com        notAfter=Dec 10 22:59:28 2026 GMT
blog.example.com   notAfter=Dec 12 07:42:26 2026 GMT
game.example.com   notAfter=Jan 10 22:50:57 2027 GMT

위 출력은 형식을 보여 주려고 도메인을 예시로 바꾼 것이다. 이 목록에서 볼 점이 몇 가지 있다.

  • 모든 인증서의 만료일은 발급일에서 정확히 90일 뒤다.
  • 인증서마다 발급 시점이 달라서 만료일이 제각각이다. 한꺼번에 만료돼 전부 멈추는 사고가 나지 않는다.
  • 서비스를 새로 올릴 때마다 목록이 한 줄씩 늘어난다. 지워진 서비스의 인증서는 남아 있을 수 있으니 가끔 정리해 준다.
  • 도메인 하나에 별칭이 여럿이면 한 장에 묶여 발급된다(SAN). 이때 목록의 대표 도메인(main)과 실제 인증서 제목이 다르게 보일 수 있다.

밖에서 실제로 어떤 인증서가 내려오는지는 이 명령으로 본다.

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -issuer -dates -ext subjectAltName
issuer=C = US, O = Let's Encrypt, CN = YR2
notBefore=Sep 11 22:59:29 2026 GMT
notAfter=Dec 10 22:59:28 2026 GMT
X509v3 Subject Alternative Name:
    DNS:example.com, DNS:www.example.com

발급자의 YR2는 Let's Encrypt가 쓰는 중간 인증서 이름이다. 중간 인증서는 시기에 따라 바뀌니 이름이 달라 보여도 놀랄 필요 없다. SAN(Subject Alternative Name) 항목에 도메인 두 개가 한 장에 묶여 있는 것도 보인다. 한 인증서에 여러 도메인을 넣는 방식이라, 같은 서비스에 붙은 여러 주소가 같은 인증서를 쓴다.

자동 갱신은 어떻게 도는가

Traefik은 유효기간이 약 30일 남으면 갱신을 시도한다. 시작할 때와 주기적으로 인증서를 점검하고, 로그에는 이런 줄이 남는다.

INF Testing certificate renew... acmeCA=https://acme-v02.api.letsencrypt.org/directory providerName=letsencrypt.acme
INF Obtaining bundled SAN certificate. domains=game.example.com lib=lego
INF Validations succeeded; requesting certificates. domains=game.example.com lib=lego
INF Server responded with a certificate. domains=game.example.com lib=lego

아래 세 줄은 시험 배포한 새 서비스(game.example.com으로 바꿔 표기)를 올렸을 때 나온 것이다. 검증을 시작해서 인증서를 받기까지 약 20초가 걸렸다. 사람이 한 일은 IngressRoute에 한 줄 적은 것뿐이다.

운영하면서 지켜야 할 것

  • acme.json을 반드시 영구 저장하라. 파드가 재시작될 때마다 파일이 사라지면 매번 새로 발급받게 되고, 곧 발급 횟수 제한에 걸린다. 파일 권한은 600이어야 하고(Traefik은 그렇지 않으면 거부한다), 이 파일에는 개인 키가 있으니 백업 대상과 공개 저장소 업로드 금지 대상에 모두 넣어서 관리해야 한다.
  • 발급 횟수 제한을 알아 두라. 같은 도메인 묶음의 중복 발급은 주 5회, 등록 도메인당 신규 인증서는 주 50장 같은 한도가 있다. 설정을 시험하다가 한도를 소진하면 최대 일주일을 기다려야 한다. 실험할 때는 아래의 스테이징 서버를 쓴다.
    --certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
    스테이징 인증서는 브라우저가 신뢰하지 않지만 한도가 훨씬 넉넉하다.
  • 443번 포트가 인터넷에서 닫혀 있으면 발급이 안 된다. TLS-ALPN-01은 CA가 외부에서 직접 443으로 접속해 검증한다. 공유기 포트포워딩과 방화벽을 먼저 확인하라. 내부망에서만 쓰는 서비스는 HTTP-01이나 TLS-ALPN-01로는 발급이 안 되므로 DNS-01을 써야 한다.
  • DNS가 서버를 가리켜야 한다. 도메인을 사고 DNS를 바꾸기 전에 라우터부터 올리면 검증이 실패한다. 실패가 반복되면 한도에 걸릴 수 있으니 DNS 반영을 확인한 뒤 접속하라.
  • 만료 알림 메일 주소는 실제로 확인하는 주소를 써라. 자동 갱신이 조용히 멈춘 경우, 알아차리는 수단이 이 메일이나 외부 모니터링뿐이다.

다른 선택지와 비교

  • certbot: 가장 널리 쓰이는 클라이언트다. nginx나 Apache 단독 서버에서 certbot --nginx 한 줄로 끝난다. 쿠버네티스 밖에서는 이쪽이 편하다.
  • acme.sh: 셸 스크립트 하나로 동작해서 가볍다. DNS-01 연동 플러그인이 많아 와일드카드에 유리하다.
  • cert-manager: 쿠버네티스에서 인증서를 Secret 리소스로 관리한다. 여러 Ingress 컨트롤러를 쓰거나 인증서를 다른 파드에서 직접 써야 하면 이쪽이 낫다.
  • Caddy / Traefik 내장 ACME: 설정 없이 자동으로 된다. 단일 노드 홈서버에는 가장 단순하다.

마치며

Let's Encrypt는 "인증서를 사서 관리한다"는 일을 "설정 한 줄을 적는다"로 바꿔 놓았다. 나는 Traefik 인자 몇 줄과 서비스마다 certResolver: letsencrypt 한 줄로 여러 도메인을 운영하고 있다. 직접 챙겨야 할 것은 세 가지다. 443 포트가 열려 있는지, acme.json이 영구 저장되고 백업되는지, 알림 메일이 도착하는지다. 이것만 지키면 인증서 만료로 사이트가 멈추는 일은 거의 없다.


댓글 0

글에 대한 질문이나 오류 제보는 문의하기로 보내 주세요.