| index c957f1fcd..ca4ad69d9 100644 |
| --- a/internal/console/apidomains.go |
| +++ b/internal/console/apidomains.go |
| @@ -43,25 +43,37 @@ import ( |
| ) |
| |
| // deniedDomains is the deny list every attach is checked against: the |
| -// operator's own CONSOLE_DENY_DOMAINS entries, plus the two names this |
| +// operator's own CONSOLE_DENY_DOMAINS entries, plus the three names this |
| // console can derive without being told (spec §5: "all derived from |
| // console/deployment config, not hardcoded"). |
| // |
| -// The derived two are not optional and not the operator's to forget. The |
| -// ORIGIN host is the console itself — an alias there would point the door's |
| -// own hostname at a tenant's route. The APPS DOMAIN is the whole platform |
| -// apex namespace (<app>.<sqid>.<apps-domain>): every app's public URL lives |
| -// under it, and an alias claim there would be one tenant claiming another |
| -// tenant's address — the same sibling-apex hazard handleAPIInstanceCreate |
| -// refuses within an account, here refused across the deployment. Hosts under |
| -// the apps domain are what `carlos instances` exists for; this door is for |
| -// hostnames the caller's own DNS controls. |
| +// The derived three are not optional and not the operator's to forget: |
| // |
| -// Parsed per call rather than cached at boot: it is one url.Parse of a short |
| -// string on a door that then does bucket I/O, and it stays honest for an |
| -// embedder (or a test) that sets Origin after NewServer. |
| +// - the ORIGIN host is the console itself — an alias there would point the |
| +// door's own hostname at a tenant's route; |
| +// - the APPS DOMAIN is the whole platform apex namespace |
| +// (<app>.<sqid>.<apps-domain>): every app's public URL lives under it, |
| +// and a claim there would be one tenant claiming another tenant's |
| +// address — the sibling-apex hazard handleAPIInstanceCreate refuses |
| +// within an account, refused here across the deployment. Hosts under the |
| +// apps domain are what `carlos instances` exists for; this door is for |
| +// hostnames the caller's own DNS controls; |
| +// - the REDIRECT HOSTS are retired console origins (CONSOLE_REDIRECT_HOSTS |
| +// — tito's carlos.tito.io), which this console still answers on and |
| +// 301s to Origin. They are guaranteed distinct from the origin host |
| +// (NewServer refuses a self-redirect), so nothing else in this list |
| +// covers them, and a tenant claiming one would be claiming a console |
| +// address that operators, bookmarks and old CLI credentials still reach. |
| +// Route-wins ordering on a box is not the answer: it is per-box, and |
| +// stops being protection the moment a second box materializes the alias. |
| +// |
| +// Derived per call rather than cached at boot — one url.Parse and a short |
| +// copy on a door that then does bucket I/O — so it stays honest for an |
| +// embedder (or a test) that sets any of these after NewServer. The redirect |
| +// entries go through the same normalizeHost ServeHTTP's retired-host check |
| +// uses, so the two readings of CONSOLE_REDIRECT_HOSTS cannot disagree. |
| func (s *Server) deniedDomains() []string { |
| - deny := make([]string, 0, len(s.cfg.DenyDomains)+2) |
| + deny := make([]string, 0, len(s.cfg.DenyDomains)+len(s.cfg.RedirectHosts)+2) |
| deny = append(deny, s.cfg.DenyDomains...) |
| if u, err := url.Parse(s.cfg.Origin); err == nil { |
| if h := strings.ToLower(u.Hostname()); h != "" { |
| @@ -71,6 +83,11 @@ func (s *Server) deniedDomains() []string { |
| if d := strings.ToLower(strings.TrimSpace(s.cfg.AppsDomain)); d != "" { |
| deny = append(deny, d) |
| } |
| + for _, h := range s.cfg.RedirectHosts { |
| + if h = normalizeHost(h); h != "" { |
| + deny = append(deny, h) |
| + } |
| + } |
| return deny |
| } |
| |