smoll-harvester
smoll-harvester is a nested RKE2 cluster provisioned on top of the parent Harvester cluster via Rancher’s node driver (smoll_harvester.py). It exists to run k8s workloads that authenticate to Vault using their own service account tokens.
VM networks
harvester-public-net and maldev are created by networks.py (create_all_networks()), alongside the images in images.py — before this they were a manual post-rebuild step, and a missing harvester-public-net NAD silently stalls smoll-harvester at “Waiting for viable init node” while the node driver’s pre-create check fails and CAPI loops create → delete.
Each network gets its own Clusternetwork plus a per-node Vlanconfig (NIC names differ per host, so each node is selected by kubernetes.io/hostname) and is attached untagged (vlan_id 0) — the Unifi port already carries the tag. The mgmt bond is never an uplink here; it carries Ceph replication and stays host-only. inference-84 has a single free wired NIC, which goes to harvester-public-net, so maldev covers compute-82/compute-83 only.
Cluster hardening changes
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 backend
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.