SpipCP
Nodes

IPv4 edge proxy

Reach an IPv6-only origin from the IPv4 internet with your own self-hosted edge — a box that owns a public IPv4, terminates the TLS, and reverse-proxies to the v6 origin over IPv6. Your own equivalent of a Cloudflare proxy, no third party in the request path.

The most important promise first: a tenant site on its own dual-stack VPS never needs an edge. Its A record points straight at it. The edge is only for things that live on a box with no public IPv4 of its own — an IPv6-only node: the panel, the docs site, the rare IPv6-only tenant site. Everything below only matters for those.

Why this exists

IPv6-only nodes run the whole stack on a cheap "IPv6 Only" box with no public IPv4. That works perfectly for outbound traffic — NAT64/DNS64 lets the v6-only box reach the v4 internet (apt, GitHub, ACME, every upstream that's still v4-only).

But NAT64 is a one-way street. It solves egress (v6 box → v4 internet). It can never solve ingress (v4 client → v6 box), and nothing on the origin side can: if a client only speaks IPv4 and your origin only has an IPv6 address, there is simply no IPv4 address for the client to dial. A translator sitting next to the origin still can't hand a v4-only visitor an address. A meaningful slice of the internet is still IPv4-only — corporate networks, older mobile carriers, some home ISPs — and to them an IPv6-only panel, docs site, or tenant site is unreachable.

The edge proxy is the inbound-v4 lane. An edge is a small box that:

  1. owns a public IPv4 address,
  2. terminates the IPv4 (TLS) connection for a fronted hostname, then
  3. reverse-proxies to your IPv6-only origin over IPv6 (reverse_proxy https://[origin]:443).

It's the same idea as Cloudflare's orange-cloud proxy — but it's yours: your box, your Caddy, your cert, no third party in the request path. Sovereignty- and GDPR-first, consistent with the rest of SpipCP.

Control plane is not data plane

The edge fronts live tenant traffic and does not depend on the panel being up. Once a route is enabled, the edge boxes serve the fronted hostname from their own Caddy + cert — the panel being offline never stops a live site from serving.

A nameserver is not a proxy

A point worth nailing down, because it's easy to conflate: a nameserver never carries traffic. DNS answers the question "what IP is this name?" and then steps out of the path — the client connects to that IP directly. "Cloudflare DNS vs our PowerDNS" is the wrong axis for this problem; neither carries traffic.

Passthrough — actually relaying the request to a different machine — is a reverse proxy, a different thing entirely. That's the edge. So when we say the edge Caddy is co-located on the PowerDNS boxes, that's a deployment convenience (you already run those two boxes), not the nameserver "doing" the proxying. The nameserver resolves; the edge proxies; they happen to share a box.

Architecture

Two boxes, co-located on the nameservers you already run. A v4-only client resolves the fronted hostname to two A records (one per edge) and reaches the IPv6 origin through whichever edge answers; a v6-capable client can skip the edge entirely via the optional direct AAAA.

                IPv4-only client
                       │  resolves panel.example.com → A 203.0.113.71  (+ A 198.51.100.72)

        ┌──────────────────────────────┐        ┌──────────────────────────────┐
        │  EDGE 1  (role {nameserver,   │        │  EDGE 2  (role {nameserver,   │
        │          edge})              │        │          edge})              │
        │  public v4 203.0.113.71       │        │  public v4 198.51.100.72      │
        │  Caddy: terminate TLS (LE)    │        │  Caddy: terminate TLS (LE)    │
        └──────────────┬───────────────┘        └──────────────┬───────────────┘
                       │  reverse_proxy https://[2a01:4f8:panel::1]:443  (over IPv6)
                       └──────────────────┬───────────────────┘

                          ┌──────────────────────────────┐
                          │   IPv6-only ORIGIN            │
                          │   panel / docs / v6-only site │
                          │   AAAA 2a01:4f8:panel::1       │  (also published directly, optional)
                          └──────────────────────────────┘
  • Co-located on the nameservers. The two PowerDNS boxes you already run also run the edge Caddy. A node carries a set of roles, so one box is {nameserver, edge} at once — no new boxes, one small fleet, two redundant v4 front doors. An edge box hosts no tenant instances or sites (it carries no Incus / tenant network fabric, same as nameserver and storage roles).

  • Multi-A by default. For a fronted hostname, SpipCP publishes one A per assigned edge's public v4 — so with two edges, both A records land. Clients happy-eyeball across the set; if one edge is slow or down, the client tries the other. (Publishing a single A would make the second edge decorative.)

  • Direct AAAA fast-path (optional, on by default). When the origin is an IPv6 literal, the route also publishes hostname AAAA <origin v6>. A v6-capable client then reaches the origin directly and only IPv4-only clients traverse the edge. Toggle per route (publishDirectAaaa).

  • TLS terminates at the edge, re-terminates to the origin. The edge owns the public cert (Caddy + Let's Encrypt over its own v4). It dials the origin over HTTPS (reverse_proxy https://[origin]:443), so it's TLS end-to-end, not a blind TCP pass-through. The origin keeps its own internal cert. (TLS pass-through / SNI routing, where the origin owns the public cert, is out of scope for now.)

  • Health-checked failover. Each edge gets a heartbeat probe (an HTTP check on its own :443, reusing the standard probe runner — no parallel health sweep). When an edge fails enough consecutive checks, SpipCP automatically withdraws that edge's A from every route it serves, and re-adds it once it recovers. It never withdraws the last edge (that would pull the hostname entirely) — instead it surfaces a critical alert.

  • The per-origin enable switch. Each route has an enabled flag, default OFF. Creating a route changes nothing until you flip it on deliberately — the "I might get v4 on this box someday, I want the choice per hostname" toggle. Off = served however it was before (direct v6); On = fronted.

The edge routes list showing fronted hostnames, their v6 origins, assigned edges, and enable state
📷The edge routes list — each fronted hostname, its IPv6-only origin, the edges serving it, and whether the per-origin switch is on.img/dns-edge-routes.avif
The edge routes list — each fronted hostname, its IPv6-only origin, the edges serving it, and whether the per-origin switch is on.

Step by step

1. Enroll an edge node

Enroll the box with the edge role. Because the edge co-locates on a PowerDNS box, enroll it as {nameserver, edge} — it runs PowerDNS and the edge Caddy. An edge box:

  • builds on the shared hardened base only (UFW + fail2ban + sysctl + sshd) — no Incus, no tenant network fabric (it hosts no workloads);
  • installs the same official Caddy the standard node's edge uses (one Caddy install path in the codebase);
  • opens UFW :80 + :443 (the public front door — :80 for the ACME http-01 challenge and the :80→:443 redirect, :443 for the terminated TLS). Everything else stays default-deny;
  • is "done" only when Caddy is active and its admin API answers on loopback :2019 — the panel pushes each box's site config over that admin API (the same /load path standard nodes use; no SSH).

2. Create an edge route

Create a route mapping a fronted hostname → its IPv6-only origin:

FieldMeaning
hostnameThe public name a v4 client resolves (e.g. panel.example.com).
originHostThe IPv6-only origin: a v6 literal (2a01:4f8::1) or a hostname. A bare IPv4 literal is refused — a box with a v4 needs no edge.
originPortDefaults to 443.
publishDirectAaaaDefault on — also publish the direct v6 fast-path.

A new route is always created disabled — nothing changes in DNS or Caddy yet. Permissions match self-hosted DNS zones: reads gate on DOMAINS_VIEW, writes on DOMAINS_MANAGE.

The create-edge-route dialog with the fronted hostname, IPv6 origin, port, and direct-AAAA toggle
📷Creating an edge route — fronted hostname, the IPv6-only origin, the origin port, and the optional direct-AAAA fast-path. A bare IPv4 origin is refused here.img/dns-edge-create.avif
Creating an edge route — fronted hostname, the IPv6-only origin, the origin port, and the optional direct-AAAA fast-path. A bare IPv4 origin is refused here.

3. Assign the serving edges

Assign the route's serving boxes. Every id must resolve to an enrolled edge-role node — a non-edge or unknown id is refused with an actionable message. Assign both edges so the route gets two A records.

4. Flip the enable switch

Enabling refuses a route with no assigned, live edge node (an honest failure — never a route fronted by nothing). On enable, SpipCP reconciles the derived world:

  • Caddy — each edge box's config gains a site block for hostname that terminates TLS (Let's Encrypt over the edge's v4) and reverse-proxies to https://[origin]:port. Pushed to every serving edge over the admin API. A box with no enabled edge routes generates byte-identical Caddy to before — the edge branch is purely additive.
  • DNS — through the PowerDNS fan-out: hostname A <edge1 v4> and hostname A <edge2 v4> (both edges), plus hostname AAAA <origin v6> when publishDirectAaaa is on and the origin is a v6 literal.
  • Health probe — provision the per-edge heartbeat probe so failover can fire.
The per-origin enable toggle on an edge route flipping from off to on
📷The per-origin enable switch — default off. Flipping it on publishes the A/AAAA records and pushes the edge Caddy blocks; flipping it off restores the prior direct-v6 behaviour.img/dns-edge-enable.avif
The per-origin enable switch — default off. Flipping it on publishes the A/AAAA records and pushes the edge Caddy blocks; flipping it off restores the prior direct-v6 behaviour.

5. What DNS records you get

For the example fleet (two edges + an IPv6-only panel origin):

panel.example.com.  300  IN  A     203.0.113.71      ; edge1 public v4
panel.example.com.  300  IN  A     198.51.100.72     ; edge2 public v4
panel.example.com.  300  IN  AAAA  2a01:4f8:panel::1 ; direct v6 fast-path (optional)

A v4-only client resolves the two A records and reaches the origin through an edge. A v6 client uses the AAAA and reaches the origin directly.

The published A and AAAA records for a fronted hostname — two A records (one per edge) and a direct AAAA
📷The records that appear when a route is enabled — two A records (one per edge) plus the optional direct AAAA for v6 clients.img/dns-edge-records.avif
The records that appear when a route is enabled — two A records (one per edge) plus the optional direct AAAA for v6 clients.

6. How failover behaves when an edge dies

  • The heartbeat probe on the dead edge fails its checks. After 3 consecutive failures (the default threshold), SpipCP withdraws that edge's A from every route it serves — leaving the other edge's A in place. New resolvers stop handing out the dead edge; the hostname stays reachable over the survivor.
  • The state is durable (an edge_health row): a worker restart never re-publishes a withdrawn edge's A.
  • When the edge recovers and passes 2 consecutive successes (the default recovery threshold), its A is re-added automatically. The hysteresis (3 to withdraw, 2 to re-add) means a single blip never pulls DNS and a single good check never flaps it back.
  • This is "good enough" two-box redundancy with happy-eyeballs — not anycast/BGP/zero-second failover. Resolver TTL + client retry govern how fast a client notices.

7. What the notification says

Every transition emails and webhooks the operator through the existing probe-notify path (one operator SMTP setting, no second email stack) and writes an audit row. The severity is chosen from how many edges remain live:

SeverityWhenExample
warningAn edge withdrew but ≥1 edge still serves every affected route — still reachable over v4.🟠 Edge ns1-edge-eu down — A record withdrawn (other edges still serving)
criticalThe last edge for a route withdrew — that hostname is now dark to v4-only clients.🔴 LAST edge ns2-edge-us down — panel.example.com unreachable over IPv4
recoveryAn edge passed its health checks again; its A was re-added.✅ Edge ns1-edge-eu recovered — A record re-added

The worst-off affected route sets the severity: one unreachable hostname is an outage even if other routes are merely degraded.

An edge-down notification showing an A record auto-withdrawn with the remaining-live-edge count
📷An edge-down notification — which edge failed, which routes it was withdrawn from, and how many edges still serve each (the warning-vs-critical line).img/dns-edge-failover.avif
An edge-down notification — which edge failed, which routes it was withdrawn from, and how many edges still serve each (the warning-vs-critical line).

8. Runbook — both edges down

If you get the critical "last edge down" alert, IPv4-only clients can't reach the fronted hostname (v6 clients are unaffected if the direct AAAA is published). In order:

  1. Check the origin is actually up. The edges only proxy — if the origin is down, both edges health-check fine but serve 5xx. Fix the origin first.
  2. Check the edges. Both edge boxes' probes are down. SSH in (break-glass) or check the host provider — is it a network outage at one location, both, or the boxes themselves? Two edges in two regions means a single-region outage shouldn't take both.
  3. Bring at least one edge back. As soon as one edge passes its recovery checks, its A is re-added automatically and v4 reachability returns. No manual DNS edit needed.
  4. Add a third edge if the two keep flapping together (correlated failure — same provider/region). Enroll another {nameserver, edge} box and assign it to the route.
  5. The v6 fallback holds throughout. If the direct AAAA is published, v6 clients keep working — so the outage is "v4-only clients can't reach it," not a total blackout.

The Cloudflare orange-cloud alternative

You do not have to self-host the edge. You can instead point the fronted hostname at Cloudflare (orange-cloud / proxied DNS): Cloudflare owns the public v4, terminates TLS, and proxies to your IPv6-only origin. SpipCP supports this as a documented choice — it is not a dependency of any feature.

Pick Cloudflare whenPick the self-hosted edge (the default) when
You don't want to run two more boxes.Sovereignty / GDPR — no third party sees plaintext requests; the whole path is your infrastructure.
You want a large anycast network and DDoS absorption out of the box.You already run the two PowerDNS boxes, so the edge is "free" (co-located).
A little third-party in the request path fits your threat model.You want one consistent stack — your Caddy, your certs, your audit log, no external account, rate limits, or ToS.

The tradeoff is SPOF-vs-sovereignty: Cloudflare's network is far more redundant than two boxes, but it puts a third party in front of every request. The self-hosted edge keeps everything yours at the cost of operating the redundancy yourself — two boxes plus health-checked failover, which is exactly what this feature automates.

Live v4-client round-trip is rig-pending

The edge model is built end to end — the edge role, the route CRUD, the Caddy generation, the multi-A publish/withdraw, and the health-checked failover with notifications. The live "a real v4-only client reaches a v6-only origin through the edge, and failover withdraws an A on a real outage" round-trip is exercised by a remote rig stage that is still pending certification at the time of writing — treat that end-to-end path as built-and-tested-in-parts, not yet live-proven.

On this page