feat(security): protect collector ServiceAccounts via ValidatingAdmissionPolicy - #3405
feat(security): protect collector ServiceAccounts via ValidatingAdmissionPolicy#3405vparfonov wants to merge 1 commit into
Conversation
…sionPolicy Restrict protected collector ServiceAccounts so only CLO-managed workloads (the operator and the built-in controllers that deploy the collector) may run a Pod under them. This closes the path where a user who can create Pods reuses a collector SA to inherit its logging-scc privileges (e.g. hostPath node access), even if they reproduce the collector's visible Pod metadata. (CVE-2026-10609, LOG-9714/LOG-9441) Two ValidatingAdmissionPolicies (Pods, workloads) key on the non-forgeable request.userInfo.username rather than Pod metadata. Protected SAs and allowed creator identities are fed to CEL via the clo-protected-serviceaccounts param ConfigMap, which the operator rebuilds from the current ClusterLogForwarder list on every CLF event. Bindings use parameterNotFoundAction: Allow to avoid operator self-lockout. Enforced as hard Deny with zero upgrade breakage: the only legitimate creators are stable identities (operator SA + kube controllers) that are allow-listed, so existing CLF users and running collectors are unaffected. CLF-layer controls (forward logs you cannot read; exfiltrate the SA token) are scoped out and documented as follow-ups in docs/design. Coverage: unit (fake client) + envtest (real kube-apiserver, CEL compiled) + e2e. Adds a ValidatingAdmissionPolicy how-to guide for newcomers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Skipping CI for Draft Pull Request. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: vparfonov The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
| ENVTEST_K8S_VERSION ?= 1.31.0 | ||
| .PHONY: test-admission-envtest | ||
| test-admission-envtest: | ||
| KUBEBUILDER_ASSETS="$$(go run sigs.k8s.io/controller-runtime/tools/setup-envtest@release-0.23 use $(ENVTEST_K8S_VERSION) -p path)" \ |
There was a problem hiding this comment.
I'm guessing we could use this for our other admissions tests?
| "strings" | ||
| "time" | ||
|
|
||
| internaladmission "github.com/openshift/cluster-logging-operator/internal/admission" |
There was a problem hiding this comment.
We should think if this should be in the controller package? Maybe that package is only for our resources?
| ### 3. A Pod or workload is denied: `uses protected collector ServiceAccount ... may only be created by a CLO-managed collector controller` | ||
|
|
||
| The cluster logging operator installs ValidatingAdmissionPolicies that prevent a | ||
| collector ServiceAccount (one referenced by a `ClusterLogForwarder`) from being |
There was a problem hiding this comment.
...being refused.... It should probably say it is refused if not used as we intend. Something to that affect
| @@ -0,0 +1,2 @@ | |||
| resources: | |||
| - ../../internal/admission/manifests | |||
There was a problem hiding this comment.
Seeing the generated bundle which provides the RBAC, if these are being managed by a controller and not deployable in the bundle, then we should not require them here. This and the associated Make target can be removed.
| expression: "(has(params.data) && ('podCreators' in params.data)) ? params.data['podCreators'].split(',') : []" | ||
| validations: | ||
| - expression: "!variables.isProtected || (request.userInfo.username in variables.allowedCreators)" | ||
| messageExpression: "'Pod uses protected collector ServiceAccount \"' + variables.sa + '\" and may only be created by a CLO-managed collector controller'" |
There was a problem hiding this comment.
Depending where this message is manifested it may require the namespace.
Maybe it should also be something like:
"'Pod uses protected ServiceAccount \"' + variables.sa + '\" which is only allowed for use by authorized ClusterLogForwarders'"
| @@ -0,0 +1,34 @@ | |||
| package admission | |||
There was a problem hiding this comment.
This should be moved to the controller package for consistency
| func (r *ProtectedSAReconciler) SetupWithManager(mgr ctrl.Manager) error { | ||
| return ctrl.NewControllerManagedBy(mgr). | ||
| For(&obsv1.ClusterLogForwarder{}). | ||
| Named("protected-sa-configmap"). |
There was a problem hiding this comment.
We can name this "clo-protected-sa" and extract the name to a constant as i presume it is used elsewhere
|
|
||
| // OperatorNamespace returns the namespace the operator runs in, used both to | ||
| // locate the param ConfigMap and to build the operator ServiceAccount username. | ||
| func OperatorNamespace() string { |
There was a problem hiding this comment.
Similar functionality may already exist elsewhere and can be ascertained from the downward API and read from an env var if it is not already provided there
| unprotectedSA = "plain-sa" | ||
| restrictedUser = "system:serviceaccount:%s:restricted-user" | ||
|
|
||
| clfFmt = ` |
There was a problem hiding this comment.
We have programmatic builders that are better suited then string interpolation
| "validatingadmissionpolicy/" + admission.ProtectedSAWorkloadsPolicyName, | ||
| "validatingadmissionpolicybinding/" + admission.ProtectedSAWorkloadsBindingName, | ||
| } { | ||
| out, err := exec.Command("oc", "get", resource).CombinedOutput() |
There was a problem hiding this comment.
replace with existing utility functions
| }, 2*time.Minute, 5*time.Second).Should(Succeed()) | ||
| } | ||
|
|
||
| func ocCreate(namespace, yaml string) (string, error) { |
There was a problem hiding this comment.
replace with existing utilities
| return string(out), err | ||
| } | ||
|
|
||
| func grantWorkloadEditor(namespace string) { |
There was a problem hiding this comment.
replace or impl into existing utilities
| // spoofedPodYAML copies the collector's visible metadata to prove that spoofing | ||
| // labels/annotations/name does not bypass the policy. | ||
| func spoofedPodYAML(namespace, sa, name string) string { | ||
| return fmt.Sprintf(` |
There was a problem hiding this comment.
replace with existing builders
|
PR needs rebase. DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
Description
Restrict protected collector ServiceAccounts so only CLO-managed workloads (the operator and the built-in controllers that deploy the collector) may run a Pod under them. This closes the path where a user who can create Pods reuses a collector SA to inherit its logging-scc privileges (e.g. hostPath node access), even if they reproduce the collector's visible Pod metadata. (CVE-2026-10609, LOG-9714/LOG-9441)
Two ValidatingAdmissionPolicies (Pods, workloads) key on the non-forgeable request.userInfo.username rather than Pod metadata. Protected SAs and allowed creator identities are fed to CEL via the clo-protected-serviceaccounts param ConfigMap, which the operator rebuilds from the current ClusterLogForwarder list on every CLF event. Bindings use parameterNotFoundAction: Allow to avoid operator self-lockout.
Enforced as hard Deny with zero upgrade breakage: the only legitimate creators are stable identities (operator SA + kube controllers) that are allow-listed, so existing CLF users and running collectors are unaffected.
CLF-layer controls (forward logs you cannot read; exfiltrate the SA token) are scoped out and documented as follow-ups in docs/design.
Coverage: unit (fake client) + envtest (real kube-apiserver, CEL compiled) + e2e. Adds a ValidatingAdmissionPolicy how-to guide for newcomers.
/cc
/assign
Links