
홈랩을 처음 구축할 때 mkcert로 자체 서명 인증서를 만들었다. home.lab이라는 로컬 도메인을 쓰고, Traefik에 인증서를 걸어두면 끝이라고 생각했는데 막상 운영해보니 문제가 꽤 있었다.
mkcert의 로컬 CA 인증서를 접속하는 모든 기기에 설치해야 했다. PC는 그렇다 치고 스마트폰이나 태블릿까지 일일이 CA를 심는 건 번거로웠다. Tailscale로 외부에서 접속할 때도 매번 브라우저 보안 경고가 떴고, 인증서 만료되면 수동으로 다시 발급해야 했다.
결국 공인 도메인과 공인 인증서가 필요하다는 결론에 도달했다. 홈랩이니까 자체 서명이면 충분하다고 생각했는데 실제로는 그렇지 않았다.
Let's Encrypt로 무료 인증서를 발급받으려면 도메인 소유를 증명해야 한다. 보통은 HTTP-01 challenge를 쓰는데 웹 서버가 외부에서 접근 가능해야 동작한다. 홈랩은 공인 IP가 없으니 이 방식은 안 된다.
DNS-01 challenge는 다르다. DNS TXT 레코드를 생성해서 도메인 소유를 증명하는 방식이라 웹 서버가 외부에 열려 있을 필요가 없다. 더 좋은 건 와일드카드 인증서를 발급받을 수 있다는 점이다. *.xssh.org 하나만 있으면 grafana.xssh.org든 syncthing.xssh.org든 인증서를 따로 신경 쓸 게 없다.
xssh.org 도메인을 Cloudflare에서 관리하고 있어서 Cloudflare API 토큰을 발급받아 cert-manager에 연결했다. 전체 흐름을 정리하면 이렇다.
갱신도 자동이다. cert-manager가 만료 30일 전에 알아서 갱신해준다.
ClusterIssuer는 인증서를 어디서 어떻게 발급받을지 정의하는 리소스다. Cloudflare API 토큰을 Secret으로 만들어두고 참조하게 했다.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-tokenCertificate 리소스에서 *.xssh.org와 xssh.org 두 개를 dnsNames에 넣었다. 와일드카드는 서브도메인만 커버하고 루트 도메인은 별도로 지정해야 한다.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: xssh-org-wildcard
namespace: traefik
spec:
secretName: xssh-org-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- "*.xssh.org"
- "xssh.org"인증서가 발급되면 Traefik에 적용해야 한다. 여기서 꽤 깔끔한 패턴을 하나 발견했다. TLSStore의 default 리소스에 와일드카드 인증서를 등록해두면, 개별 IngressRoute에서는 tls: {} 한 줄만 넣으면 된다.
apiVersion: traefik.io/v1alpha1
kind: TLSStore
metadata:
name: default
namespace: traefik
spec:
defaultCertificate:
secretName: xssh-org-tls기존에는 mkcert로 만든 homelab-tls Secret을 참조하고 있었는데, 이걸 cert-manager가 관리하는 xssh-org-tls로 바꿔주면 끝이다. Grafana, Syncthing 등 모든 IngressRoute의 TLS 설정을 건드릴 필요가 없었다.
인증서 전환과 함께 도메인도 바꿨다. home.lab은 로컬에서만 쓸 수 있는 가짜 도메인이라 외부 접속이 안 됐다. xssh.org로 통일하면 내부든 외부든 같은 주소로 접속할 수 있다.
바꿔야 할 것이 꽤 있었다.
Cloudflare DNS 관리를 편하게 하려고 간단한 셸 스크립트도 만들었다. A 레코드를 추가하거나 삭제할 때 대시보드에 안 들어가도 된다.
cert-manager Helm 차트를 설치할 때 installCRDs: true 옵션을 빼먹으면 CRD가 없어서 ClusterIssuer나 Certificate를 만들 수 없다. Helm 차트 설치 시 CRD를 같이 설치하거나 kubectl apply로 먼저 CRD를 깔아야 한다.
Cloudflare API 토큰도 권한 설정에 주의가 필요하다. Zone:DNS:Edit 권한이 있어야 TXT 레코드를 생성할 수 있고, 토큰의 Zone 범위도 정확히 해당 도메인으로 지정해야 한다. 처음에 All zones로 설정했다가 보안상 좋지 않아서 xssh.org만 지정하도록 바꿨다.
Certificate 리소스의 네임스페이스도 중요하다. Traefik이 traefik 네임스페이스에서 동작하고 있으면 Certificate도 같은 네임스페이스에 만들어야 한다. Secret이 다른 네임스페이스에 생기면 Traefik이 접근하지 못한다.
브라우저 보안 경고가 완전히 사라졌다. 어디서 접속하든 초록색 자물쇠가 뜬다. 새 서비스를 추가할 때 인증서를 따로 만들 필요도 없다. IngressRoute에 호스트명만 지정하고 tls: {}를 넣으면 와일드카드 인증서가 자동으로 적용된다.
인증서 갱신도 완전 자동이다. 3개월마다 Let's Encrypt 인증서가 만료되는데 cert-manager가 알아서 갱신해준다. 한번 세팅해두니 신경 쓸 일이 없어졌다.
이번 작업에서 배운 핵심은 두 가지다.
첫째, 홈랩이라도 공인 TLS 인증서를 자동으로 관리할 수 있다. DNS-01 challenge 덕분에 공인 IP가 없어도 Let's Encrypt 인증서를 발급받을 수 있고, cert-manager가 발급부터 갱신까지 전부 처리해준다.
둘째, 와일드카드 인증서 + TLSStore 기본 인증서 패턴은 서비스 확장이 정말 편하다. 새 서비스 추가할 때 인증서 걱정이 사라진다. 처음에 제대로 세팅해두면 나중에 할 일이 없다.
"홈랩이니까 대충 해도 되지" 싶었는데 보안 기본기는 처음부터 제대로 하는 게 맞았다. 나중에 외부에 서비스를 공개할 때도 추가 작업 없이 바로 가능하니까.