Kubernetes
The container
Section titled “The container”The image is built by apps/runner/Dockerfile, with the repository root as context. The entrypoint is
apps/runner/docker-entrypoint.sh and the command is:
node --import @rootpilotsh/otel/register apps/runner/dist/index.jsThe image runs as node (uid 1000), not root — which is why Compose mounts ~/.aws at
/home/node/.aws. The mount target is coupled to the image’s USER: the SDK’s default chain resolves
the SSO cache from $HOME, and no env var repoints that cache. If you mount ~/.aws elsewhere, mount
it at the $HOME of the user the pod runs as.
Requirement 1: there is no HTTP probe
Section titled “Requirement 1: there is no HTTP probe”The runner is an outbound client. It listens on no port, exposes no /healthz, and has no
readiness endpoint.
The direct consequence: do not configure httpGet liveness or readiness probes: every one of
them will fail. If you need a signal, it comes from the boot logs (boot: ready) and from the
control-plane’s Fleet view, which sees the runner connected. Per-connector health is also reported in
the handshake, not over HTTP.
Requirement 2: termination grace longer than the drain
Section titled “Requirement 2: termination grace longer than the drain”The runner handles SIGTERM by draining: it stops accepting new work, finishes what’s in flight, and
only then exits. The ceiling is RUNNER_DRAIN_TIMEOUT_MS, which defaults to 120,000 ms.
Kubernetes defaults terminationGracePeriodSeconds to 30 s. Leave it there and SIGKILL arrives
mid-drain.
terminationGracePeriodSeconds: 150 # > RUNNER_DRAIN_TIMEOUT_MS (120s), with headroomA hard reclaim with no drain (SIGKILL, OOM, spot) loses learned state since the last flush.
RUNNER_LEARNED_FLUSH_INTERVAL_MS (default 60 s) exists to bound that loss, but it doesn’t eliminate
it.
Requirement 3: no persistent volume
Section titled “Requirement 3: no persistent volume”The runner is cattle and holds no durable state. The learned-store SQLite cache is :memory: by
default. Do not create a PVC. If you point RUNNER_LEARNED_STORE_PATH at a path on disk, know
that you’re creating a cache, not a source of truth: the source of truth is the control-plane.
Requirement 4: identity with no secret in the image
Section titled “Requirement 4: identity with no secret in the image”This is where Kubernetes changes the design, and for the better.
GKE: gke-oidc
Section titled “GKE: gke-oidc”The runner reads the projected OIDC token of the Kubernetes ServiceAccount and sends it as attestation. Nothing secret enters the image: the token is a short-lived, audience-bound JWT the kubelet projects.
The default path is /var/run/secrets/tokens/gke-oidc/token, configurable via
RUNNER_GKE_TOKEN_PATH. So the pod needs a projected volume with a serviceAccountToken mounted
exactly there — projected for the rootpilot-enroll audience:
volumes: - name: rootpilot-token projected: sources: - serviceAccountToken: path: token audience: rootpilot-enroll # the control-plane requires this audience expirationSeconds: 3600 containers: - name: rootpilot-agent volumeMounts: - name: rootpilot-token mountPath: /var/run/secrets/tokens/gke-oidc readOnly: trueThe audience isn’t decoration: it’s what stops a KSA token projected for some other service from being replayed into an enrollment. The control-plane asserts it and refuses anything that doesn’t match — including tokens projected with no audience at all.
If the volume isn’t mounted, the error says so explicitly:
RUNNER_ATTESTATION_MODE=gke-oidc: failed to read projected KSA token at/var/run/secrets/tokens/gke-oidc/token (is the Workload Identity token volume mounted?)If the audience is wrong, the volume mounts, the runner reads the token, and the enroll is
refused — the error arrives as attestation_invalid, exactly what a bad credential would
produce. Before investigating the KSA or Workload Identity, check the audience field above.
EKS: aws-sts, not eks-oidc
Section titled “EKS: aws-sts, not eks-oidc”aws-sts signs a GetCallerIdentity request using the SDK’s default credential chain, which on
EKS is IRSA. That means an annotated ServiceAccount with an IAM role is enough, with no bootstrap
token and no human in a browser.
It requires AWS_REGION explicitly. Without it, boot fails with
RUNNER_ATTESTATION_MODE=aws-sts requires AWS_REGION. The region is not guessed on purpose, because
the control-plane only accepts regions from its own allowlist, and a guess would surface as an
attestation_invalid that reads like a credential problem.
Requirement 5: secrets
Section titled “Requirement 5: secrets”Prefer the secret manager you already operate over Kubernetes Secrets:
RUNNER_SECRET_STORE_MODE=aws (AWS Secrets Manager) or vault (HashiCorp Vault KV v2). Both resolve
through workload identity, with no static keys. See Secret stores.
For AWS read credentials, RUNNER_AWS_CRED_MODE=chain makes the SDK resolve through IRSA directly,
with no key passing through the cluster at all.
Requirement 6: egress
Section titled “Requirement 6: egress”Outbound only, nothing inbound. If you use NetworkPolicy, the destination list is in Requirements. Remember that the allowlist declared in connector manifests is documentation, not enforcement: for native connectors, the cluster is what enforces egress.
What’s still missing for a chart to exist
Section titled “What’s still missing for a chart to exist”Being explicit about the gap, since the question will come up: a real chart would need decisions this
repository hasn’t made: resources values (there is no versioned benchmark) and a canonical way to
express the attestation choice per provider. (runAsNonRoot is no longer a gap: the image has run as
node/uid 1000 since the #160 hardening.) Until those
exist, writing the manifests on the installer’s side is the honest path.