smoll-harvester
smoll-harvester is a nested RKE2 cluster provisioned on top of the parent Harvester cluster via Rancher’s node driver (k8s.py). It exists to run k8s workloads that authenticate to Vault using their own service account tokens.
Scoped Rancher credentials
Rather than handing the admin rancher2:tokenKey to every consumer, scoped_credential.py provides reusable building blocks for minting dedicated low-privilege Rancher identities:
scoped_cluster_user(username, cluster_id, role_template_id, ...)— creates a Rancher user with login-only global access plus one cluster-scoped RBAC binding. Returns aScopedClusterUser.cluster_user_token(scoped_user, cluster_id, ...)— mints a non-expiring login token for that user via the customRancherUserTokendynamic resource (nativerancher2.Tokencan only mint tokens for the provider’s own identity).
scoped_harvester_kubeconfig composes them into the provisioner credential for the parent Harvester cluster (node driver), scoped to the smoll-harvester-vms project + read access to the shared image/network namespace. Feeds the harvester CloudCredential.
The two other wrappers that used to exist here — scoped_smoll_harvester_token (DebugVM’s kubeconfig) and scoped_vault_k8s_token (the dead kubernetes auth backend’s token_reviewer_jwt) — were removed along with their consumers; see DebugVM removal below.
Stale-token self-heal: RancherUserToken.diff() now probes the stored token against /v3/users?me=true and forces a replace if it 401/403s, since a dead token otherwise sits in state forever (none of api_url/username/password ever change to trigger a natural replace). See pulumi-up-401-stale-rancher-usertoken.
Kubeconfig Output plumbing: rancher_proxy_kubeconfig now resolves api_url/cluster_id inside .apply() rather than f-string-interpolating them directly, since either can be an unresolved pulumi.Output (e.g. ClusterV2.cluster_v1_id).
Cluster hardening changes
reserved_memory_size="-1"added to the harvester node driver config.ignore_changes=["harvesterCredentialConfig"]on the harvesterCloudCredential— Rancher ≥2.8.5 auto-renews the embedded token in place, so Pulumi no longer needs to push a fresh kubeconfig on every token rotation. This also sidesteps a preview hard-failure when the provisioner token is mid-replacement (stale-token self-heal above) and its value is unknown during preview.anonymous-auth=trueadded to the RKE2kube-apiserver-arg. RKE2 disables anonymous auth by default (CIS 1.2.1); Vault’s k8s auth backend needs unauthenticated access to/.well-known/openid-configurationand/openid/v1/jwksfor OIDC discovery.oidc-reviewerClusterRoleBinding scopessystem:unauthenticatedto just thesystem:service-account-issuer-discoveryClusterRole, so anonymous-auth only unlocks those two discovery endpoints and nothing else. Applied via a newsmoll_harvester_proxykubernetes provider (proxied straight at smoll-harvester itself, not the rancher local/management cluster).
JWT auth for workload identities (replaces k8s auth backend)

The original plan was Vault’s kubernetes auth backend (TokenReview against the apiserver). That failed testing — see vault-jwt-auth-over-kubernetes-auth — and has been superseded by Vault’s JWT auth backend, which validates smoll-harvester service account tokens directly against the cluster’s JWKS rather than calling back into the apiserver.
K8sApiProxy
New component: k8s_api_proxy.py. Deploys a Flatcar VM on the parent Harvester cluster (shares harvester-public-net with smoll-harvester nodes) running a Cloudflare Tunnel container in HTTP reverse-proxy mode (--url, not raw TCP passthrough) pointed at a smoll-harvester node’s apiserver (:6443).
Key design points:
- HTTP mode means Cloudflare’s edge terminates TLS with its own publicly-trusted cert; the origin hop (cloudflared → node apiserver) uses
--no-tls-verifysince the node presents a self-signed, periodically-rotatingrke2-server-cacert. This means Vault’s JWT auth backend can fetchjwks_urlover normal public trust — nojwks_ca_pem, so CA rotation no longer requires updating Vault config. - Two Cloudflare Access applications gate the hostname:
- Hostname-wide app requiring a Cloudflare Access Service Token (for non-browser callers).
- A path-scoped app for exactly
/openid/v1/jwkswith abypasspolicy (no auth), since Vault’s JWT auth backend can’t send Access service-token headers. Access matches the most specific application, so this doesn’t widen access elsewhere. Defense in depth is provided by the apiserver’s own anonymous-auth RBAC scoping (see above).
node_ipis currently hardcoded (10.10.180.49, set viapulumi config set ggl:smoll_harvester_node_ip) pending a proper lookup — marked debug-only.
Vault backends
kubernetesauth backend — removed. It was already dead (TokenReview failed testing) and depended entirely onDebugVM’s internal IP forkubernetes_host, so it was deleted along withDebugVMrather than kept as unreachable code.scoped_vault_k8s_token/VAULT_TOKEN_USER(itstoken_reviewer_jwthelper) were removed fromscoped_credential.pytoo.k8sjwtauth backend (active path) —pvault.jwt.AuthBackendwithjwks_urlpointed atK8sApiProxy.jwks_url. Ak8sworkloadrole is bound tobound_audiences=["https://kubernetes.default.svc.cluster.local"]and hardcodedbound_subject="system:serviceaccount:default:default"(debug/PoC only — multi-user path mapping is a later problem), grantingtoken_policies=["default", "k8sworkload"]with a 15-minute TTL. Left untouched during the DebugVM cleanup.k8sworkload.hcl(k8sworkload.hcl) — policy hardcoded toworkspaces/*/test@hul.to/*paths (list/read/create/update/delete on the KV v2 mount), mirroring the debug-only role binding above. Also left untouched.
DebugVM removed
services/debug_vm/ (the GCP f1-micro COS VM used to smoke-test K8sApiProxy’s public JWKS path) has been deleted entirely, along with everything that only existed to support it:
- The
DebugVMimport/instantiation anddebug_vm_ipexport in__main__.py. - The dead
kubernetesVault auth backend block (see above) — itskubernetes_hostwas built fromdebug_vm.internal_ip, so it couldn’t survive without it. scoped_smoll_harvester_token/DEBUG_TOKEN_USERandscoped_vault_k8s_token/VAULT_TOKEN_USERinscoped_credential.py.- Stale
DebugVMmentions inK8sApiProxy’s docstring/comments.
The node_ip hardcoded-IP TODO in K8sApiProxy and the debug-only hardcoding in k8sjwt-k8sworkload/k8sworkload.hcl are unrelated to DebugVM and were intentionally left in place.
End-to-end result
Per commit ef4fbbe, an e2e test confirms k8s workloads on smoll-harvester can authenticate to Vault using their own service account JWT via the k8sjwt backend.
Cloudflare wiring
common/cloudflare/tunnels.py’sTunnelcomponent is now reused byK8sApiProxy(previously only used elsewhere).setup_cloudflare_vault_authin__main__.py— theZeroTrustAccessIdentityProviderregistering Vault as a Cloudflare Access OIDC identity provider is now live (previously commented-outTODO).- README documents the Cloudflare API token permissions needed: Tunnel edit, Access Apps/Policies edit, Access Identity Providers edit, Access Service Tokens edit, Zone DNS edit, Zone read.
CFTUNNEL_IMAGEbumped2026.3.0→2026.7.2.