Skip to content

Commit a65c34b

Browse files
committed
UPSTREAM: <carry>: STOR-829: Add CSIInlineVolumeSecurity admission plugin
The CSIInlineVolumeSecurity admission plugin inspects inline CSI volumes on pod creation and compares the security.openshift.io/csi-ephemeral-volume-profile label on the CSIDriver object to the pod security profile on the namespace.
1 parent 761cfe9 commit a65c34b

File tree

5 files changed

+823
-1
lines changed

5 files changed

+823
-1
lines changed

openshift-kube-apiserver/admission/admissionenablement/register.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"k8s.io/kubernetes/openshift-kube-apiserver/admission/route/hostassignment"
2222
projectnodeenv "k8s.io/kubernetes/openshift-kube-apiserver/admission/scheduler/nodeenv"
2323
schedulerpodnodeconstraints "k8s.io/kubernetes/openshift-kube-apiserver/admission/scheduler/podnodeconstraints"
24+
"k8s.io/kubernetes/openshift-kube-apiserver/admission/storage/csiinlinevolumesecurity"
2425
)
2526

2627
func RegisterOpenshiftKubeAdmissionPlugins(plugins *admission.Plugins) {
@@ -38,6 +39,7 @@ func RegisterOpenshiftKubeAdmissionPlugins(plugins *admission.Plugins) {
3839
sccadmission.RegisterSCCExecRestrictions(plugins)
3940
externalipranger.RegisterExternalIP(plugins)
4041
restrictedendpoints.RegisterRestrictedEndpoints(plugins)
42+
csiinlinevolumesecurity.Register(plugins)
4143
}
4244

4345
var (
@@ -67,7 +69,8 @@ var (
6769
"security.openshift.io/SecurityContextConstraint",
6870
"security.openshift.io/SCCExecRestrictions",
6971
"route.openshift.io/IngressAdmission",
70-
hostassignment.PluginName, // "route.openshift.io/RouteHostAssignment"
72+
hostassignment.PluginName, // "route.openshift.io/RouteHostAssignment"
73+
csiinlinevolumesecurity.PluginName, // "storage.openshift.io/CSIInlineVolumeSecurity"
7174
}
7275

7376
// openshiftAdmissionPluginsForKubeAfterResourceQuota are the plugins to add after ResourceQuota plugin
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
package csiinlinevolumesecurity
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"k8s.io/apimachinery/pkg/runtime/schema"
11+
"k8s.io/apimachinery/pkg/util/validation/field"
12+
"k8s.io/apiserver/pkg/admission"
13+
"k8s.io/apiserver/pkg/admission/initializer"
14+
"k8s.io/apiserver/pkg/audit"
15+
"k8s.io/apiserver/pkg/warning"
16+
"k8s.io/client-go/informers"
17+
corev1listers "k8s.io/client-go/listers/core/v1"
18+
storagev1listers "k8s.io/client-go/listers/storage/v1"
19+
"k8s.io/component-base/featuregate"
20+
"k8s.io/klog/v2"
21+
appsapi "k8s.io/kubernetes/pkg/apis/apps"
22+
batchapi "k8s.io/kubernetes/pkg/apis/batch"
23+
coreapi "k8s.io/kubernetes/pkg/apis/core"
24+
"k8s.io/kubernetes/pkg/features"
25+
podsecapi "k8s.io/pod-security-admission/api"
26+
)
27+
28+
const (
29+
// Plugin name
30+
PluginName = "storage.openshift.io/CSIInlineVolumeSecurity"
31+
// Label on the CSIDriver to declare the driver's effective pod security profile
32+
csiInlineVolProfileLabel = "security.openshift.io/csi-ephemeral-volume-profile"
33+
// Default values for the profile labels when no such label exists
34+
defaultCSIInlineVolProfile = podsecapi.LevelPrivileged
35+
defaultPodSecEnforceProfile = podsecapi.LevelRestricted
36+
defaultPodSecWarnProfile = podsecapi.LevelRestricted
37+
defaultPodSecAuditProfile = podsecapi.LevelRestricted
38+
)
39+
40+
var (
41+
podSpecResources = map[schema.GroupResource]bool{
42+
coreapi.Resource("pods"): true,
43+
coreapi.Resource("replicationcontrollers"): true,
44+
coreapi.Resource("podtemplates"): true,
45+
appsapi.Resource("replicasets"): true,
46+
appsapi.Resource("deployments"): true,
47+
appsapi.Resource("statefulsets"): true,
48+
appsapi.Resource("daemonsets"): true,
49+
batchapi.Resource("jobs"): true,
50+
batchapi.Resource("cronjobs"): true,
51+
}
52+
)
53+
54+
var _ = initializer.WantsExternalKubeInformerFactory(&csiInlineVolSec{})
55+
var _ = initializer.WantsFeatures(&csiInlineVolSec{})
56+
var _ = admission.ValidationInterface(&csiInlineVolSec{})
57+
58+
func Register(plugins *admission.Plugins) {
59+
plugins.Register(PluginName,
60+
func(config io.Reader) (admission.Interface, error) {
61+
return &csiInlineVolSec{
62+
Handler: admission.NewHandler(admission.Create),
63+
}, nil
64+
})
65+
}
66+
67+
// csiInlineVolSec validates whether the namespace has permission to use a given
68+
// CSI driver as an inline volume.
69+
type csiInlineVolSec struct {
70+
*admission.Handler
71+
enabled bool
72+
inspectedFeatureGates bool
73+
defaultPolicy podsecapi.Policy
74+
nsLister corev1listers.NamespaceLister
75+
nsListerSynced func() bool
76+
csiDriverLister storagev1listers.CSIDriverLister
77+
csiDriverListSynced func() bool
78+
podSpecExtractor PodSpecExtractor
79+
}
80+
81+
// SetExternalKubeInformerFactory registers an informer
82+
func (c *csiInlineVolSec) SetExternalKubeInformerFactory(kubeInformers informers.SharedInformerFactory) {
83+
c.nsLister = kubeInformers.Core().V1().Namespaces().Lister()
84+
c.nsListerSynced = kubeInformers.Core().V1().Namespaces().Informer().HasSynced
85+
c.csiDriverLister = kubeInformers.Storage().V1().CSIDrivers().Lister()
86+
c.csiDriverListSynced = kubeInformers.Storage().V1().CSIDrivers().Informer().HasSynced
87+
c.podSpecExtractor = &OCPPodSpecExtractor{}
88+
c.SetReadyFunc(func() bool {
89+
return c.nsListerSynced() && c.csiDriverListSynced()
90+
})
91+
92+
// set default pod security policy
93+
c.defaultPolicy = podsecapi.Policy{
94+
Enforce: podsecapi.LevelVersion{
95+
Level: defaultPodSecEnforceProfile,
96+
Version: podsecapi.GetAPIVersion(),
97+
},
98+
Warn: podsecapi.LevelVersion{
99+
Level: defaultPodSecWarnProfile,
100+
Version: podsecapi.GetAPIVersion(),
101+
},
102+
Audit: podsecapi.LevelVersion{
103+
Level: defaultPodSecAuditProfile,
104+
Version: podsecapi.GetAPIVersion(),
105+
},
106+
}
107+
}
108+
109+
func (c *csiInlineVolSec) InspectFeatureGates(featureGates featuregate.FeatureGate) {
110+
c.enabled = featureGates.Enabled(features.CSIInlineVolumeAdmission)
111+
c.inspectedFeatureGates = true
112+
}
113+
114+
func (c *csiInlineVolSec) ValidateInitialization() error {
115+
if !c.inspectedFeatureGates {
116+
return fmt.Errorf("%s did not see feature gates", PluginName)
117+
}
118+
if c.nsLister == nil {
119+
return fmt.Errorf("%s plugin needs a namespace lister", PluginName)
120+
}
121+
if c.nsListerSynced == nil {
122+
return fmt.Errorf("%s plugin needs a namespace lister synced", PluginName)
123+
}
124+
if c.csiDriverLister == nil {
125+
return fmt.Errorf("%s plugin needs a node lister", PluginName)
126+
}
127+
if c.csiDriverListSynced == nil {
128+
return fmt.Errorf("%s plugin needs a node lister synced", PluginName)
129+
}
130+
if c.podSpecExtractor == nil {
131+
return fmt.Errorf("%s plugin needs a pod spec extractor", PluginName)
132+
}
133+
return nil
134+
}
135+
136+
func (c *csiInlineVolSec) PolicyToEvaluate(labels map[string]string) (podsecapi.Policy, field.ErrorList) {
137+
return podsecapi.PolicyToEvaluate(labels, c.defaultPolicy)
138+
}
139+
140+
func (c *csiInlineVolSec) Validate(ctx context.Context, attrs admission.Attributes, o admission.ObjectInterfaces) error {
141+
// Only validate if feature gate is enabled
142+
if !c.enabled {
143+
return nil
144+
}
145+
// Only validate applicable resources
146+
gr := attrs.GetResource().GroupResource()
147+
if !podSpecResources[gr] {
148+
return nil
149+
}
150+
// Do not validate subresources
151+
if attrs.GetSubresource() != "" {
152+
return nil
153+
}
154+
155+
// Get namespace
156+
namespace, err := c.nsLister.Get(attrs.GetNamespace())
157+
if err != nil {
158+
return admission.NewForbidden(attrs, fmt.Errorf("failed to get namespace: %v", err))
159+
}
160+
// Require valid labels if they exist (the default policy is always valid)
161+
nsPolicy, nsPolicyErrs := c.PolicyToEvaluate(namespace.Labels)
162+
if len(nsPolicyErrs) > 0 {
163+
return admission.NewForbidden(attrs, fmt.Errorf("invalid policy found on namespace %s: %v", namespace, nsPolicyErrs))
164+
}
165+
// If the namespace policy is fully privileged, no need to evaluate further
166+
// because it is allowed to use any inline volumes.
167+
if nsPolicy.FullyPrivileged() {
168+
return nil
169+
}
170+
171+
// Extract the pod spec to evaluate
172+
obj := attrs.GetObject()
173+
podMeta, podSpec, err := c.podSpecExtractor.ExtractPodSpec(obj)
174+
if err != nil {
175+
return admission.NewForbidden(attrs, fmt.Errorf("failed to extract pod spec: %v", err))
176+
}
177+
// If an object with an optional pod spec does not contain a pod spec, skip validation
178+
if podMeta == nil && podSpec == nil {
179+
return nil
180+
}
181+
182+
klogV := klog.V(5)
183+
if klogV.Enabled() {
184+
klogV.InfoS("CSIInlineVolumeSecurity evaluation", "policy", fmt.Sprintf("%v", nsPolicy), "op", attrs.GetOperation(), "resource", attrs.GetResource(), "namespace", attrs.GetNamespace(), "name", attrs.GetName())
185+
}
186+
187+
// For each inline volume, find the CSIDriver and ensure the profile on the
188+
// driver is allowed by the pod security profile on the namespace.
189+
// If it is not: create errors, warnings, and audit as defined by policy.
190+
for _, vol := range podSpec.Volumes {
191+
// Only check for inline volumes
192+
if vol.CSI == nil {
193+
continue
194+
}
195+
196+
// Get the policy level for the CSIDriver
197+
driverName := vol.CSI.Driver
198+
driverLevel, err := c.getCSIDriverLevel(driverName)
199+
if err != nil {
200+
return admission.NewForbidden(attrs, err)
201+
}
202+
203+
// Compare CSIDriver level to the policy for the namespace
204+
if podsecapi.CompareLevels(nsPolicy.Enforce.Level, driverLevel) > 0 {
205+
// Not permitted, enforce error and deny admission
206+
return admission.NewForbidden(attrs, fmt.Errorf("admission denied: pod %s uses an inline volume provided by CSIDriver %s and namespace %s has a pod security enforce level that is lower than %s", podMeta.Name, driverName, namespace.Name, driverLevel))
207+
}
208+
if podsecapi.CompareLevels(nsPolicy.Warn.Level, driverLevel) > 0 {
209+
// Violates policy warn level, add warning
210+
warning.AddWarning(ctx, "", fmt.Sprintf("pod %s uses an inline volume provided by CSIDriver %s and namespace %s has a pod security warn level that is lower than %s", podMeta.Name, driverName, namespace.Name, driverLevel))
211+
}
212+
if podsecapi.CompareLevels(nsPolicy.Audit.Level, driverLevel) > 0 {
213+
// Violates policy audit level, add audit annotation
214+
auditMessageString := fmt.Sprintf("pod %s uses an inline volume provided by CSIDriver %s and namespace %s has a pod security audit level that is lower than %s", podMeta.Name, driverName, namespace.Name, driverLevel)
215+
audit.AddAuditAnnotation(ctx, PluginName, auditMessageString)
216+
}
217+
}
218+
219+
return nil
220+
}
221+
222+
// getCSIDriverLevel returns the effective policy level for the CSIDriver.
223+
// If the driver is found and it has the label, use that policy.
224+
// If the driver or the label is missing, default to the privileged policy.
225+
func (c *csiInlineVolSec) getCSIDriverLevel(driverName string) (podsecapi.Level, error) {
226+
driverLevel := defaultCSIInlineVolProfile
227+
driver, err := c.csiDriverLister.Get(driverName)
228+
if err != nil {
229+
return driverLevel, nil
230+
}
231+
232+
csiDriverLabel, ok := driver.ObjectMeta.Labels[csiInlineVolProfileLabel]
233+
if !ok {
234+
return driverLevel, nil
235+
}
236+
237+
driverLevel, err = podsecapi.ParseLevel(csiDriverLabel)
238+
if err != nil {
239+
return driverLevel, fmt.Errorf("invalid label %s for CSIDriver %s: %v", csiInlineVolProfileLabel, driverName, err)
240+
}
241+
242+
return driverLevel, nil
243+
}
244+
245+
// PodSpecExtractor extracts a PodSpec from pod-controller resources that embed a PodSpec.
246+
// This is the same as what is used in the pod-security-admission plugin (see
247+
// staging/src/k8s.io/pod-security-admission/admission/admission.go) except here we
248+
// are provided coreapi resources instead of corev1, which changes the interface.
249+
type PodSpecExtractor interface {
250+
// HasPodSpec returns true if the given resource type MAY contain an extractable PodSpec.
251+
HasPodSpec(schema.GroupResource) bool
252+
// ExtractPodSpec returns a pod spec and metadata to evaluate from the object.
253+
// An error returned here does not block admission of the pod-spec-containing object and is not returned to the user.
254+
// If the object has no pod spec, return `nil, nil, nil`.
255+
ExtractPodSpec(runtime.Object) (*metav1.ObjectMeta, *coreapi.PodSpec, error)
256+
}
257+
258+
type OCPPodSpecExtractor struct{}
259+
260+
func (OCPPodSpecExtractor) HasPodSpec(gr schema.GroupResource) bool {
261+
return podSpecResources[gr]
262+
}
263+
264+
func (OCPPodSpecExtractor) ExtractPodSpec(obj runtime.Object) (*metav1.ObjectMeta, *coreapi.PodSpec, error) {
265+
switch o := obj.(type) {
266+
case *coreapi.Pod:
267+
return &o.ObjectMeta, &o.Spec, nil
268+
case *coreapi.PodTemplate:
269+
return extractPodSpecFromTemplate(&o.Template)
270+
case *coreapi.ReplicationController:
271+
return extractPodSpecFromTemplate(o.Spec.Template)
272+
case *appsapi.ReplicaSet:
273+
return extractPodSpecFromTemplate(&o.Spec.Template)
274+
case *appsapi.Deployment:
275+
return extractPodSpecFromTemplate(&o.Spec.Template)
276+
case *appsapi.DaemonSet:
277+
return extractPodSpecFromTemplate(&o.Spec.Template)
278+
case *appsapi.StatefulSet:
279+
return extractPodSpecFromTemplate(&o.Spec.Template)
280+
case *batchapi.Job:
281+
return extractPodSpecFromTemplate(&o.Spec.Template)
282+
case *batchapi.CronJob:
283+
return extractPodSpecFromTemplate(&o.Spec.JobTemplate.Spec.Template)
284+
default:
285+
return nil, nil, fmt.Errorf("unexpected object type: %s", obj.GetObjectKind().GroupVersionKind().String())
286+
}
287+
}
288+
289+
func extractPodSpecFromTemplate(template *coreapi.PodTemplateSpec) (*metav1.ObjectMeta, *coreapi.PodSpec, error) {
290+
if template == nil {
291+
return nil, nil, nil
292+
}
293+
return &template.ObjectMeta, &template.Spec, nil
294+
}

0 commit comments

Comments
 (0)