# Homelab Topology & Service Routing

Three-tier Traefik architecture — browser → service request flow, all hosts and networks, all routed services. State as of 2026-07-06 (post-grizzley-replacement migration complete).

## 1. Physical Hosts

### Public Edge

| Host | IP | Role |
|------|-----|------|
| **Internet / Public DNS** | `47.156.104.83` (apex tophermayor.com) | Cloudflare authoritative DNS for `*.tophermayor.com` |
| **UCG Ultra** | LAN `192.168.1.1`, gateway `192.168.50.1` | UniFi OS 5.1.x · Network App 10.4.57 · 3 internal VLANs (10, 30, 50) · LAN DNS resolver + DHCP + DNS Policies |

### Tier 1 — Public Traefik Ingress (LXC)

| Host | IP | Role |
|------|-----|------|
| **CT 102 traefik-lxc** | `192.168.50.115` (LAN) · public IP via UCG Ultra | Debian 13 LXC · `traefik:v3.x` systemd service · 4 entrypoints: `web:80`, `websecure:443`, `sshclone:2222`, `metrics:8080` · ~70 HTTP routers + 1 TCP router |

### Tier 2 — Forwarder Layer

| Host | IP | Role |
|------|-----|------|
| **VM 9003 ubuntu** | `192.168.50.61` | Ubuntu 26.04 on PVE · own Traefik (public ingress) · file-provider forwarders (priority 250) · **12 non-migrated containers** + postgres-shared (kept for 30-day rollback until 2026-08-06) |

### Tier 3 — New Primary Compute

| Host | IP | Role |
|------|-----|------|
| **Grizzley (NEW)** | `192.168.50.144` (eth0) · `192.168.10.145` (wlan0, VLAN 10) | Raspberry Pi 5 · Ubuntu 26.04 LTS (Resolute Raccoon, arm64) · Docker 29.6.1 + Compose 5.3.0 · bare Traefik 3.0.0 (systemd) · NOPASSWD sudo for `bear` |

### Tier 0 — Storage & Other Compute

| Host | IP | Role |
|------|-----|------|
| **TrueNAS** | `192.168.50.12` | NFS server · mediadata, backups, traefik-certs |
| **Ice** | `192.168.50.197` | Pi 4 · Ubuntu 26.04 · Immich + traefik-ice + jump host to PVE |
| **Panda** | `192.168.30.196` (VLAN 30) | Home Assistant OS (smart home control plane) |
| **PVE hypervisor** | `192.168.50.11` | Proxmox VE · runs CT 102, CT 104, CT 200, VM 9003 (ubuntu) |
| **CT 104 (this machine)** | `192.168.50.x` | Where the docs webapp is hosted · LXC container on PVE · Hermes default profile |
| **CT 200 media-lxc** | `192.168.50.200` | 6c/16GB · GPU-passthrough GTX 1080 · Jellyfin (29.7x HEVC 1080p transcode) |

## 2. Networks & VLANs

| Network | Range | Notes |
|---------|-------|-------|
| Production (VLAN 50) | `192.168.50.0/24` | gateway `192.168.50.1` · 7 clients (truenas, traefik-CT102, termix-CT126, grizzley, hermes-pve-CT104, VectorPro, CT200) |
| Family / WiFi (VLAN 10) | `192.168.10.0/24` | grizzley wlan0 `.145` lives here |
| Home Assistant (VLAN 30) | `192.168.30.0/24` | panda `.196` |
| DMZ / Staging (VLAN 40) | `192.168.40.0/24` | |
| Mgmt / UGC Ultra (VLAN 1) | `192.168.1.0/24` | UniFi controller at `.1` |
| docker bridge (proxy-net) | `172.19.0.0/16` | grizzley containers & postgres-shared |

## 3. Three-Tier Traefik — Static + Dynamic Config Map

### Tier 1 — CT 102 (public Traefik)

```
/etc/traefik/traefik.yml (STATIC)
  entryPoints:
    web:80         → redirect→websecure
    websecure:443  → websecure routers
    sshclone:2222  → TCP forwarder → gitea
    metrics:8080
/etc/traefik/dynamic/ (file-provider, hot-reload)
  ├── conf.yml                  — wildcard cert + dashboards
  ├── ubuntu.yml                — Host(ubuntu.tophermayor.com) → 192.168.50.61 (legacy)
  ├── gitea-ssh-forwarder.yml   — TCP :2222 → 192.168.50.144:2222
  ├── grizzley-backup-routers.yml
  ├── splash-fallbacks.yml      — priority-1 fallbacks
  ├── media-lxc.yml, pve.yml, truenas.yml, termix.yml, ... — 20+ more
  └── 95 DNS policies via UniFi integration v1
```

### Tier 2 — ubuntu Traefik (FORWARDER ONLY post-migration)

```
/etc/traefik/dynamic/
  ├── grizzley-auth-forwarder.yml         — Host(auth.tophermayor.com) + PathPrefix(/outpost) → grizzley:9000
  ├── grizzley-migrated-forwarder.yml     — gitea/vaultwarden/navidrome/slskd/homepage → grizzley
  ├── upstream-ingress.yml                — homepage public routes (repointed to .144)
  └── 12+ other files for ubuntu-local services
```

### Tier 3 — grizzley bare Traefik (systemd, NOT docker)

```
/etc/traefik/traefik.yml (STATIC)
  entryPoints:
    websecure:8443 → app routers
    metrics:8080   (off by default)
/etc/traefik/dynamic/
  ├── authentik.yml — Host(auth.tophermayor.com) → :9000 + PathPrefix(/outpost) → :9000
  └── (each app's compose has its own service exposure via ports:, NOT via traefik labels)
```

Auth: `CF_DNS_API_TOKEN` via `/etc/traefik/dns-token.env` (chmod 600).
Cert: Let's Encrypt DNS-01 via Cloudflare (one cert for `auth.tophermayor.com`).

## 4. Request Flow — Browser → Service

### 4a. Generic Public Request (e.g. `https://gitea.tophermayor.com`)

```
[Step 1 — DNS resolution (client side)]
Browser/curl → Cloudflare DNS → gitea.tophermayor.com → tophermayor.com apex
            → A 47.156.104.83 (public IP)

[Step 2 — TCP/TLS handshake + HTTP request (Tier 1)]
Browser → UCG Ultra → CT 102 traefik-lxc :443 (websecure entrypoint)
     → TLS SNI=gitea.tophermayor.com → wildcard cert from acme.json
     → dynamic/ubuntu.yml: Host(gitea.tophermayor.com) → ubuntu-traefik svc (priority 100)

[Step 3 — Forwarder (Tier 2)]
ubuntu Traefik :443 → dynamic/grizzley-migrated-forwarder.yml: grizzley-gitea (priority 250)
                → svc gitea-grizzley @ http://192.168.50.144:3000

[Step 4 — Backend (Tier 3)]
grizzley .144:3000 → gitea container (port 3000)
```

> **Note**: Each service may take additional hops. For `auth.tophermayor.com`, the backend is `:9000` (Authentik). For OIDC forwardAuth on OIDC-protected services, the forwardAuth call hops: CT 102 → ubuntu → grizzley :9000 → outpost.goauthentik.io path.

### 4b. SSH-clone Request (e.g. `git clone gitea.tophermayor.com:2222`)

```
client → Cloudflare → 47.156.104.83:2222
      → CT 102 traefik :2222 (sshclone entrypoint, TCP)
      → dynamic/gitea-ssh-forwarder.yml: tcp.routers.gitea-ssh-public
      → svc gitea-ssh @ 192.168.50.144:2222
      → gitea sshd (port 22 in container)
```

### 4c. OIDC ForwardAuth on gitea

```
browser → CT 102 :443
       → router: Host(gitea.tophermayor.com) middlewares=[forwardAuth,local-only]
       → forwardAuth → http://192.168.50.144:9000/outpost.goauthentik.io/auth/traefik
         (hops ubuntu .61 forwarder)
       → Authentik :9000 checks session, returns 302 to /application/o/authorize/
       → If unauthed: 302 → auth.tophermayor.com/application/o/authorize/?...
       → User logs in via Authentik → cookie set → gitea request succeeds
```

### 4d. LAN-only request (e.g. `https://gitea.local.tophermayor.com`)

```
LAN client → UCG Ultra LAN DNS (192.168.50.1)
          → A 47.156.104.83 (or direct to 192.168.50.115 if UniFi has split-horizon)
          → CT 102 :443
          → router: Host(gitea.local.tophermayor.com) middlewares=[local-only,forwardAuth]
          → Same as 4a from Tier 2 onward
```

## 5. Per-Service Routing Table

| Hostname | Tier 1 (CT 102) | Tier 2 (ubuntu forwarder) | Tier 3 (grizzley) | Backend | Auth |
|----------|------------------|---------------------------|-------------------|---------|------|
| `gitea.tophermayor.com` | ubuntu.yml: gitea-public (100) | grizzley-migrated-forwarder: grizzley-gitea (250) | — (direct port) | `192.168.50.144:3000` (gitea) | forwardAuth → auth.tophermayor.com |
| `gitea.tophermayor.com:2222` | TCP: gitea-ssh-public (200) | — (passthrough) | — (direct port) | `192.168.50.144:2222` (gitea sshd) | SSH key (gitea user account) |
| `vaultwarden.tophermayor.com` | ubuntu.yml: vaultwarden-public (200) | grizzley-migrated-forwarder: grizzley-vaultwarden (250) | — (direct port) | `192.168.50.144:80` (vaultwarden) | forwardAuth (cookie session) |
| `navidrome.tophermayor.com` | ubuntu.yml: navidrome-public (200) | grizzley-migrated-forwarder: grizzley-navidrome (250) | — (direct port) | `192.168.50.144:4533` (navidrome) | navidrome native session |
| `slskd.local.tophermayor.com` | ubuntu.yml: sldk-local (200) | grizzley-migrated-forwarder: grizzley-slskd (250) | — (direct port) | `192.168.50.144:5030` (slskd) | slskd native session |
| `homepage.tophermayor.com` | upstream-ingress.yml: homepage (100) | — (bypassed, Tier 2→Tier 3 direct) | — (direct port) | `192.168.50.144:3003` (homepage) | forwardAuth |
| `homepage.local.tophermayor.com` | upstream-ingress.yml: homepage-local (100) | — (direct) | — (direct port) | `192.168.50.144:3003` | local-only + forwardAuth |
| `homepage-grizzley.local.tophermayor.com` | upstream-ingress.yml: homepage-grizzley (250) | grizzley-migrated-forwarder: grizzley-homepage-grizzley (250) | — (direct port) | `192.168.50.144:3003` | forwardAuth |
| `auth.tophermayor.com` | — (CT 102 passes through) | grizzley-auth-forwarder: grizzley-authentik (100/2001) | — (direct port) | `192.168.50.144:9000` (authentik) | Authentik native login |
| `auth.tophermayor.com/outpost.goauthentik.io/*` | — | grizzley-auth-forwarder: outpost router (2001) | — (direct port) | `192.168.50.144:9000` | — (OIDC forwardAuth endpoint) |
| `immich.tophermayor.com` | — | immich route on ubuntu | — (still on ubuntu) | `192.168.50.61:2283` (immich) | forwardAuth (immich OIDC) |
| `jellyfin.tophermayor.com` | ubuntu.yml: jellyfin-public (200) | — (still on ubuntu) | — | `192.168.50.200:8096` (jellyfin, media-lxc) | jellyfin native |
| `traefik-lxc.lan` | — (own host) | — | — | `192.168.50.115:8080` (metrics) | basic-auth |
| `media-lxc.lan` | media-lxc.yml routes | — (still on media-lxc, .200) | — | `192.168.50.200` | varies |

## 6. DNS Layer Details

### Cloudflare (public)

Authoritative for `tophermayor.com`. 70 records:

- 3 A-records at public IP `47.156.104.83`: `opencode`, `termix`, apex `tophermayor.com`
- 10 CNAMEs → apex (auth, cloud, gitea, immich, jellyfin, jellyseerr, jfa, minecraft, n8n, vault)
- ~57 TXT records (`_acme-challenge.*` for cert validation, DKIM, etc.)
- `navidrome.tophermayor.com` added 2026-07-06 (was missing — caused the public 502)

### UniFi DNS Policies (LAN)

Authoritative for `{host}.lan` zones. 95 policies total.

| Record | Old → New | Notes |
|--------|-----------|-------|
| `grizzley.lan` | `.84` → `.144` | Stale fixed 2026-07-06 |
| `ct102.lan` | (missing) → `.115` | Added 2026-07-06 |
| `ubuntu.lan`, `ice.lan`, `pve.lan`, `truenas.lan`, `media-lxc.lan` | already correct | Verified 2026-07-06 |
| `*.local.tophermayor.com` (14 service hostnames) | `.84` → `.144` | aiomanager, aiometadata, aiostreams, komodo, metrics-grizzley, minecraft*, oc-grizz, reccollection, starfield, status, traefik-grizzley, traefik-wildcard |

## 7. Migration State

Phase A (Authentik) and Phase B (postgres-shared + 5 apps) complete on 2026-07-06. New grizzley (`.144`) is the new primary. Public Traefik ingress unchanged: CT 102 → ubuntu (file-provider forwarders) → grizzley (`:8443` LAN-side). 30-day rollback window: ubuntu containers stopped, data preserved on disk, postgres-shared still running until 2026-08-06.

> **2 stale records intentionally NOT changed**: `*.pi.tophermayor.com → .84` (wildcard, separate host) and `oc-ice.local.tophermayor.com → .84` (OpenCode on ice, not grizzley).
