feat(hub): extend trust-bundle mechanism to the hub pod#160
feat(hub): extend trust-bundle mechanism to the hub pod#160oren-openteams wants to merge 2 commits into
Conversation
Closes nebari-dev#159. The chart already ships an enterprise CA bundle mechanism for singleuser pods via ``custom.trust-bundle-enabled`` — ``_setup_trust_bundle`` in ``01-spawner.py`` mounts trust-manager's org-ca ConfigMap, merges it with the image's system CA via an init container, and points the standard CA env vars at the merged file so pip/conda/git/requests all see both public CAs and the org CA. The equivalent wiring on the hub pod itself was missing: outbound HTTPS from the hub to TLS-inspected external endpoints (oauthenticator refresh against an external Keycloak, jhub-apps configuration fetches, etc.) had no way to trust the org CA short of overriding the entire authenticator via ``hub.extraConfig``. This adds the missing hub-side wiring to ``values.yaml`` under ``jupyterhub.hub.*``: - ``extraVolumes``: an ``org-ca`` ConfigMap volume with ``optional: true`` (so a cluster without trust-manager still starts) plus a ``ca-merged`` emptyDir volume. - ``extraVolumeMounts``: the merged bundle mounted at ``/etc/ssl/certs-extra`` — same path as the singleuser side. - ``initContainers``: a ``merge-ca-bundle`` init container that copies ``/etc/ssl/certs/ca-certificates.crt`` from the hub image into ``/merged/ca-bundle.crt`` and then appends the org CA IFF the mounted file exists. On clusters without trust-manager the merged bundle is a byte-copy of the system bundle — functionally a no-op. Uses the hub image so the system CA store the init container reads matches what the hub container has at runtime; a generic busybox would not. - ``extraEnv``: ``REQUESTS_CA_BUNDLE``, ``SSL_CERT_FILE``, ``NODE_EXTRA_CA_CERTS``, ``CURL_CA_BUNDLE``, ``GIT_SSL_CAINFO`` all point at the merged bundle. Rust tooling (rustls-native-certs) honours ``SSL_CERT_FILE`` but does not iterate ``SSL_CERT_DIR``, so the explicit file path is required. Design note: unlike the singleuser side (which gates on ``custom.trust-bundle-enabled`` at spawn time via a Python hook), the hub-side wiring is unconditional. The hub Deployment is rendered by z2jh from static values, so runtime gating is not available without a chart helper. ``optional: true`` on the ConfigMap and the ``[ -f ... ]`` guard on the append make the always-on wiring functionally identical to the singleuser disabled path — merged bundle equals system bundle, env vars point at a file that matches the system trust store, no behaviour change on clusters without trust-manager. Costs one extra init container run per hub pod start. Deployers who want to opt out entirely can override ``hub.extraVolumes`` etc. in their own values (with the existing warning about re-including the pre-existing entries). The ``trust-bundle-configmap`` and ``trust-bundle-key`` values under ``jupyterhub.custom.*`` drive BOTH sides. Changing them today requires editing values in two places (the singleuser Python hook reads the custom values at runtime; the hub-side static YAML references the same default explicitly). A follow-up could bind them via a chart helper. Tests: ``tests/unit/test_hub_ca_bundle.py`` pins the shape of the ``jupyterhub.hub.*`` block — the volumes, mounts, init container, env vars, and the alignment between the hub-side static reference and the default ``trust-bundle-configmap`` value. 12 assertions covering the contract in the ``trust-bundle-enabled`` docstring so a future edit that accidentally drops one of them fails CI instead of silently breaking outbound HTTPS from the hub.
runAsNonRoot z2jh renders the hub Deployment with a pod-level ``securityContext`` of ``runAsNonRoot: true`` but no ``runAsUser``, so containers inherit the non-root requirement while relying on their image's default USER directive to pick a uid. The hub image's default is root, so the ``merge-ca-bundle`` init container (which reuses the hub image so the system CA store it reads matches what the hub container has at runtime) was blocked by kubelet with ``container has runAsNonRoot and image will run as root`` and the pod was stuck at ``Init:CreateContainerConfigError`` indefinitely. Fix: give the init container an explicit ``securityContext`` that mirrors the hub container itself — ``runAsUser: 1000``, ``runAsGroup: 1000``, ``allowPrivilegeEscalation: false``, drop all capabilities. Uid 1000 has world-read access to ``/etc/ssl/certs/ca-certificates.crt``, and the shared ``ca-merged`` emptyDir inherits the pod's ``fsGroup: 1000``, so the merge script can read the system bundle and write the merged file without privilege. Adds a unit test pinning the shape so a future re-render that drops the block fails loudly rather than silently reintroducing the CreateContainer ConfigError. Caught by the e2e ``hub_login_page`` CI job on the original push — hub pod never left ``Init:0/1``. Local kind reproduce confirmed the fix.
|
Verified end-to-end on a private-VPC dev cluster. The Post-fix: all 14 CI checks green (including the previously-failing e2e shared-storage suite and
One minor documentation note worth flagging: on deploys where the target trust-manager Bundle uses a different key than the chart default ( |
Closes #159.
Summary
Extends the chart's existing enterprise CA bundle mechanism (
custom.trust-bundle-enabled+_setup_trust_bundlein01-spawner.py) to also cover the hub pod. The singleuser side was already thorough; the hub was the last gap.Before this PR, outbound HTTPS from the hub to any TLS-inspected external endpoint (external Keycloak URL, oauthenticator refresh, jhub-apps configuration fetches) had no way to trust the org CA short of overriding the entire authenticator via
hub.extraConfig.Changes
values.yamljupyterhub.hub.*:extraVolumes— addsorg-ca(ConfigMap,optional: true) +ca-merged(emptyDir). Same names as the singleuser side.extraVolumeMounts— addsca-mergedat/etc/ssl/certs-extra, matching the singleuser path.initContainers— newmerge-ca-bundlecontainer. Copies the image's system CA into the emptyDir, then appends the org CA IFF/org-ca/<key>exists (so missing trust-manager is a no-op). Uses the hub image so the system CA store the init container reads matches what the hub container has at runtime.extraEnv— adds all five CA env vars pointing at the merged file:REQUESTS_CA_BUNDLE,SSL_CERT_FILE,NODE_EXTRA_CA_CERTS,CURL_CA_BUNDLE,GIT_SSL_CAINFO. Rust tooling (rustls-native-certs) honoursSSL_CERT_FILEbut does not iterateSSL_CERT_DIR, so the explicit file path is required.Also updates the
trust-bundle-enableddocstring to describe the hub-side wiring and cross-references the two places that now consumetrust-bundle-configmap/trust-bundle-key.Design note — unconditional on the hub
Unlike the singleuser side (gated at spawn time by
custom.trust-bundle-enabledvia a Python hook), the hub-side wiring is unconditional. z2jh renders the hub Deployment from static values, so runtime gating isn't available without a chart-side helper (which would require the chart to interpose on how values reach z2jh).optional: trueon the ConfigMap and the[ -f ... ]guard on the shell append make the always-on wiring functionally identical to the singleuser disabled path on clusters without trust-manager — merged bundle equals system bundle, env vars point at a file that matches the system trust store, no behaviour change. Costs one extra init container run per hub pod start (< 1s wall-clock on typical images).Deployers who need to opt out entirely can override
hub.extraVolumesetc. in their own values, subject to the existing warning about re-including the pre-existing chart entries.Follow-up work worth flagging
custom.trust-bundle-configmapandcustom.trust-bundle-keynow drive TWO sites (singleuser Python hook reads them at runtime; hub-side static YAML references the same default explicitly). Changing them requires editing values in two places until a chart helper binds them. Kept the duplication out of scope for this PR to keep the diff surgical, but happy to add the helper in a follow-up if reviewers prefer.Tests
tests/unit/test_hub_ca_bundle.py— 12 assertions pinning the shape of thejupyterhub.hub.*block:extraVolumescontainsorg-ca(optional ConfigMap) +ca-merged(emptyDir); preserves the two pre-existing chart entries (custom-config,oauth-client)extraVolumeMountsexposesca-mergedat/etc/ssl/certs-extra; preserves pre-existing entriesinitContainershasmerge-ca-bundle; it uses the hub image; the shell command copies system CA and conditionally appends org CA; volume mounts are wired correctlyextraEnvsets all five CA env vars to the merged bundle path; preserves the existing OIDC-client-secret entryorg-caConfigMap name matches the defaultcustom.trust-bundle-configmapvalue (drift detector)Runs standalone (pyyaml + pytest only, no chart deps):
Verification
Not deployed end-to-end yet — will be validated as part of the downstream deploy that motivated this. Change is declarative YAML plus a values-shape unit test; the maintainers' existing CI (helm lint / chart-render) should cover the rest. Happy to iterate if anything doesn't render cleanly.