SafetyCard Request a review

Robusta

Kubernetes Prometheus alert enrichment and automatic remediation platform with AI investigation via HolmesGPT

github.com/robusta-dev/robusta · 2026-10-03 · e6b2b56

Defense-in-depth score

1.5 / 10

Robusta's in-cluster runner holds an admin-equivalent service account and exposes an unauthenticated HTTP endpoint (/api/trigger) that runs any registered action, including arbitrary bash in a privileged hostPID pod on any node and arbitrary kubectl commands. Any pod that can reach the runner service can therefore get node root and read the output back, and the chart ships no NetworkPolicy or approval step to stop it. The Robusta relay path is properly signed, and the AI component (HolmesGPT) is off by default and lives in a separate service, but the default runner is unsafe as shipped unless the operator locks down network access and RBAC.

Safest setup

Robusta checks signatures on requests that arrive through its relay, loads no third-party playbooks by default, and ships its AI component turned off. The default chart gives the runner a cluster-wide, admin-equivalent role, an in-cluster API that any pod can reach, and no approval step, so the safest setup narrows the role with the chart's own options and builds network and pod-security boundaries around the runner.

SetupHowWhat it gives you
A namespace-scoped rolenamespaceScoped: true in the runner's rbac Helm valuesKubernetes itself keeps the runner inside its own namespace, whatever an action asks for C1
A read-only cluster roleList only read rules in the runner's overrideClusterRoles Helm valueCluster-wide write verbs exist only where you add them on purpose C1
A network policyAdd a NetworkPolicy that lets only Alertmanager and kubewatch reach the runner serviceOnly the components that feed it alerts and events can trigger actions C5
Baseline pod securitySet the Robusta namespace's Pod Security admission level to baseline or stricterThe cluster admits no privileged or host-namespace pods there, so pods the runner spawns can't take over a node C4
Built-in playbooks onlyKeep the default empty playbookReposOnly code shipped with Robusta runs inside the runner process C7
Playbook rate limitsSet rate_limit: <seconds> on each playbook trigger that accepts itAutomatic actions fire at most once per interval for the same trigger C10
Logs kept off-clusterShip the runner's container logs to a log store outside the cluster that the runner can't write toA record of errors and logged actions that outlives the runner and its namespace C9

Approving with confidence

  • There is no approval step, so send Slack action buttons only to channels whose members may take those actions; a click runs the action at once.

Handling data

  • signing_key authorizes any action through the relay; keep it in a Secret and handle it like a cluster-admin credential.
  • TRACE_INCOMING_REQUESTS logs raw incoming requests; keep it off where runner logs are shared.
  • Basic telemetry (counts and a hashed account ID) is on by default; ENABLE_TELEMETRY=false in runner.additional_env_vars turns it off.

Key gaps (3)

  1. Unauthenticated /api/trigger runs any action, including privileged node bash, and returns its output to the caller. C5 · Untrusted input blast radius
  2. The default node command-execution path creates privileged, hostPID pods with SYS_ADMIN (C4-HOSTROOT), giving host-equivalent reach. C4 · Code-execution isolation
  3. The runner's default ClusterRole can delete/patch pods and deployments cluster-wide and create pods with exec in its namespace, an admin-equivalent blast radius. C1 · Identity & least privilege

Criteria

C1 Identity & least privilege

0.15 / 1.00

The runner uses its own Kubernetes service account, but the default chart binds it cluster-wide to a role that can delete and patch pods, patch and delete Deployments, patch nodes and evict pods everywhere, and create pods (including privileged ones) and exec into them in its own namespace, which together amount to cluster-admin-equivalent power. Requests that arrive through the Robusta relay are authenticated (HMAC signature, RSA partial keys, or a light-action allowlist), but the in-cluster HTTP endpoint /api/trigger runs any registered action with no authentication at all. The pods the runner spawns (kubectl jobs, privileged node debuggers) reuse the same service account. A namespace-scoped or read-only role exists only as opt-in Helm values.

C2 Approval gates

0.00 / 1.00

There is no human approval step in front of the runner's consequential actions. Operator-configured playbooks run automatically on alerts and Kubernetes events, the in-cluster /api/trigger endpoint runs any action immediately, and relay/Slack requests execute as soon as their signature or light-action check passes. Slack buttons carry pre-signed action requests that skip the timestamp check, so anyone who can click the button in the channel triggers the action. HolmesGPT's tool-approval flag is a separate, opt-in feature of the AI service and defaults to off.

C3 Tool & action scoping

0.07 / 1.00

Actions take typed parameters, but the most powerful ones are raw passthroughs: node_bash_enricher and pod_bash_enricher run any bash string, kubectl_command runs any shell string through /bin/sh -c, http_stress_test hits any URL, and ask_holmes accepts an arbitrary Holmes URL. Every bundled action is registered and callable by default, and the only narrowing is the relay's light-action name list, which itself includes destructive actions such as delete_pod and drain and does not cover the in-cluster HTTP API.

C4 Code-execution isolation

0.00 / 1.00

Caller-supplied commands are executed with no meaningful isolation. node_bash_enricher creates a privileged pod with hostPID, SYS_ADMIN and the runner's service account on the target node and executes the command there, which is root on the node. pod_bash_enricher executes inside the target workload's own container, and kubectl_command runs the string in a stock container that holds the runner's cluster-wide token. None of this is gated or sandboxed, and the chart does not set a restricted policy for these pods.

C5 Untrusted input blast radius

0.00 / 1.00

The runner acts on input it cannot authenticate: Alertmanager webhooks, Kubernetes events and the /api/trigger endpoint all accept unauthenticated POSTs from anywhere inside the cluster. The chart ships no NetworkPolicy, and the server binds to 0.0.0.0. Through /api/trigger, any pod in the cluster can run node_bash_enricher or kubectl_command and, with sync_response, get the output back in the HTTP response. That combines data exfiltration with irreversible actions, and no human is involved. Nothing in the code tracks where an input came from or how far it is trusted. When HolmesGPT is enabled, alert labels are also pasted into the AI prompt.

C6 Memory, context & configuration integrity

0.25 / 1.00

The runner has no AI memory, and it does not load configuration from any workspace. Its playbook configuration comes from a Kubernetes Secret that its own role cannot update. However, the role can patch Deployments cluster-wide, including the runner's own, and can create ConfigMaps in its namespace. A caller of /api/trigger can therefore use kubectl_command to rewrite the runner's environment or mount a different playbook config. That change survives restarts and can trigger later actions. No code checks or reverts changes to the runner's own configuration.

C7 Third-party extensions

0.25 / 1.00

By default the runner loads no third-party playbook packages. When an operator adds playbookRepos, they are pip-installed from a git branch or tarball URL with no hash check and imported into the runner's process. Built-in actions pull third-party container images at run time: bitnami/kubectl:latest with imagePullPolicy Always, and the untagged williamyeh/hey. The kubectl image runs with the runner's service account, so a compromised upstream image inherits the runner's cluster-wide authority.

C8 Secrets & sensitive-data protection

0.20 / 1.00

Secrets are stored in a Kubernetes Secret and passed to the runner as environment variables or a mounted config. Action parameters are partly masked in logs, and a few sinks use pydantic SecretStr, but the Slack and PagerDuty keys are plain strings. Basic telemetry is on by default and sends counts and a hashed account ID. Sentry error reporting is opt-in. The runner's service-account token, which is admin-equivalent, is mounted by default and passed to every pod the runner spawns. The signing key that authorizes relay actions is a long-lived shared secret.

C9 Audit & traceability

0.30 / 1.00

The runner does not keep a structured record of the actions it runs. Successful actions are not logged by the executor. Relay callbacks are logged only at debug level, and incoming /api/trigger requests are traced only if a debug env var is set. Failures and some individual actions (for example kubectl_command's description) are logged to stdout. Prometheus counters record counts and durations, and findings go to sinks when the operator has configured any. None of this records which caller requested an action.

C10 Limits & kill switch

0.25 / 1.00

The runner bounds its intake with a 500-entry event queue, a fixed worker pool, and per-action timeouts such as the kubectl job wait. The caller sets the kubectl timeout, and it defaults to an hour. Per-trigger rate limits exist but are opt-in per playbook. There are no overall caps on actions, time, or spawned pods. Jobs the runner creates can keep running for up to 12 hours (ttlSecondsAfterFinished). Stopping the runner does not stop pods or jobs it already launched.