MakerFLOSS_Troubleshooting/docs/superpowers/plans/2026-06-28-tappaas-side-publishing.md
sjat 8a5966e367 plan: TaPPaaS-side publishing (sendable, self-contained)
Standalone plan for the TaPPaaS operator's Claude Code: WireGuard client
(peer 10.13.0.9, split-tunnel), Caddy plain-HTTP backend on 10.13.0.9:80,
firewall lock to 10.13.0.1, internal split-horizon DNS. Bakes in the
verified VPS-side contract (hub endpoint/pubkey, preserved Host, *.tappaas
wildcard, public DNS) and the key-exchange handshake. Flags the internal-TLS
decision (internal CA vs Gandi DNS-01 vs no internal TLS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 13:47:38 +02:00

12 KiB

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://<svc>.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.euA 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)

  1. Key exchange (do first — it gates everything).
  2. WireGuard client on the FLOSSFirewall (peer 10.13.0.9, split-tunnel).
  3. Caddy plain-HTTP backend on 10.13.0.9:80 (routes by Host, redirect OFF).
  4. Firewall allowing tcp/80 to the wg interface only from 10.13.0.1.
  5. 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):

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):

[Interface]
Address = 10.13.0.9/32
PrivateKey = <contents of /etc/wireguard/wg1.key from Task 0>
# 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:

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):

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):

# --- 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 <whoami-upstream-host>:<port>
}

http://forgejo.tappaas.makerfloss.eu {
    bind 10.13.0.9
    reverse_proxy <forgejo-upstream-host>:<port>
}

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):

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):

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 <svc>.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):

local-zone: "tappaas.makerfloss.eu." redirect
local-data: "tappaas.makerfloss.eu. 300 IN A <caddy-local-ip>"

Example — dnsmasq:

address=/tappaas.makerfloss.eu/<caddy-local-ip>

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 <svc>.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).

# 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.