Edge fleet -> receiver
An appliance fleet streaming data into a central receiver, with reverse
admin back down the same tunnels. Any hub-and-spoke telemetry fleet has
this shape: edge stream hubs pushing into a central ingest service, and
operators needing a way back down to the appliances. It is the
edge-fleet profile.
This is the shape we run culvert in ourselves, which is why the profile is opinionated rather than a blank template.
Two things make this different from an ordinary client VPN. Understand both before you deploy it.
Difference 1: reverse admin
A normal VPN only ever lets clients initiate outbound. This shape also lets named admin networks INITIATE back DOWN an established client tunnel to reach an appliance - for updates, restricted repos, remote support. That inbound-to-client path is deliberate and needed here; it is not something you want on a general-purpose VPN.
It is gated: only the CIDRs in CULVERT_DOWNSTREAM_ADMIN_CIDRS may
initiate inbound. Everything else unsolicited into the tunnels is
dropped.
Difference 2: CGNAT addressing
Because the operator controls BOTH ends (the appliances and the
receiver), the tunnels use 100.64.0.0/10 (RFC 6598, the carrier-grade
NAT range) instead of the default 10.8.0.0/22. The appliances' own LANs
and the destination network do not sit in 100.64/10, so tunnel
addresses never collide with either end and no NAT gateway is needed just
to dodge overlap.
Warning: do not use this preset for laptops on carrier or Starlink
links. Those WANs already sit in 100.64/10, as do Tailscale clients -
run a CGNAT tunnel on top of a CGNAT underlay and the transport
misroutes. The why is in addressing.md. This is safe
here precisely because the operator owns both ends and knows the
appliances are not on CGNAT WANs.
Traffic model
State it exactly:
outbound (data): appliance -> culvert -> receiver
inbound (admin): admin / repos -> culvert (back down tunnel) -> appliance
Data flows up from the appliances to the receiver. Admin flows down from the admin networks to the appliances, over the tunnels the appliances already established.
Routing control
CULVERT_ROUTING_CONTROL_ENABLED=true installs a dedicated FORWARD
filtering chain. With the edge-fleet profile it enforces:
- Replies always pass. Established/related flows are accepted, so an appliance reaching the receiver still gets its answers back.
- Appliances cannot reach each other.
CULVERT_CLIENT_ISOLATIONis on - client-to-client traffic is dropped. - Appliances may only initiate to the receiver.
CULVERT_ALLOWED_DESTINATIONSis the receiver CIDR; clients may initiate only to those destinations. Pair it withCULVERT_PUSH_ROUTESso clients are routed at exactly what they are allowed to reach. - Only admin CIDRs may initiate inbound.
CULVERT_DOWNSTREAM_ADMIN_CIDRSare the only sources allowed to open connections down into the tunnels; all other unsolicited inbound is dropped.
The profile ships example CIDRs - set them to yours:
allowed_destinations: "10.20.0.0/16" # the receiver network
downstream_admin_cidrs: "10.10.0.0/16" # admin / bastion networks
push_routes: "10.20.0.0/16,10.10.0.0/16" # receiver AND admin - see below
udp_network: "100.64.0.0" # CGNAT tunnel subnet
udp_netmask: "255.255.255.0"
push_routes covers both the receiver and the admin CIDRs. The receiver
route carries the appliance's outbound data; the admin route is what lets
an appliance's REPLY to an admin-initiated connection travel back UP the
tunnel instead of leaking out its own default gateway. Omit the admin
CIDR and reverse admin fails one-way - the admin can send but never gets
a response.
Those are illustrative ranges, not real addresses. Substitute your own receiver, admin, and tunnel CIDRs.
Deploying it
The profile also sets log_mode: stdout for the cluster log pipeline and
starts on a local CA (external PKI - a fleet CA in OpenBao or a mounted
Secret - is typical here; set CULVERT_PKI_MODE=external +
CULVERT_SECRETS_* to swap).
Run it on the Kubernetes chart - see
use-case-kubernetes.md. The chart runs one replica,
so a single pod holds every appliance's client-IP allocation and certificate
state. Keep publicService.externalTrafficPolicy: Local, as the install
values there do, so the pod sees each appliance's real source address, which
per-client routing rules and the connection log need.
See also
- use-case-kubernetes.md - the chart, the privileged namespace and the PKI volume
- addressing.md - why
100.64/10here and why it is wrong for laptops - Routing Control in .env.example - the exact
CULVERT_*routing-control variables