호스트 기준 커스텀 도메인 라우팅
이 회고는 Custom Domain White-Label Routing Incident의 2026년 7월 13일 후속 작업을 다룹니다. 7월 12일 수정은 canonical landing leakage를 막았지만, 브라우저에 보이는 custom-domain 경험에는 여전히 https://joossameng.com/joossameng?auth=login 같은 canonical tenant path가 노출될 수 있었습니다.
후속 작업의 목표는 custom domain을 진짜 host-native 모델로 만드는 것이었습니다. joossameng.com에서는 tenant identity가 path가 아니라 host에서 결정됩니다.
배경
Lumie에는 두 가지 유효한 public URL 모델이 있습니다.
| URL 모델 | 예시 | 의미 |
|---|---|---|
| canonical tenant path | https://lumie-edu.com/joossameng | 첫 path segment가 tenant identity입니다 |
| white-label host | https://joossameng.com/ | host가 tenant identity입니다 |
7월 12일 사고 대응은 먼저 안전 문제를 고쳤습니다. 알 수 없거나 resolve되지 않은 custom domain이 canonical Lumie marketing content로 빠지지 않게 했습니다. 하지만 그 뒤에도 UX와 라우팅 모델 문제가 남았습니다. 설정된 custom domain에서 login 또는 auth refresh 이후 canonical tenant path가 보일 수 있었습니다.
현재 source surface는 다음입니다.
lumie-frontend/proxy.tslumie-frontend/src/shared/lib/serverCustomDomain.tslumie-frontend/app/page.tsxlumie-frontend/app/[customId]/page.tsxlumie-frontend/src/shared/lib/middlewareCustomDomain.test.ts
영향
이 후속 작업은 새로운 data leak을 고친 것이 아니라 branding과 routing boundary의 빈틈을 고친 것입니다.
- custom-domain 사용자가 브라우저에서 내부 canonical path shape를 볼 수 있었습니다.
?auth=login같은 auth URL은 host root에 남아야 했습니다.- Middleware test만으로는 충분하지 않았습니다. Rendering은 middleware, server component, auth state를 가로질렀습니다.
- Git에 fix가 있다는 사실이 아니라, 새 image가 실제 deployment에 반영됐는지를 검증해야 했습니다.
원본 기록에서 확인된 tenant는 joossameng입니다.
변경 내용
Host-native root rendering
lumie-frontend/app/page.tsx는 이제 trusted custom-domain header를 사용해 /가 tenant landing을 렌더링할지 canonical Lumie marketing page를 렌더링할지 결정합니다.
결과는 다음과 같습니다.
| 요청 | 기대 동작 |
|---|---|
https://joossameng.com/ | root에서 Joossameng tenant landing을 렌더링합니다 |
https://joossameng.com/?auth=login | /joossameng를 붙이지 않고 login flow를 엽니다 |
https://lumie-edu.com/ | canonical Lumie marketing home을 렌더링합니다 |
https://lumie-edu.com/joossameng | canonical tenant landing을 렌더링합니다 |
Custom host에서 tenant path 차단
lumie-frontend/app/[customId]/page.tsx는 canonical host용 tenant route로 남습니다. Proxy는 설정된 custom domain에서 /${customId}를 404로 막습니다. 따라서 custom host는 더 이상 canonical path model의 브라우저 표시 alias처럼 동작하지 않습니다.
불변조건은 다음입니다.
configured custom domain + tenant path == route-model leak
올바른 custom-domain URL은 tenant identity를 host에 둡니다.
Trusted header로 middleware와 server component 연결
Middleware는 spoof 가능한 tenant header를 제거하고, configured host map으로 domain을 resolve한 뒤에만 trusted 값을 주입합니다. Server component는 serverCustomDomain.ts를 통해 이 trusted header를 읽습니다.
중요한 경계는 server component가 runtime by-domain lookup을 다시 실행하지 않는다는 점입니다. Server component는 middleware routing decision의 결과를 소비합니다.
검증 게이트
이 후속 작업은 구현만큼이나 검증이 중요했습니다.
Unit boundary
lumie-frontend/src/shared/lib/middlewareCustomDomain.test.ts는 middleware 계약을 고정합니다.
- configured custom-domain root는 canonical rewrite 없이 통과합니다.
/login은/?auth=login으로 redirect됩니다.- configured custom domain에서
/auth/refresh가 허용됩니다. - configured custom domain에서
/${customId}가 차단됩니다. - resolve되지 않은 custom-domain root는 fail-closed로 처리됩니다.
Rendering boundary
Rendering 계약은 여러 파일을 가로지릅니다.
proxy.ts
-> trusted custom-domain headers
-> serverCustomDomain.ts
-> app/page.tsx
-> tenant landing or canonical marketing
그래서 middleware test가 통과해도 browser-level smoke test가 필요했습니다.
Deployment boundary
원본 사건 기록은 또 다른 검증 함정도 지적합니다. Merge된 code는 deployed code가 아닙니다. Production 동작은 build된 image가 deployment에 도달하고 running pod가 그 tag를 반영한 뒤에야 바뀝니다.
라우팅 수정에는 두 검증이 모두 필요합니다.
- proxy와 rendering decision에 대한 source-level test
- rollout 이후 실제 host에 대한 production smoke test
잘된 점
- 안전 수정과 UX/routing-model 수정을 분리했습니다.
- 최종 모델은 custom domain에서 host를 identity로 사용하므로 path alias보다 이해하기 쉽습니다.
- 이전에 regression이 났던 middleware case들이 test로 고정됐습니다.
- commit만 믿지 않고 deployment state를 명시적으로 확인했습니다.
아쉬웠던 점
- 첫 fix 이후에도 사용자에게 보이는 tenant-path leakage가 남았습니다.
- Middleware-level verification만으로 server-component rendering까지 덮지 못했습니다.
- Auth와 refresh flow가 별도의 custom-domain URL shape로 다시 발견돼야 했습니다.
- 일부 production uncertainty는 code behavior가 아니라 image rollout timing에서 왔습니다.
지속 불변조건
- Configured custom domain에서는 host가 tenant identity입니다.
- Canonical tenant path는 canonical host에서만 유효하고 custom host에서는 유효하지 않습니다.
- Auth, admin, dashboard, API, refresh flow는 매 요청마다 runtime lookup에 의존하지 않고 trusted tenant context를 보존해야 합니다.
- 알 수 없는 custom-domain host는 fail-closed 해야 하며 canonical Lumie marketing content를 렌더링하면 안 됩니다.
- Host-native root rendering은 middleware, server component, auth flow를 가로지르므로 browser-level verification이 필요합니다.
액션 아이템
| 작업 | 소유 영역 | 상태 | 증거 |
|---|---|---|---|
| Root, login, refresh, tenant path, unresolved host case를 다루는 custom-domain route test를 유지합니다. | lumie-frontend/src/shared/lib/middlewareCustomDomain.test.ts | 완료 / 유지 | 원본 기록에 test surface가 명시되어 있습니다. |
| 라우팅 배포 후 실제 custom host를 smoke test합니 다. | frontend rollout process | 향후 변경 필수 | 사건 기록에 deployment-state ambiguity가 있었습니다. |
| Server component가 임의의 by-domain lookup이 아니라 trusted middleware header에 의존하도록 유지합니다. | serverCustomDomain.ts, app/page.tsx | 지속 불변조건 | Host-native rendering은 이 분리에 의존합니다. |