---
id: w7/m6/t002
title: Reserved / platform-host guard — reject base-domain apex, dashboard host, and foreign <app>.<base> hosts
worker: worker7
status: todo
estimate: 45m
depends_on: []
---

## Objective

Refuse custom domains that collide with platform-owned hostnames. Because the platform controls `BEX_BASE_DOMAIN` DNS, any `*.<base>` host already resolves to the shared ingress — so a tenant claiming `victim-app.<base>` (or the `<base>` apex, or the dashboard host) would pass cert-manager's ACME HTTP-01 challenge and get a **valid** cert, hijacking another App's platform subdomain. ACME cannot prevent this; a reserved-host predicate can. Reject: the base-domain apex, the dashboard host, and any `<app>.<base>` platform auto host that is not the requesting App's own.

## Context

- `lego/backend/internal/apps/domains.go:66-73` — `platformHost`/`BaseDomain`; the operator mints `<app>.<BEX_BASE_DOMAIN>` auto hosts.
- The requesting App's **own** auto host must stay allowed (adding your own `<app>.<base>` is a no-op, not a rejection).
- The dashboard host: derive from config if available (`BEX_DASHBOARD_URL` host) or treat any well-known platform host under `<base>` as reserved; keep the reserved set explicit and documented.
- Distinguish "under the platform base domain but not yours" (reject) from "an unrelated external domain" (allow, subject to t001 uniqueness).

## Steps

1. Add a reserved-host predicate: true for the `<base>` apex, the dashboard host, and `<x>.<base>` where `<x>` is not the requesting App's name.
2. Reject reserved hosts in `AddDomain` with a typed 400-class error; leave the App's own auto host and genuine external domains unaffected.
3. Cover the `BaseDomain`-unset case (no platform base ⇒ nothing reserved under it) without panicking.

## Files

- `lego/backend/internal/apps/domains.go` — reserved-host predicate + `AddDomain` rejection

## Acceptance criteria

- [ ] Adding the base-domain apex, the dashboard host, or a foreign `<app>.<base>` is rejected; the requesting App's own auto host and free external domains still succeed.
- [ ] With `BEX_BASE_DOMAIN` unset the guard is inert (no false rejections, no panic).

## Out of scope

- Cross-App uniqueness for arbitrary external domains (t001); adapter/dashboard surfacing (t003).
