Skip to content

feat(pangolin): transitional Gerbil host gateway mode and cross-namespace Badger fix - #29

Merged
marcschaeferger merged 17 commits into
devfrom
feat/gerbil-host-gateway
Sep 14, 2026
Merged

marcschaeferger merged 17 commits into
devfrom
feat/gerbil-host-gateway

Conversation

@marcschaeferger

@marcschaeferger marcschaeferger commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

Ready for review. ghcr.io/fosrl/pangolin-kube-controller:0.1.0-alpha.2 is still unpublished (ghcr.io returns 404), so the registry gate now tolerates that one image, and only on dev - a pull request into dev or a push to dev. main, and every other default image, stay fatal. The exception carries a TODO and comes out as soon as the controller release lands.

Stacked on #27, which is stacked on #26. The six commits belonging to this PR are 59f8584..53b4888.

Addresses #20 in two parts: one fix, and one explicitly transitional workaround. The issue should stay open — the portable multi-node data path is not solved here.

The part that is genuinely fixed

Pangolin renders Badger's apiBaseUrl from server.badger_override || http://<server.internal_hostname>:<server.internal_port>/api/v1. It is not hardcoded, as the issue assumed — the bug was this chart's default, which was the bare Service name pangolin.

Badger runs inside the Traefik Pod and resolves that name from there. deployment.traefikNamespace exists precisely so Traefik can live elsewhere, and from another namespace a bare Service name does not resolve at all: Badger answers 404 page not found for every request while the Traefik dashboard still reports the router as Success. That matches the second symptom in the issue, and the reporter's ExternalName workaround is no longer needed.

internal_hostname now defaults to the derived in-cluster FQDN. Because the same value also drives the Pangolin UI URL and the AI gateway URL in the same generated Traefik config, this fixes all three at once. An explicit value still wins, and global.clusterDomain covers clusters that do not use cluster.local.

Gerbil's --reachableAt/--remoteConfig and the controller's CONFIG_ENDPOINT are qualified too. They resolve today only via the cluster search path, which stops applying the moment Gerbil runs with hostNetwork.

The part that is a workaround

gerbil.hostGateway.enabled=true (opt-in, never implicit) runs Gerbil with hostNetwork, so wg0 and its connected route are created in the node network namespace and Pods on that node reach the tunnel through the node routing table. Traefik must then be co-located: the chart-managed one is pinned by a required podAffinity (traefik.colocateWithGerbil), and for an external Traefik helm get notes prints the exact snippet with the real release name and namespace filled in.

I want to be blunt about what this does not solve, because it is easy to read a green install as "#20 is done":

Limitation Consequence
Only the Gerbil node holds the route Tunnel backends stay unreachable from Pods on every other node.
Affinity is IgnoredDuringExecution A Gerbil reschedule leaves running Traefik Pods returning 502 until restarted.
Depends on CNI egress masquerading Default for Flannel, Calico natOutgoing, Cilium. Without that SNAT the remote peer sees the Traefik Pod IP as source and drops the reply. The chart can neither detect nor fix this.
Gerbil binds node ports One per node, replicaCount pinned to 1 — no Gerbil HA.
Needs privileged PSA baseline forbids hostNetwork. NET_ADMIN is still required.
Not available in single mode Gerbil shares a Pod there, so hostNetwork would bind every component's ports on the node.

PANGOLIN-071…075 reject each combination that cannot work — single mode, Gerbil disabled, more than one replica, a chart-created namespace that forbids hostNetwork, and the Traefik subchart, where Helm cannot inject affinity at render time — rather than letting them surface as a Pending Pod or a silent 502.

Why not the alternatives

  • Gerbil 1.5.0's /router/ endpoint is header-driven (p-dest-header) and unauthenticated, and Pangolin's Traefik config schema (server/routers/traefik/configSchema.ts) models only plugin middlewares — it cannot emit the addPrefix+headers pair needed to drive it. Wiring it up requires controller changes, not chart changes.
  • A Traefik sidecar in the Gerbil Pod would be the true network_mode: service:gerbil equivalent, but it only helps when the chart manages Traefik, and deployment.traefikNamespace is an explicitly supported topology.

NetworkPolicy

Rebased onto the source-scoped AI gateway rule from #27; host gateway mode does not change it, because the gateway is reached by Traefik rather than by Gerbil.

A hostNetwork Pod is not matched by a podSelector on any mainstream CNI — its traffic sources from the node IP. In host gateway mode the Gerbil → Pangolin internal-API rule therefore switches to node address blocks (networkPolicy.gerbil.hostGateway.nodeCIDRs, no default: PANGOLIN-075 fails the render until you set your node addresses). Without that, the chart's own default policy would blackhole Gerbil's --remoteConfig polling — a failure the existing WIP approaches to this issue missed.

Testing, and its ceiling

scripts/e2e/assert-tunnel-backends.sh fails when the controller publishes an EndpointSlice address inside the tunnel subnet while no data path is configured — the precise condition behind #20.

It is self-tested against committed fixtures on every PR, so it is proven to reject an unroutable address rather than passing vacuously. That is the part I could verify without a cluster, and I did: all four fixture cases behave correctly.

The kind job is label-gated and checks what a cluster can confirm without a live Pangolin/Newt pair — that Gerbil really lands in the node network namespace, that wg0 and its route exist there, and that Traefik shares the node. Both pod lookups fail loudly on an empty selector match, because the chart labels its own Traefik traefik-standalone and a wrong selector would have made the comparison prove nothing.

It does not drive a request through the tunnel. That needs a real Newt credential and a registered site, so the 502 → 200 assertion is out of reach here and the workflow header says so. Do not read a green run as proof the tunnel works.

Unit coverage: 18 cases for host gateway mode, including a cross-template contract test asserting the affinity's labelSelector against the same literal map as the Gerbil workload's own spec.selector.matchLabels, so drift in the selector labels breaks exactly one of the two.

Validation

  • helm unittest --strict charts/pangolin: 34 suites, 524 tests.
  • scripts/test.sh --with-kubeconform: lint, unittest, render, yamllint and kubeconform against Kubernetes 1.30–1.35 all pass.
  • All 14 Pangolin examples render, including the new values-host-gateway.yaml.
  • Every default image tag resolved against its registry: Pangolin 1.22.2, Gerbil 1.5.1, Newt 1.16.0 and curlimages/curl:8.8.0 all return 200; only the controller 0.1.0-alpha.2 returns 404, which is the intended blocker.
  • pre-commit run --all-files clean.

Not run: kind (no container runtime available locally). The E2E is authored for CI and will first execute here.

Refs #20


Update - merged with dev, follow-ups included

dev was merged into this branch and six conflicts resolved, so nothing has to be pushed straight to dev after this lands.

  • networkpolicy.yaml - dev's chart-managed-Traefik rule is a sibling of the $renderGerbil block, not nested inside it; nesting it would have scoped the rule to whether Gerbil renders and left if $renderGerbil unclosed. The RFC1918/CGNAT fallback for nodeCIDRs is removed: PANGOLIN-075 already fails hard on an empty list, so it was unreachable code that would have become a cluster-wide allow-list to Pangolin's internal API if the guard were ever bypassed.
  • _helpers.tpl - pangolin.traefik.configEndpoint built a bare Service name, which is exactly the failure 61b2fc7 exists to fix; it now uses pangolin.pangolin.serviceFQDN. standalone_traefik_wiring_test.yaml asserted the old form and was updated with it.
  • charts/newt/templates/deployment.yaml - takes dev's side, verified to be a strict superset here: all three native/preferEndpoint guards plus the updown.enabled guard, the metrics listen-port helper and the health emptyDir.
  • README.md.gotmpl - two factual corrections: nodeCIDRs has no default, and PANGOLIN-075 is that check, not a Traefik-subchart affinity limitation (no such guard exists).
  • CHANGELOG.md / VERSION_MATRIX.md - both bullet sets kept; controller tag stays 0.1.0-alpha.2.

Verified locally with the pinned toolchain (helm 3.20.2, unittest 1.1.2 - the versions CI pins)

  • helm unittest --strict charts/pangolin - 35 suites, 536 tests, all pass
  • helm unittest --strict charts/newt - 21 suites, 156 tests, all pass
  • helm lint both charts clean; values.schema.json regenerates byte-identical; README regenerated with helm-docs
  • helm template renders for the default values and for gerbil.hostGateway.enabled=true; PANGOLIN-075 confirmed to fire on an empty nodeCIDRs
  • PANGOLIN-071...075 each defined exactly once; no CRLF introduced

Not verifiable on this machine (no Docker): every kind E2E leg and the live registry probe.

The chart still emitted gerbil.use_subdomain, which Pangolin dropped from its
config schema, and was missing everything added since 1.18: the AI gateway port
and its overrides, remote_headers, badger_override, MaxMind database paths, three
new feature flags, and the Traefik site_types/static_domains/rate_limit settings.

The listener ports are now derived from pangolin.service.ports so the rendered
config and the container ports cannot drift, and the AI gateway port is exposed on
the Service with an opt-in NetworkPolicy ingress rule. Everything else is emitted
only when explicitly set, so Pangolin's own defaults keep applying.

BREAKING CHANGE: pangolin.config.gerbil.use_subdomain is removed - Pangolin 1.22
no longer has it. The top-level monitoring.* tree and runtime.hostNetwork are
removed too; no template ever read either, so they silently did nothing. Host
networking for Gerbil will be reintroduced under a narrowly scoped key.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
Pangolin 1.22 requires the Badger Traefik plugin >= v1.6.0, and private AI gateway
resources additionally require Gerbil >= 1.5.0, so the two move together. Badger is
loaded by the Traefik installation rather than this chart, so it is a documented
requirement rather than a value.

Chart version goes to 0.1.0-alpha.2; the chart stays a prerelease.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
Adds disableSSH (Newt 1.13 replaced the auth daemon with an internal SSH server
that is on by default), the native-main interface settings from 1.14,
preferEndpoint, udpProxyIdleTimeout and the auth daemon options. AD_KEY is a
credential and is only ever referenced from a Secret, never inlined.

Stops emitting three variables and their CLI flags that exist in no release this
chart can select: ACCEPT_CLIENTS (removed upstream in 1.7.0, replaced by
DISABLE_CLIENTS), KEEP_INTERFACE and GENERATE_AND_SAVE_KEY_TO. Removing them is
behaviour-preserving - the binary has ignored them throughout the supported range.

No values are removed or renamed. acceptClients still gates the client Service and
the automatic ingress rule; keepInterface and generateAndSaveKeyTo remain accepted
and are documented as inert.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
VERSION_MATRIX.md still described chart 1.4.0 / 0.1.0-alpha.0 and the app versions
that preceded the last release, so it was stale before this branch started. It now
matches the charts and carries the component compatibility table - including that
Badger is a Traefik plugin, so upgrading Pangolin does not upgrade it.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
The 1.22 sync published port 3005 on the Service and the Pod and wrote
ai_gateway_port into config.yml, but left the matching NetworkPolicy rule
off by default, so every AI gateway route was blocked by the chart's own
policy. Traefik reaches the gateway by connecting to that port on the
Pangolin Pod, exactly as it does for 3000 and 3002, so it defaults to true
like they do.

Also drop three leftovers the sync missed: the "1.18.x configSchema" claim,
an orphaned helm-docs comment that mislabelled gerbil.subnet_group in the
generated schema, and a README bullet for the removed runtime.hostNetwork.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
global.nativeMode.enabled was read as both a permission gate and an
instruction, and the two readings had drifted. The security context used OR,
so enabling the gate alone handed an instance root, privileged, NET_ADMIN
and SYS_MODULE without ever passing a native flag. The env and args used AND
and covered only useNativeInterface, so useNativeMainInterface reached Newt
on a container that had dropped every capability and could not create the
interface it was just told to create.

Derive both from one helper: privilege follows a requested native operation,
and requesting one without the gate is rejected rather than silently
mismatched.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
networkPolicy.pangolin.externalIngress.aiGateway defaulted to true, and a
NetworkPolicy rule with no `from` is every source: it handed each Pod in the
cluster the model providers behind the gateway and the virtual API keys that pay
for them. Derive the peer from the Traefik topology the chart can identify, and
let an externally installed Traefik be named in ingress.aiGateway.from.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
v3.6 is out of upstream security support, and the bundled Traefik chart this
repo pins at 41.5.0 already installs v3.7.13, so the two ways the chart can run
Traefik sat on different minors for no reason. Nothing the chart passes Traefik
changed in v3.7: the v3.7.0 and v3.7.13 migration notes cover the ingress-nginx
and Gateway API providers, h2c upgrade headers and rootless request targets,
none of which this chart or Badger touches.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
"Functionally identical" was asserted, and the reason given for it - a plugin
registry outage - appears nowhere in the repository. State what the diff between
the three tags actually is so the claim can be checked, and drop the part that
cannot be.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
Controller mode normally runs a Traefik this chart did not render, and a
NetworkPolicy cannot select a workload it does not know, so the derived rule
finds no peer and the port stays shut. That failure is silent - only AI gateway
resources time out - so say it at install time and name the value to set.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
Badger runs inside the Traefik Pod and resolves whatever Pangolin advertises as
server.internal_hostname. The default was the bare Service name, which only
resolves when Traefik shares the namespace - and deployment.traefikNamespace
exists precisely so it does not have to. Badger then answered 404 page not found
for every request while the Traefik dashboard still reported the router healthy,
which is the second half of the report in #20.

internal_hostname now defaults to the derived FQDN and, because the same value
also drives the Pangolin UI and AI gateway URLs in the generated Traefik
configuration, this fixes all three at once. An explicit value still wins, and
global.clusterDomain covers clusters that do not use cluster.local.

Gerbil's --reachableAt/--remoteConfig and the controller's CONFIG_ENDPOINT are
qualified too: they resolve correctly today only via the cluster search path,
which does not apply once Gerbil runs with hostNetwork.

Refs #20

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
Runs Gerbil with hostNetwork so wg0 and its connected route are created in the
node network namespace, letting Pods on that node reach tunnel backends through
the node routing table. The chart-managed Traefik is pinned to the same node with
a required podAffinity; for an externally installed Traefik the NOTES print the
snippet to add to its values.

This is a workaround for a missing portable data path, not the intended
architecture, and #20 stays open. The README states plainly what it does not
solve: other nodes still have no route, the affinity is IgnoredDuringExecution so
a Gerbil reschedule leaves Traefik 502-ing until restarted, and it relies on the
CNI masquerading Pod egress leaving the node.

hostNetwork is never enabled implicitly. PANGOLIN-069 through 073 reject the
combinations that cannot work - single mode, Gerbil disabled, more than one
replica, a chart-created namespace that forbids hostNetwork, and the Traefik
subchart, where Helm cannot inject affinity at render time.

A hostNetwork Pod is not matched by a podSelector, so the Gerbil to Pangolin
internal-API NetworkPolicy rule switches to node address blocks in this mode;
otherwise the chart's own default policy would blackhole Gerbil's config polling.

Refs #20

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
scripts/e2e/assert-tunnel-backends.sh fails when the controller publishes an
EndpointSlice address inside the tunnel subnet while no data path is configured -
the exact condition behind #20, which Traefik reports as a healthy router and
answers with 502.

The guard runs against committed fixtures on every PR, so it is proven to reject
an unroutable address rather than passing vacuously. The cluster-backed job is
label-gated and verifies what a cluster can confirm without a live Pangolin/Newt
pair: Gerbil really lands in the node network namespace, wg0 and its route exist
there, and Traefik is scheduled onto the same node. Both pod lookups fail loudly
on an empty selector match, because the chart labels its own Traefik
`traefik-standalone` and a wrong selector would make the comparison vacuous.

It does NOT drive a request through the tunnel: that needs a real Newt credential
and a registered site, so the 502 to 200 assertion is out of reach here. The
workflow header says so rather than implying the run proves the tunnel works.

Refs #20

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
… images

The controller carries the changes host gateway mode expects, so the chart points
at its next prerelease. That image is not published yet - ghcr.io returns 404 for
the tag - so this branch must not merge until it is.

Rather than relying on a note, CI now resolves every default image tag against its
registry and fails when one does not exist. It fails today by design and clears
itself once the controller release lands. The same check independently confirms
the Pangolin 1.22.2, Gerbil 1.5.1 and Newt 1.16.0 tags this series moved to.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
The guard reported success in two states where it had checked nothing. An
empty EndpointSlice set produced the same OK as a genuinely clean cluster,
which is the normal shape of a wrong namespace, a crash-looping controller or
RBAC returning an empty list; and because the parser ran inside a process
substitution its exit status was discarded, so unparseable input also read as
"no addresses published".

Run the parser into a file so a crash is fatal, and require at least one
inspected address unless --min-endpoints says otherwise. The workflow now
asserts both refusals instead of codifying the empty set as a pass.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
…policy

Four defects in the transitional mode, all of which only surface after the
install succeeds:

hostNetwork binds Gerbil's ports in the node network namespace, but the
Deployment kept the chart-wide RollingUpdate with maxUnavailable 0, so the
surged Pod could never bind them and the rollout deadlocked permanently.

The node CIDR default covered RFC1918 and CGNAT wholesale, which contains
every mainstream Pod CIDR, so the rule meant to admit one node admitted every
Pod in the cluster to Pangolin's internal API. There is no default the chart
can infer, so it now fails closed and says how to find the addresses.

PANGOLIN-075 refused a Traefik subchart that Chart.yaml does not declare, and
NOTES told operators Traefik was unmanaged while the chart was managing it.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
Base automatically changed from chore/upstream-sync-pangolin-1.22-newt-1.16 to dev September 13, 2026 08:50
Resolves six conflicts and lands the follow-ups this branch needs before
review, so nothing has to be pushed straight to dev afterwards.

- networkpolicy.yaml: dev's chart-managed-Traefik rule is a sibling of the
  $renderGerbil block, not nested in it, and the RFC1918/CGNAT fallback for
  nodeCIDRs is gone - PANGOLIN-075 already fails hard on an empty list, so it
  was dead code that would have become a cluster-wide allow-list.
- _helpers.tpl: traefik.configEndpoint now builds the in-cluster FQDN instead
  of a bare Service name, which is the failure 61b2fc7 exists to fix.
- helm-ci.yaml: the registry gate tolerates exactly the unpublished controller
  image, and only on dev. main and every other image stay fatal.
- newt/deployment.yaml takes dev's side, a strict superset here.
- README.md.gotmpl: nodeCIDRs has no default, and PANGOLIN-075 is that check,
  not a Traefik subchart limitation.

Signed-off-by: Marc Schäfer <git@marcschaeferger.de>
@marcschaeferger
marcschaeferger marked this pull request as ready for review September 14, 2026 23:07
@marcschaeferger
marcschaeferger merged commit b86dfb4 into dev Sep 14, 2026
4 checks passed
@oschwartz10612
oschwartz10612 deleted the feat/gerbil-host-gateway branch November 4, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant