1
16
17 package auth
18
19 import (
20 "context"
21 "crypto/tls"
22 "fmt"
23 "io"
24 "net"
25 "net/http"
26 "net/url"
27 "testing"
28 "time"
29
30 admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
31 corev1 "k8s.io/api/core/v1"
32 apierrors "k8s.io/apimachinery/pkg/api/errors"
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/apimachinery/pkg/util/wait"
35 apiserver "k8s.io/apiserver/pkg/server"
36 "k8s.io/apiserver/pkg/server/dynamiccertificates"
37 utilfeature "k8s.io/apiserver/pkg/util/feature"
38 "k8s.io/client-go/kubernetes"
39 "k8s.io/client-go/rest"
40 "k8s.io/component-base/featuregate"
41 featuregatetesting "k8s.io/component-base/featuregate/testing"
42 "k8s.io/component-base/metrics/testutil"
43 kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
44 "k8s.io/kubernetes/pkg/capabilities"
45 "k8s.io/kubernetes/pkg/features"
46 "k8s.io/kubernetes/test/integration/framework"
47 utiltest "k8s.io/kubernetes/test/utils"
48 podsecurityconfigloader "k8s.io/pod-security-admission/admission/api/load"
49 podsecurityserver "k8s.io/pod-security-admission/cmd/webhook/server"
50 podsecuritytest "k8s.io/pod-security-admission/test"
51 )
52
53 func TestPodSecurity(t *testing.T) {
54
55 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ProcMountType, true)()
56 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.UserNamespacesSupport, true)()
57 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AppArmor, true)()
58
59 server := startPodSecurityServer(t)
60 opts := podsecuritytest.Options{
61 ClientConfig: server.ClientConfig,
62
63
64
65
66 ExemptClient: nil,
67 ExemptNamespaces: []string{},
68 ExemptRuntimeClasses: []string{},
69 }
70 podsecuritytest.Run(t, opts)
71
72 ValidatePluginMetrics(t, opts.ClientConfig)
73 }
74
75
76 func TestPodSecurityGAOnly(t *testing.T) {
77
78 for k, v := range utilfeature.DefaultFeatureGate.DeepCopy().GetAll() {
79 if k == "AllAlpha" || k == "AllBeta" {
80
81
82 continue
83 } else if v.PreRelease == featuregate.Alpha || v.PreRelease == featuregate.Beta {
84 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, k, false)()
85 }
86 }
87
88 server := startPodSecurityServer(t)
89
90 opts := podsecuritytest.Options{
91 ClientConfig: server.ClientConfig,
92
93 Features: utilfeature.DefaultFeatureGate,
94 }
95 podsecuritytest.Run(t, opts)
96
97 ValidatePluginMetrics(t, opts.ClientConfig)
98 }
99
100 func TestPodSecurityWebhook(t *testing.T) {
101
102 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ProcMountType, true)()
103 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.UserNamespacesSupport, true)()
104 defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AppArmor, true)()
105
106
107 capabilities.SetForTests(capabilities.Capabilities{AllowPrivileged: true})
108 testServer := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
109 "--anonymous-auth=false",
110 "--allow-privileged=true",
111
112 "--disable-admission-plugins=PodSecurity",
113 }, framework.SharedEtcd())
114 t.Cleanup(testServer.TearDownFn)
115
116 webhookAddr, err := startPodSecurityWebhook(t, testServer)
117 if err != nil {
118 t.Fatalf("Failed to start webhook server: %v", err)
119 }
120 if err := installWebhook(t, testServer.ClientConfig, webhookAddr); err != nil {
121 t.Fatalf("Failed to install webhook configuration: %v", err)
122 }
123
124 opts := podsecuritytest.Options{
125 ClientConfig: testServer.ClientConfig,
126
127
128
129
130 ExemptClient: nil,
131 ExemptNamespaces: []string{},
132 ExemptRuntimeClasses: []string{},
133 }
134 podsecuritytest.Run(t, opts)
135
136 ValidateWebhookMetrics(t, webhookAddr)
137 }
138
139 func startPodSecurityServer(t *testing.T) *kubeapiservertesting.TestServer {
140
141 capabilities.SetForTests(capabilities.Capabilities{AllowPrivileged: true})
142
143 server := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
144 "--anonymous-auth=false",
145 "--enable-admission-plugins=PodSecurity",
146 "--allow-privileged=true",
147
148 }, framework.SharedEtcd())
149 t.Cleanup(server.TearDownFn)
150 return server
151 }
152
153 func startPodSecurityWebhook(t *testing.T, testServer *kubeapiservertesting.TestServer) (addr string, err error) {
154
155 secureListener, err := net.Listen("tcp", "127.0.0.1:")
156 if err != nil {
157 return "", err
158 }
159 insecureListener, err := net.Listen("tcp", "127.0.0.1:")
160 if err != nil {
161 return "", err
162 }
163 cert, err := dynamiccertificates.NewStaticCertKeyContent("localhost", utiltest.LocalhostCert, utiltest.LocalhostKey)
164 if err != nil {
165 return "", err
166 }
167 defaultConfig, err := podsecurityconfigloader.LoadFromData(nil)
168 if err != nil {
169 return "", err
170 }
171
172 c := podsecurityserver.Config{
173 SecureServing: &apiserver.SecureServingInfo{
174 Listener: secureListener,
175 Cert: cert,
176 },
177 InsecureServing: &apiserver.DeprecatedInsecureServingInfo{
178 Listener: insecureListener,
179 },
180 KubeConfig: testServer.ClientConfig,
181 PodSecurityConfig: defaultConfig,
182 }
183
184 t.Logf("Starting webhook server...")
185 webhookServer, err := podsecurityserver.Setup(&c)
186 if err != nil {
187 return "", err
188 }
189
190 ctx, cancel := context.WithCancel(context.Background())
191 go webhookServer.Start(ctx)
192 t.Cleanup(cancel)
193
194
195 t.Logf("Waiting for webhook server /readyz to be ok...")
196 readyz := (&url.URL{
197 Scheme: "http",
198 Host: c.InsecureServing.Listener.Addr().String(),
199 Path: "/readyz",
200 }).String()
201 if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
202 resp, err := http.Get(readyz)
203 if err != nil {
204 return false, err
205 }
206 defer resp.Body.Close()
207 return resp.StatusCode == 200, nil
208 }); err != nil {
209 return "", err
210 }
211
212 return c.SecureServing.Listener.Addr().String(), nil
213 }
214
215 func installWebhook(t *testing.T, clientConfig *rest.Config, addr string) error {
216 client, err := kubernetes.NewForConfig(clientConfig)
217 if err != nil {
218 return fmt.Errorf("error creating client: %w", err)
219 }
220
221 fail := admissionregistrationv1.Fail
222 equivalent := admissionregistrationv1.Equivalent
223 none := admissionregistrationv1.SideEffectClassNone
224 endpoint := (&url.URL{
225 Scheme: "https",
226 Host: addr,
227 }).String()
228
229
230 _, err = client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), &admissionregistrationv1.ValidatingWebhookConfiguration{
231 ObjectMeta: metav1.ObjectMeta{Name: "podsecurity-webhook.integration.test"},
232 Webhooks: []admissionregistrationv1.ValidatingWebhook{
233 {
234 Name: "podsecurity-webhook.integration.test",
235 ClientConfig: admissionregistrationv1.WebhookClientConfig{
236 URL: &endpoint,
237 CABundle: utiltest.LocalhostCert,
238 },
239 Rules: []admissionregistrationv1.RuleWithOperations{
240 {
241 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
242 Rule: admissionregistrationv1.Rule{
243 APIGroups: []string{""},
244 APIVersions: []string{"v1"},
245 Resources: []string{"namespaces", "pods", "pods/ephemeralcontainers", "replicationcontrollers", "podtemplates"},
246 },
247 },
248 {
249 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
250 Rule: admissionregistrationv1.Rule{
251 APIGroups: []string{"apps"},
252 APIVersions: []string{"v1"},
253 Resources: []string{"replicasets", "deployments", "statefulsets", "daemonsets"},
254 },
255 },
256 {
257 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create, admissionregistrationv1.Update},
258 Rule: admissionregistrationv1.Rule{
259 APIGroups: []string{"batch"},
260 APIVersions: []string{"v1"},
261 Resources: []string{"cronjobs", "jobs"},
262 },
263 },
264 },
265 FailurePolicy: &fail,
266 MatchPolicy: &equivalent,
267 AdmissionReviewVersions: []string{"v1"},
268 SideEffects: &none,
269 },
270 },
271 }, metav1.CreateOptions{})
272 if err != nil {
273 return err
274 }
275
276 t.Logf("Waiting for webhook to be established...")
277 invalidNamespace := &corev1.Namespace{
278 ObjectMeta: metav1.ObjectMeta{
279 Name: "validation-fail",
280 Labels: map[string]string{
281 "pod-security.kubernetes.io/enforce": "invalid",
282 },
283 },
284 }
285
286 if err := wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) {
287 _, err := client.CoreV1().Namespaces().Create(context.TODO(), invalidNamespace, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
288 if err != nil && apierrors.IsInvalid(err) {
289 return true, nil
290 }
291 return false, nil
292 }); err != nil {
293 return err
294 }
295
296 return nil
297 }
298
299 func ValidatePluginMetrics(t *testing.T, clientConfig *rest.Config) {
300 client, err := kubernetes.NewForConfig(clientConfig)
301 if err != nil {
302 t.Fatalf("Error creating client: %v", err)
303 }
304 ctx := context.Background()
305 data, err := client.CoreV1().RESTClient().Get().AbsPath("metrics").DoRaw(ctx)
306 if err != nil {
307 t.Fatalf("Failed to read metrics: %v", err)
308 }
309 validateMetrics(t, data)
310 }
311
312 func ValidateWebhookMetrics(t *testing.T, webhookAddr string) {
313 endpoint := &url.URL{
314 Scheme: "https",
315 Host: webhookAddr,
316 Path: "/metrics",
317 }
318 client := &http.Client{Transport: &http.Transport{
319 TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
320 }}
321 resp, err := client.Get(endpoint.String())
322 if err != nil {
323 t.Fatalf("Failed to fetch metrics from %s: %v", endpoint.String(), err)
324 }
325 defer resp.Body.Close()
326 if resp.StatusCode != http.StatusOK {
327 t.Fatalf("Non-200 response trying to scrape metrics from %s: %v", endpoint.String(), resp)
328 }
329 data, err := io.ReadAll(resp.Body)
330 if err != nil {
331 t.Fatalf("Unable to read metrics response: %v", err)
332 }
333 validateMetrics(t, data)
334 }
335
336 func validateMetrics(t *testing.T, rawMetrics []byte) {
337 metrics := testutil.NewMetrics()
338 if err := testutil.ParseMetrics(string(rawMetrics), &metrics); err != nil {
339 t.Fatalf("Failed to parse metrics: %v", err)
340 }
341
342 if err := testutil.ValidateMetrics(metrics, "pod_security_evaluations_total",
343 "decision", "policy_level", "policy_version", "mode", "request_operation", "resource", "subresource"); err != nil {
344 t.Errorf("Metric validation failed: %v", err)
345 }
346 if err := testutil.ValidateMetrics(metrics, "pod_security_exemptions_total",
347 "request_operation", "resource", "subresource"); err != nil {
348 t.Errorf("Metric validation failed: %v", err)
349 }
350 }
351
View as plain text