279 lines
12 KiB
Markdown
279 lines
12 KiB
Markdown
|
|
# 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.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 = <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:
|
||
|
|
```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 <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):**
|
||
|
|
```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 `<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`):
|
||
|
|
```text
|
||
|
|
local-zone: "tappaas.makerfloss.eu." redirect
|
||
|
|
local-data: "tappaas.makerfloss.eu. 300 IN A <caddy-local-ip>"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Example — dnsmasq:**
|
||
|
|
```text
|
||
|
|
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).
|
||
|
|
|
||
|
|
```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.
|
||
|
|
</content>
|