Repository navigation
Description
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:
- Grant
kubescape-scheduler's andkubevuln-scheduler's ServiceAccounts (kubescapeandkubevuln— they already exist) a narrowly-scopedRole:createonoperatorcommands.kubescape.io, namespaced tokubescapeonly. (The registry-scan CronJob, created dynamically, would need the same.) - Replace the
http-requestcontainer with something that creates theOperatorCommandCR directly (e.g. a smallkubectl apply-based image + script, the same pattern the chart's owncertgen-create/certgen-patchinit containers already use for a different resource). - Once migrated, delete
/v1/triggerActionentirely — the route inrestapihandler/restapi.go, andActionRequest/HandleActionRequestinrestapihandler/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
- feat(remediation): add generic patch action for arbitrary workload patches #410 — added the
patchremediation action, which is what raised the stakes of this endpoint's lack of auth enough to investigate it. - fix(restapi): allowlist commandName on the unauthenticated triggerAction endpoint #411 — stopgap
CommandNameallowlist while this migration is pending.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsFeature
Activity
matthyx commentedon Sep 4, 2026
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/triggerActionas the intended transport for thekubescapeCLI'soperator remediate annotate|quarantine|revertsubcommand (reached viakubectl 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 anOperatorCommandCR" without something equivalent to whatkubectl applyalready 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 directOperatorCommandCR 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
NetworkPolicyinkubescape/helm-chartsrestricting external (Service/pod-network) ingress to port 4002 —kubectl port-forwardtraffic never crosses the pod network aNetworkPolicygoverns, 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 againstkubescape/helm-chartsrather than bundled with the CronJob migration here.Separately: the immediate risk that motivated all of this — the new
patchaction (#410) being reachable through this endpoint with no other actions' safety net — is now addressed directly in #410 by gatingpatchon theOperatorCommandCRD delivery path specifically (sincepatch, unlikeannotate/quarantine/revert, was never part of the CLI design and has no legitimate reason to arrive viatriggerAction).blacksoxx commentedon Oct 14, 2026
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.jsonintoOperatorCommandobjects of typeOperatorAPI, labelledkubescape.io/app-name=operatorand owned by their Job so the history limits clean them up. Namespaced Roles on thekubescapeandkubevulnServiceAccounts (createon operatorcommands,geton 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 withglobal.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.blacksoxx commentedon Oct 16, 2026
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
OperatorCommandobjects 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-portingress 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.