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 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.)
- 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).
- 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
Background
/v1/triggerAction(restapihandler/triggeraction.go, port 4002) is an unauthenticated HTTP endpoint — no caller identity check, and its Service (operator:4002, namedtrigger-port) carries no NetworkPolicy. Verified live onarmo-dev-stage: an anonymous, tokenless POST from a throwaway in-cluster pod returns200/okwith no auth challenge at any layer.Its only legitimate callers, confirmed live:
commandNamesentkubescape-schedulerkubescapeScankubevuln-schedulerscanwatcher/registryhandler.gowhen registry scanning is configured)scanRegistryV2All three run
quay.io/kubescape/http-request, POSTing a fixed request body tooperator:4002/v1/triggerActionwithautomountServiceAccountToken: false— i.e. no credentials at all.#411 adds a
CommandNameallowlist as a stopgap, closing off the immediate risk (an unauthenticated caller directingoperatorAction/remediation commands, includingpatchfrom #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
synchronizercomponent holds its own K8s ServiceAccount with aClusterRoleexplicitly grantingcreate/update/patch/deleteonoperatorcommands.kubescape.io, reached only through its own authenticated outbound connection to the backend. It createsOperatorCommandcustom 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:
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.)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)./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-operatorHelm 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
patchremediation action, which is what raised the stakes of this endpoint's lack of auth enough to investigate it.CommandNameallowlist while this migration is pending.