Repository navigation
feat(pangolin): transitional Gerbil host gateway mode and cross-namespace Badger fix - #29
Merged
Merged
Conversation
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>
marcschaeferger
force-pushed
the
feat/gerbil-host-gateway
branch
from
September 10, 2026 20:51
dac5de9 to
22d15f9
Compare
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>
marcschaeferger
force-pushed
the
feat/gerbil-host-gateway
branch
from
September 12, 2026 10:09
22d15f9 to
53b4888
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
apiBaseUrlfromserver.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 namepangolin.Badger runs inside the Traefik Pod and resolves that name from there.
deployment.traefikNamespaceexists precisely so Traefik can live elsewhere, and from another namespace a bare Service name does not resolve at all: Badger answers404 page not foundfor every request while the Traefik dashboard still reports the router asSuccess. That matches the second symptom in the issue, and the reporter'sExternalNameworkaround is no longer needed.internal_hostnamenow 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, andglobal.clusterDomaincovers clusters that do not usecluster.local.Gerbil's
--reachableAt/--remoteConfigand the controller'sCONFIG_ENDPOINTare qualified too. They resolve today only via the cluster search path, which stops applying the moment Gerbil runs withhostNetwork.The part that is a workaround
gerbil.hostGateway.enabled=true(opt-in, never implicit) runs Gerbil withhostNetwork, sowg0and 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 requiredpodAffinity(traefik.colocateWithGerbil), and for an external Traefikhelm get notesprints 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":
IgnoredDuringExecution502until restarted.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.replicaCountpinned to 1 — no Gerbil HA.privilegedPSAbaselineforbidshostNetwork.NET_ADMINis still required.singlemodehostNetworkwould bind every component's ports on the node.PANGOLIN-071…075reject each combination that cannot work — single mode, Gerbil disabled, more than one replica, a chart-created namespace that forbidshostNetwork, 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
/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 theaddPrefix+headerspair needed to drive it. Wiring it up requires controller changes, not chart changes.network_mode: service:gerbilequivalent, but it only helps when the chart manages Traefik, anddeployment.traefikNamespaceis 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
hostNetworkPod is not matched by apodSelectoron 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-075fails the render until you set your node addresses). Without that, the chart's own default policy would blackhole Gerbil's--remoteConfigpolling — a failure the existing WIP approaches to this issue missed.Testing, and its ceiling
scripts/e2e/assert-tunnel-backends.shfails 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
wg0and 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 Traefiktraefik-standaloneand 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
labelSelectoragainst the same literal map as the Gerbil workload's ownspec.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.values-host-gateway.yaml.1.22.2, Gerbil1.5.1, Newt1.16.0andcurlimages/curl:8.8.0all return 200; only the controller0.1.0-alpha.2returns 404, which is the intended blocker.pre-commit run --all-filesclean.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 includeddevwas merged into this branch and six conflicts resolved, so nothing has to be pushed straight todevafter this lands.networkpolicy.yaml- dev's chart-managed-Traefik rule is a sibling of the$renderGerbilblock, not nested inside it; nesting it would have scoped the rule to whether Gerbil renders and leftif $renderGerbilunclosed. The RFC1918/CGNAT fallback fornodeCIDRsis removed:PANGOLIN-075already 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.configEndpointbuilt a bare Service name, which is exactly the failure61b2fc7exists to fix; it now usespangolin.pangolin.serviceFQDN.standalone_traefik_wiring_test.yamlasserted 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 theupdown.enabledguard, the metrics listen-port helper and the healthemptyDir.README.md.gotmpl- two factual corrections:nodeCIDRshas no default, andPANGOLIN-075is that check, not a Traefik-subchart affinity limitation (no such guard exists).CHANGELOG.md/VERSION_MATRIX.md- both bullet sets kept; controller tag stays0.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 passhelm unittest --strict charts/newt- 21 suites, 156 tests, all passhelm lintboth charts clean;values.schema.jsonregenerates byte-identical; README regenerated with helm-docshelm templaterenders for the default values and forgerbil.hostGateway.enabled=true;PANGOLIN-075confirmed to fire on an emptynodeCIDRsPANGOLIN-071...075each defined exactly once; no CRLF introducedNot verifiable on this machine (no Docker): every kind E2E leg and the live registry probe.