# TaPPaaS-side Publishing — Implementation Plan (for the TaPPaaS operator) > **For agentic workers (Claude Code on the TaPPaaS side):** this plan is > self-contained. The VPS edge is **already built, deployed, and verified** — > your job is the TaPPaaS/FLOSSFirewall end so the two meet. Every fact about > the VPS side below is live and confirmed (2026-06-28). Work in the TaPPaaS > infra/config repo (whatever holds the FLOSSFirewall WireGuard, Caddy, firewall > and DNS config). Ask the operator when a value is environment-specific > (upstream addresses, interface names, the local Caddy IP). **Goal:** Make `https://.tappaas.makerfloss.eu` serve real TaPPaaS services from anywhere, and give internal clients a local (no-VPS-round-trip) path to the same names (split-horizon DNS). **How it works (the pattern):** TLS for external clients terminates on the **makerfloss VPS**. The VPS forwards every `*.tappaas.makerfloss.eu` request as **plain HTTP, original Host preserved**, over a **WireGuard tunnel (`wg1`)** to the FLOSSFirewall at **`10.13.0.9:80`**, where **Caddy** routes by Host to the service. (This mirrors the already-running `mf01` setup.) --- ## The contract — what the VPS already provides (live & verified) You are building the other end of these. Do **not** change them; rely on them. | Fact | Value | |---|---| | Public DNS (live) | `tappaas.makerfloss.eu` and `*.tappaas.makerfloss.eu` → **A `88.99.32.236`** | | External TLS | Terminates at the VPS. Wildcard cert `*.tappaas.makerfloss.eu` already issued (Gandi DNS-01). **You do NOT need a public cert for the external path.** | | What the VPS sends you | Plain **HTTP** to **`10.13.0.9:80`**, over `wg1`, with the **original `Host` header preserved** (so Caddy sees `whoami.tappaas.makerfloss.eu`, etc.) | | Current state of that route | Returns **HTTP 502** today, because `10.13.0.9:80` isn't answering yet. Your work makes it answer → 200. | | WireGuard hub (the VPS) | Endpoint **`makerfloss.eu:51820`** (UDP) · hub overlay IP **`10.13.0.1`** · overlay subnet **`10.13.0.0/24`** · **hub public key `mtx4bxTq5KvQmFPRubRBrVoWL6WDUrll9LWLhOMSlCQ=`** | | Your assigned peer overlay IP | **`10.13.0.9/32`** | > A placeholder peer public key is currently registered on the VPS for > `10.13.0.9`. It will be **replaced by yours** in the key-exchange step below — > the tunnel won't complete a handshake until that swap happens. --- ## What you will build (4 parts + 1 handshake) 0. **Key exchange** (do first — it gates everything). 1. **WireGuard client** on the FLOSSFirewall (peer `10.13.0.9`, split-tunnel). 2. **Caddy plain-HTTP backend** on `10.13.0.9:80` (routes by Host, redirect OFF). 3. **Firewall** allowing `tcp/80` to the wg interface **only from `10.13.0.1`**. 4. **Internal split-horizon DNS** for `*.tappaas.makerfloss.eu`. --- ## Task 0 — WireGuard key exchange (do this first) WireGuard needs a public-key swap; this is the one round-trip with the VPS owner (sjat). Private keys never leave the FLOSSFirewall. **Steps (on the FLOSSFirewall, or in your WG GUI):** ```bash umask 077 wg genkey | tee /etc/wireguard/wg1.key | wg pubkey | tee /etc/wireguard/wg1.pub # Keep wg1.key secret (it goes only into your WG config, never committed). cat /etc/wireguard/wg1.pub # <-- send THIS public key to sjat ``` **Send sjat:** the contents of `wg1.pub` (a 44-char base64 string ending `=`) and confirm your peer IP is `10.13.0.9`. **sjat then** replaces the peer public key on the VPS (`vault_wireguard_makerfloss_peers.flossfw.public_key`) and re-runs the WireGuard server play. After that the handshake can complete. If your FLOSSFirewall is OPNsense/pfSense, generate the keypair in the WireGuard GUI instead and copy out the public key the same way. --- ## Task 1 — WireGuard client (peer `10.13.0.9`, split-tunnel) **Requirement:** an interface that holds `10.13.0.9/32`, peers with the VPS hub, and routes **only** the overlay subnet — never default traffic (isolation: nothing from TaPPaaS should egress via the VPS). **Example `wg-quick` config — `/etc/wireguard/wg1.conf`** (adapt to your stack): ```ini [Interface] Address = 10.13.0.9/32 PrivateKey = # No DNS=, no default route — split tunnel. [Peer] PublicKey = mtx4bxTq5KvQmFPRubRBrVoWL6WDUrll9LWLhOMSlCQ= Endpoint = makerfloss.eu:51820 AllowedIPs = 10.13.0.0/24 PersistentKeepalive = 25 ``` Bring up and enable on boot: ```bash wg-quick up wg1 systemctl enable wg-quick@wg1 # systemd hosts ``` **OPNsense/pfSense equivalent:** local tunnel address `10.13.0.9/32`; peer endpoint `makerfloss.eu:51820`; peer public key `mtx4bxTq5KvQmFPRubRBrVoWL6WDUrll9LWLhOMSlCQ=`; allowed IPs `10.13.0.0/24`; keepalive `25`. Do **not** set it as a gateway / default route. **Verify (after sjat has swapped your key):** ```bash wg show wg1 # latest handshake is recent; transfer > 0 ping -c3 10.13.0.1 # the VPS hub answers over the overlay ``` If there is no handshake: confirm sjat swapped your public key; confirm UDP `51820` egress to `makerfloss.eu` isn't blocked by an upstream firewall. --- ## Task 2 — Caddy plain-HTTP backend on `10.13.0.9:80` **Requirement:** Caddy must serve **plain HTTP on `10.13.0.9:80`**, route by the `Host` the VPS preserved, and **NOT redirect to HTTPS** on this listener (the VPS already did TLS; a redirect here causes a loop — this is the one gotcha the `mf01` setup hit). Your existing internal `:443` serving stays unchanged; you are only **adding** the wg-bound HTTP backend. **Why `http://` in the site address:** in Caddy v2 an `http://` site address disables Automatic HTTPS for that site and serves on port 80 — exactly the no-cert, no-redirect behavior needed. `bind 10.13.0.9` pins it to the wg interface. **Example Caddyfile — one block per published service** (adapt upstreams): ```caddy # --- VPS edge backend: plain HTTP on the wg interface only --- # Repeat one block per service you want published publicly. http://whoami.tappaas.makerfloss.eu { bind 10.13.0.9 reverse_proxy : } http://forgejo.tappaas.makerfloss.eu { bind 10.13.0.9 reverse_proxy : } ``` Notes: - **Exposure is opt-in here.** Only hostnames you give a block become reachable publicly; the VPS forwards the whole wildcard, but unmatched hosts just get a Caddy 404. Add a block to publish a service; remove it to unpublish. - `bind 10.13.0.9` requires the `wg1` interface to exist before Caddy starts. Order Caddy after `wg-quick@wg1`, **or** bind to `0.0.0.0` on `:80` and rely on the Task 3 firewall to restrict it to `10.13.0.1` (simpler startup, firewall does the isolation). - Point `reverse_proxy` at the same upstreams your internal `:443` blocks already use for these services. **Verify locally (on the FLOSSFirewall):** ```bash curl -s -o /dev/null -w '%{http_code}\n' \ -H 'Host: whoami.tappaas.makerfloss.eu' http://10.13.0.9:80/ # Expect 200 (or whatever the service returns) — NOT a 3xx redirect. ``` --- ## Task 3 — Firewall: lock `:80` to the VPS **Requirement:** on the wg interface, allow **`tcp/80` only from `10.13.0.1`** (the VPS hub); block `:80` from anywhere else; ensure `:80` is not exposed on other interfaces. This is the isolation guarantee — the plain-HTTP backend is reachable solely by the VPS. **Example (nftables/iptables host firewall):** ```bash iptables -A INPUT -i wg1 -p tcp --dport 80 -s 10.13.0.1 -j ACCEPT iptables -A INPUT -i wg1 -p tcp --dport 80 -j DROP ``` **OPNsense/pfSense:** on the WireGuard interface, a pass rule — proto TCP, source `10.13.0.1`, destination `(this firewall)` port `80`; below it a block rule for TCP to port `80` on that interface. Confirm no other interface rule exposes `:80` to untrusted networks. **Verify:** the local `curl` in Task 2 (sourced from the host itself) still works; once end-to-end is up (Task 5), the request arrives from `10.13.0.1` and is allowed. --- ## Task 4 — Internal split-horizon DNS **Requirement:** internal clients (the TaPPaaS cluster now; the makerspace LAN later) resolving `.tappaas.makerfloss.eu` should get **Caddy's local IP**, so they reach Caddy directly and skip the VPS round-trip. External clients keep getting the VPS (public DNS already does that). Configure this on the FLOSSFirewall's resolver. **Example — unbound** (wildcard for the whole subdomain via `redirect`): ```text local-zone: "tappaas.makerfloss.eu." redirect local-data: "tappaas.makerfloss.eu. 300 IN A " ``` **Example — dnsmasq:** ```text address=/tappaas.makerfloss.eu/ ``` Serve this view to the **cluster** now. (Makerspace-LAN coverage is a later phase and depends on the OrangeMakers router — out of scope here.) > **Decision point — internal TLS.** Internal clients hitting Caddy directly will > get **Caddy's** certificate for `.tappaas.makerfloss.eu`, not the VPS's. > While the cluster is isolated, Caddy can obtain a publicly-trusted cert for > these names only via **DNS-01** (which means putting a Gandi DNS-write > credential on the FLOSSFirewall — the very thing the external-path design kept > *off* the makerspace). Choose one, with sjat: > 1. **Internal CA / `mkcert`** — issue an internal cert for `*.tappaas` and have > cluster clients trust that CA. Keeps Gandi creds off-site. *(Recommended.)* > 2. **Gandi DNS-01 on the FLOSSFirewall** — Caddy serves the real public > wildcard internally too; reintroduces the credential in the makerspace. > 3. **No internal TLS termination** — point internal DNS at the VPS as well > (drop split-horizon); simplest, but every internal request round-trips the > VPS. > > Tasks 1-3 do not depend on this; pick it before relying on the internal view. --- ## Task 5 — End-to-end verification Run after Tasks 0-3 are in place (Task 4 is for the internal view). ```bash # 1) Tunnel handshake (on the FLOSSFirewall) wg show wg1 # recent handshake, transfer > 0 ping -c3 10.13.0.1 # hub reachable # 2) External end-to-end (from anywhere on the internet) curl -s -o /dev/null -w '%{http_code}\n' https://whoami.tappaas.makerfloss.eu/ # Expect 200 (was 502 before your backend existed). TLS is the VPS's # *.tappaas wildcard and validates with no warning. # 3) Internal split-horizon (from a TaPPaaS cluster node, after Task 4) dig +short whoami.tappaas.makerfloss.eu # → Caddy's LOCAL IP, not 88.99.32.236 curl -sI https://whoami.tappaas.makerfloss.eu/ # served by Caddy directly ``` Report back to sjat once `(2)` returns 200 — that confirms the full path (VPS TLS → wg1 → Caddy → service) is live. --- ## Isolation requirements (must hold) - WireGuard is **split-tunnel** (`AllowedIPs = 10.13.0.0/24`, no default route) — TaPPaaS never egresses through the VPS, and nothing on `wg1` reaches the wider makerspace or the VPS owner's homelab. - The plain-HTTP backend is reachable **only** from `10.13.0.1` (Task 3). - The backend hop is plain HTTP but travels **inside** the WireGuard tunnel, so it is encrypted on the wire. - No public-DNS-write credential is required on the FLOSSFirewall for the external path (the VPS owns that). Only the internal-TLS option 2 above would reintroduce one — avoid it unless deliberately chosen. --- ## Reference - Already-running sibling pattern (the template this mirrors): `MakerFLOSS_Troubleshooting/runbooks/publishing-services-mf01.md`. - VPS-side design + the changes that produced the contract above: `MakerFLOSS_Troubleshooting/docs/superpowers/specs/2026-06-28-tappaas-vps-publishing-design.md` and `.../plans/2026-06-28-tappaas-vps-publishing.md`. - Questions about the VPS side / to do the key swap: contact sjat.