멀티테넌트 주소 설계 — 목적을 먼저 정해야 두 번 짓지 않습니다
앞의 두 글에서 와일드카드 인증서를 DNS-01로 받는 방법과 그것이 못 덮는 영역, 그리고 Caddy on-demand TLS를 정리했습니다. 두 글 모두 "어떻게"에 대한 이야기였습니다.
그런데 작업을 마치고 돌이켜 생각해보니, 정작 제일 먼저 물었어야 할 질문은 따로 있었습니다. 왜 테넌트에게 주소를 주려고 하는가입니다. 부끄럽지만 저는 이걸 나중에 물었습니다.
하나의 요구가 세 가지 목적을 감추고 있었습니다
"테넌트마다 주소를 주자"는 말은 한 문장이지만, 그 안에는 서로 다른 목적이 섞여 있습니다.
| 목적 | 원하는 것 | 필요한 인프라 |
|---|---|---|
| ⓐ 브랜딩 | 주소에 회사 이름이 보이면 됨 | 없음 — 경로면 충분 |
| ⓑ 서브도메인 | acme.example.com |
와일드카드 인증서 (DNS-01) |
| ⓒ 커스텀 도메인 | workspace.customer.co.kr |
on-demand TLS |
ⓐ가 목적이라면 example.com/acme 같은 경로 방식이 압도적으로 쌉니다. 인증서도, DNS도, 프록시 설정도 건드릴 것이 없습니다. 인프라 비용이 문자 그대로 0입니다.
그런데 ⓐ와 ⓑ는 겉보기 요구가 거의 같습니다. 둘 다 "우리 회사 이름이 보였으면 좋겠다"로 시작합니다. 그래서 확인하지 않으면 경로 한 줄이면 될 일에 인증서와 IAM 정책을 짓게 됩니다.
ⓒ가 로드맵에 있으면 ⓑ는 버려집니다
더 중요한 것은 ⓑ와 ⓒ의 관계였습니다.
앞 글에서 봤듯이 *.example.com은 고객 소유 도메인을 못 덮습니다. 반대로 on-demand TLS는 둘 다 처리합니다. 서브도메인이든 고객 도메인이든 같은 메커니즘으로 발급되기 때문입니다.
ⓑ만 필요 → 와일드카드 DNS-01 (Traefik 유지)
ⓒ가 필요 → on-demand TLS 하나로 ⓑ + ⓒ
ⓑ 먼저 → ⓒ 나중 → 와일드카드는 폐기, 프록시 교체를 두 번째로 미룬 것뿐
마지막 줄이 핵심입니다. ⓒ가 언젠가 온다면 ⓑ를 위해 지은 와일드카드 구조는 나중에 버려질 중간 구조물입니다. IAM 정책, resolver 설정, DNS 레코드, 라우터 라벨을 다 만들어두고 몇 달 뒤에 전부 걷어내게 됩니다.
그래서 결론은 이렇게 좁아졌습니다. 와일드카드 DNS-01이 정답인 구간은 "서브도메인은 필요한데 커스텀 도메인은 로드맵에 없다" 하나뿐입니다. 그 조건이 아니라면 위로 가거나(경로) 아래로 가야(on-demand) 합니다.
이 판단을 먼저 했다면 작업 순서가 달라졌을 것입니다. 저는 반대로 인프라를 거의 다 설계한 뒤에야 "그런데 이게 목적에 맞나"를 물었습니다.
그리고 진짜 비용은 TLS가 아니었습니다
커스텀 도메인 이야기를 하면서 계속 인증서만 봤는데, 실제로 더 비싼 것은 따로 있었습니다. 세션입니다.
브라우저는 쿠키의 same-site 여부를 등록가능도메인(eTLD+1) 기준으로 판정합니다. 그래서 이렇게 됩니다.
| 화면 주소 | API 주소 | 판정 |
|---|---|---|
acme.example.com |
api.example.com |
same-site (둘 다 example.com) |
workspace.customer.co.kr |
api.example.com |
cross-site |
서브도메인일 때는 아무 문제가 없습니다. 그런데 고객 도메인으로 가는 순간 등록가능도메인이 달라지고, SameSite=Lax 쿠키는 전송되지 않습니다. 인증서를 아무리 잘 발급해도 로그인이 유지되지 않습니다.
정공법은 생각보다 깁니다
교과서적인 해법은 SameSite=None; Secure로 바꾸는 것입니다. 다만 이건 한 줄 수정이 아닙니다. SameSite=Lax가 덤으로 해주던 CSRF 방어가 사라지므로 CSRF 토큰 체계를 따로 세워야 합니다. 그리고 서드파티 쿠키 제한이 강해지는 흐름을 생각하면, 이 방향은 시간이 갈수록 불안해집니다.
즉 TLS는 며칠이면 되는데 세션은 1~2주짜리가 됩니다. 비용의 무게중심이 완전히 다른 곳에 있었습니다.
문제를 없애는 쪽이 더 쌉니다
그래서 다른 길을 봤습니다. cross-site를 해결하는 대신 발생하지 않게 하는 것입니다. 브라우저가 API를 별도 호스트로 부르지 않고, 지금 보고 있는 그 오리진의 /api/*로 부르게 하면 됩니다.
기존
브라우저 → https://workspace.customer.co.kr (화면)
브라우저 → https://api.example.com (API) ← cross-site
같은 오리진으로
브라우저 → https://workspace.customer.co.kr (화면)
브라우저 → https://workspace.customer.co.kr/api/* (API) ← same-origin
프록시가 내부 API로 전달
프록시가 이미 앞단에 있으니 경로 하나를 내부로 넘기는 설정이면 됩니다. 쿠키는 다시 first-party가 되고, SameSite=Lax를 유지할 수 있고, CSRF 토큰 체계도 필요 없어집니다. OAuth 콜백 주소가 오리진별로 달라지는 문제도 같이 정리됩니다.
공교롭게도 이 구조는 커스텀 도메인과 무관하게도 이점이 있습니다. 프론트가 API 주소를 몰라도 되니 환경별 설정이 하나 줄어듭니다.
정리하면
이 작업에서 배운 것을 한 줄로 줄이면 이렇습니다. 눈에 보이는 부분이 제일 비싼 부분은 아니었습니다.
TLS는 눈에 보였습니다. 인증서가 없으면 브라우저가 빨간 경고를 띄우니까요. 그래서 자연스럽게 거기부터 파고들었고, ACME 챌린지와 IAM 조건키를 꽤 깊이 들여다봤습니다. 그 시간이 낭비였다고 생각하지는 않습니다.
다만 "인증서가 정상적으로 발급된 다음에는 무엇이 깨지는가"를 먼저 물었다면 순서가 달라졌을 것입니다. 그 질문 하나로 세션 문제가 드러났고, 세션 문제를 알고 나니 목적 확인이 왜 먼저인지도 분명해졌습니다.
결국 인프라 작업에서 가장 먼저 해야 할 일은 설정을 여는 것이 아니라 무엇을 위해 이걸 하는지 확인하는 것이라는 생각이 들었습니다. 기술 선택지는 대개 이미 존재하고 문서도 잘 되어 있습니다. 틀리기 쉬운 쪽은 언제나 "그래서 우리가 원하는 게 뭐였지"입니다.
관련 글
와일드카드가 못 덮는 것 — Traefik DNS-01과 Caddy on-demand TLS를 가른 지점
와일드카드 인증서를 손에 넣고 나서야 그것이 한 단계밖에 못 덮는다는 사실이 문제가 됐습니다. 고객사 소유 도메인을 받으려면 애초에 다른 메커니즘이 필요했고, 그 판단이 프록시 선택 자체를 다시 보게 만들었습니다.
와일드카드 인증서는 HTTP-01으로 못 받습니다 — DNS-01이 강제하는 IAM 결정
멀티테넌트 서브도메인을 위해 와일드카드 인증서를 발급하려다 알게 된 것은, 문제가 TLS가 아니라 DNS 쓰기 권한이라는 점이었습니다. 프록시에 Route53 권한을 주는 순간 생기는 폭발 반경과 그것을 좁히는 방법을 정리했습니다.
무중단 서버 이전: DNS 컷오버와 Let's Encrypt 타이밍 함정
이미지를 옮기고 DNS만 바꾸면 끝일 줄 알았는데, Let's Encrypt 인증서가 발급되지 않았습니다. 무중단 이전의 설계와, TTL을 미리 낮추지 않아 인증서 발급이 지연된 함정을 로그와 함께 정리했습니다.