Skip to content

Migrate scan-scheduling CronJobs off unauthenticated /v1/triggerAction to OperatorCommand CRDs #412

Open

Description

@matthyx

Background

/v1/triggerAction (restapihandler/triggeraction.go, port 4002) is an unauthenticated HTTP endpoint — no caller identity check, and its Service (operator:4002, named trigger-port) carries no NetworkPolicy. Verified live on armo-dev-stage: an anonymous, tokenless POST from a throwaway in-cluster pod returns 200/ok with no auth challenge at any layer.

Its only legitimate callers, confirmed live:

CronJob commandName sent
kubescape-scheduler kubescapeScan
kubevuln-scheduler scan
registry-scan CronJob (created dynamically by watcher/registryhandler.go when registry scanning is configured) scanRegistryV2

All three run quay.io/kubescape/http-request, POSTing a fixed request body to operator:4002/v1/triggerAction with automountServiceAccountToken: false — i.e. no credentials at all.

#411 adds a CommandName allowlist as a stopgap, closing off the immediate risk (an unauthenticated caller directing operatorAction/remediation commands, including patch from #410, at the operator's cluster-wide remediation RBAC). But the endpoint itself remains unauthenticated for the scan commands it does allow.

The real fix

The operator already has an authenticated, RBAC-gated channel for exactly this kind of command delivery: the synchronizer component holds its own K8s ServiceAccount with a ClusterRole explicitly granting create/update/patch/delete on operatorcommands.kubescape.io, reached only through its own authenticated outbound connection to the backend. It creates OperatorCommand custom resources directly, which the operator's existing CRD watcher (watcher/commandswatcher.go) already picks up — no HTTP listener involved.

The scan-scheduling CronJobs should do the same instead of an unauthenticated HTTP callback:

  1. Grant kubescape-scheduler's and kubevuln-scheduler's ServiceAccounts (kubescape and kubevuln — they already exist) a narrowly-scoped Role: create on operatorcommands.kubescape.io, namespaced to kubescape only. (The registry-scan CronJob, created dynamically, would need the same.)
  2. Replace the http-request container with something that creates the OperatorCommand CR directly (e.g. a small kubectl apply-based image + script, the same pattern the chart's own certgen-create/certgen-patch init containers already use for a different resource).
  3. Once migrated, delete /v1/triggerAction entirely — the route in restapihandler/restapi.go, and ActionRequest/HandleActionRequest in restapihandler/triggeraction.go — removing the unauthenticated HTTP surface instead of just narrowing it.

This is strictly better than adding application-level auth (TokenReview/SubjectAccessReview) to the endpoint: it reuses Kubernetes' own authentication and authorization, which is already the trust boundary for the equivalent synchronizer-driven path, and it removes the attack surface rather than policing it.

Scope note

Steps 1–2 require changes to the kubescape-operator Helm chart (a separate repo from this one) — the CronJob templates, their ServiceAccount RBAC, and the request-body ConfigMaps all live there. Step 3 (deleting the endpoint) is this repo's change, and should land only once the chart-side migration has shipped and rolled out, so no live CronJob is left calling a route that no longer exists.

Related

Activity

  1. matthyx commented on Sep 4, 2026

    @matthyx
    ContributorAuthor

    Correction: the endpoint itself cannot be deleted

    I need to walk back part of this issue's original framing. I've since found designs-and-proposals/cli-cluster-operations.md, which documents /v1/triggerAction as the intended transport for the kubescape CLI's operator remediate annotate|quarantine|revert subcommand (reached via kubectl port-forward, itself RBAC-gated). This isn't a narrow CronJob-only integration detail — it's a first-class, documented feature. Step 3 below (deleting the endpoint) is no longer correct as a goal; it's out unless/until the CLI itself is changed to use a different transport, which is a much larger, separate effort with its own tradeoffs (a human running the CLI from their laptop can't as conveniently "create an OperatorCommand CR" without something equivalent to what kubectl apply already offers, which the design doc notes is already an alternative path).

    What's still valid and still worth doing: migrating the three scan-scheduling CronJobs (kubescape-scheduler, kubevuln-scheduler, the dynamically-created registry-scan CronJob) off this endpoint onto direct OperatorCommand CR creation, per steps 1–2 below — that part doesn't depend on the CLI at all and still closes real attack surface for those specific callers.

    What replaces step 3: the actual fix for the endpoint's lack of caller authentication is a NetworkPolicy in kubescape/helm-charts restricting external (Service/pod-network) ingress to port 4002 — kubectl port-forward traffic never crosses the pod network a NetworkPolicy governs, so this closes the vulnerability (any pod hitting the Service directly) without breaking the CLI's port-forward-based flow. That should be tracked as its own issue against kubescape/helm-charts rather than bundled with the CronJob migration here.

    Separately: the immediate risk that motivated all of this — the new patch action (#410) being reachable through this endpoint with no other actions' safety net — is now addressed directly in #410 by gating patch on the OperatorCommand CRD delivery path specifically (since patch, unlike annotate/quarantine/revert, was never part of the CLI design and has no legitimate reason to arrive via triggerAction).

  2. moved this to Feature in Kubescapingon Sep 29, 2026
  3. blacksoxx commented on Oct 14, 2026

    @blacksoxx
    Contributor

    Taking steps 1 and 2 on the chart side, PR on kubescape/helm-charts in a moment. The three CronJobs (kubescape-scheduler, kubevuln-scheduler and the registry template) run a kubectl plus jq script that turns request-body.json into OperatorCommand objects of type OperatorAPI, labelled kubescape.io/app-name=operator and owned by their Job so the history limits clean them up. Namespaced Roles on the kubescape and kubevuln ServiceAccounts (create on operatorcommands, get on jobs), and the scheduler NetworkPolicies egress to the API server instead of 4002. No operator change is needed for that part.

    For the endpoint itself, an alternative to the NetworkPolicy: bind the trigger listener to loopback. The CLI reaches it through a pod port-forward (core/core/clusterconnector.go), which dials the pod's loopback, so it keeps working, while nothing on the pod network can reach 4002 even with global.networkPolicy.enabled=false. I will open that here as a config option with the current default unchanged, so the default can flip once the chart side has shipped.

  4. blacksoxx commented on Oct 16, 2026

    @blacksoxx
    Contributor

    Steps 1 and 2 are merged on the chart side: kubescape/helm-charts#964 (2026-10-16) moves the kubescape, kubevuln and registry scheduler CronJobs to creating OperatorCommand objects with the namespaced Roles and the API server egress, on top of operator v0.2.188, which carries the parent job id repair from #435. The loopback option for the trigger listener is in since #431 (bindAddress, default unchanged).

    Left for after a chart release that contains #964: flipping that default, or setting it from the chart, and dropping the trigger-port ingress rule from the operator NetworkPolicy once no old CronJob can still exist. The NetworkPolicy for port 4002 you described stays its own helm-charts item.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions