k8s.io/kubernetes@v1.29.3/test/e2e/framework/security/apparmor.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package security 18 19 import ( 20 "context" 21 "fmt" 22 23 "github.com/onsi/gomega" 24 v1 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/labels" 27 clientset "k8s.io/client-go/kubernetes" 28 "k8s.io/kubernetes/test/e2e/framework" 29 e2epod "k8s.io/kubernetes/test/e2e/framework/pod" 30 imageutils "k8s.io/kubernetes/test/utils/image" 31 ) 32 33 const ( 34 appArmorProfilePrefix = "e2e-apparmor-test-" 35 appArmorAllowedPath = "/expect_allowed_write" 36 appArmorDeniedPath = "/expect_permission_denied" 37 38 loaderLabelKey = "name" 39 loaderLabelValue = "e2e-apparmor-loader" 40 ) 41 42 // LoadAppArmorProfiles creates apparmor-profiles ConfigMap and apparmor-loader ReplicationController. 43 func LoadAppArmorProfiles(ctx context.Context, nsName string, clientset clientset.Interface) { 44 createAppArmorProfileCM(ctx, nsName, clientset) 45 createAppArmorProfileLoader(ctx, nsName, clientset) 46 } 47 48 // CreateAppArmorTestPod creates a pod that tests apparmor profile enforcement. The pod exits with 49 // an error code if the profile is incorrectly enforced. If runOnce is true the pod will exit after 50 // a single test, otherwise it will repeat the test every 1 second until failure. 51 func CreateAppArmorTestPod(ctx context.Context, nsName string, clientset clientset.Interface, podClient *e2epod.PodClient, unconfined bool, runOnce bool) *v1.Pod { 52 profile := "localhost/" + appArmorProfilePrefix + nsName 53 testCmd := fmt.Sprintf(` 54 if touch %[1]s; then 55 echo "FAILURE: write to %[1]s should be denied" 56 exit 1 57 elif ! touch %[2]s; then 58 echo "FAILURE: write to %[2]s should be allowed" 59 exit 2 60 elif [[ $(< /proc/self/attr/current) != "%[3]s" ]]; then 61 echo "FAILURE: not running with expected profile %[3]s" 62 echo "found: $(cat /proc/self/attr/current)" 63 exit 3 64 fi`, appArmorDeniedPath, appArmorAllowedPath, appArmorProfilePrefix+nsName) 65 66 if unconfined { 67 profile = v1.AppArmorBetaProfileNameUnconfined 68 testCmd = ` 69 if cat /proc/sysrq-trigger 2>&1 | grep 'Permission denied'; then 70 echo 'FAILURE: reading /proc/sysrq-trigger should be allowed' 71 exit 1 72 elif [[ $(< /proc/self/attr/current) != "unconfined" ]]; then 73 echo 'FAILURE: not running with expected profile unconfined' 74 exit 2 75 fi` 76 } 77 78 if !runOnce { 79 testCmd = fmt.Sprintf(`while true; do 80 %s 81 sleep 1 82 done`, testCmd) 83 } 84 85 loaderAffinity := &v1.Affinity{ 86 PodAffinity: &v1.PodAffinity{ 87 RequiredDuringSchedulingIgnoredDuringExecution: []v1.PodAffinityTerm{{ 88 Namespaces: []string{nsName}, 89 LabelSelector: &metav1.LabelSelector{ 90 MatchLabels: map[string]string{loaderLabelKey: loaderLabelValue}, 91 }, 92 TopologyKey: "kubernetes.io/hostname", 93 }}, 94 }, 95 } 96 97 pod := &v1.Pod{ 98 ObjectMeta: metav1.ObjectMeta{ 99 GenerateName: "test-apparmor-", 100 Annotations: map[string]string{ 101 v1.AppArmorBetaContainerAnnotationKeyPrefix + "test": profile, 102 }, 103 Labels: map[string]string{ 104 "test": "apparmor", 105 }, 106 }, 107 Spec: v1.PodSpec{ 108 Affinity: loaderAffinity, 109 Containers: []v1.Container{{ 110 Name: "test", 111 Image: imageutils.GetE2EImage(imageutils.BusyBox), 112 Command: []string{"sh", "-c", testCmd}, 113 }}, 114 RestartPolicy: v1.RestartPolicyNever, 115 }, 116 } 117 118 if runOnce { 119 pod = podClient.Create(ctx, pod) 120 framework.ExpectNoError(e2epod.WaitForPodSuccessInNamespace(ctx, 121 clientset, pod.Name, nsName)) 122 var err error 123 pod, err = podClient.Get(ctx, pod.Name, metav1.GetOptions{}) 124 framework.ExpectNoError(err) 125 } else { 126 pod = podClient.CreateSync(ctx, pod) 127 framework.ExpectNoError(e2epod.WaitTimeoutForPodReadyInNamespace(ctx, clientset, pod.Name, nsName, framework.PodStartTimeout)) 128 } 129 130 // Verify Pod affinity colocated the Pods. 131 loader := getRunningLoaderPod(ctx, nsName, clientset) 132 gomega.Expect(pod.Spec.NodeName).To(gomega.Equal(loader.Spec.NodeName)) 133 134 return pod 135 } 136 137 func createAppArmorProfileCM(ctx context.Context, nsName string, clientset clientset.Interface) { 138 profileName := appArmorProfilePrefix + nsName 139 profile := fmt.Sprintf(`#include <tunables/global> 140 profile %s flags=(attach_disconnected) { 141 #include <abstractions/base> 142 143 file, 144 145 deny %s w, 146 audit %s w, 147 } 148 `, profileName, appArmorDeniedPath, appArmorAllowedPath) 149 150 cm := &v1.ConfigMap{ 151 ObjectMeta: metav1.ObjectMeta{ 152 Name: "apparmor-profiles", 153 Namespace: nsName, 154 }, 155 Data: map[string]string{ 156 profileName: profile, 157 }, 158 } 159 _, err := clientset.CoreV1().ConfigMaps(nsName).Create(ctx, cm, metav1.CreateOptions{}) 160 framework.ExpectNoError(err, "Failed to create apparmor-profiles ConfigMap") 161 } 162 163 func createAppArmorProfileLoader(ctx context.Context, nsName string, clientset clientset.Interface) { 164 True := true 165 One := int32(1) 166 loader := &v1.ReplicationController{ 167 ObjectMeta: metav1.ObjectMeta{ 168 Name: "apparmor-loader", 169 Namespace: nsName, 170 }, 171 Spec: v1.ReplicationControllerSpec{ 172 Replicas: &One, 173 Template: &v1.PodTemplateSpec{ 174 ObjectMeta: metav1.ObjectMeta{ 175 Labels: map[string]string{loaderLabelKey: loaderLabelValue}, 176 }, 177 Spec: v1.PodSpec{ 178 Containers: []v1.Container{{ 179 Name: "apparmor-loader", 180 Image: imageutils.GetE2EImage(imageutils.AppArmorLoader), 181 Args: []string{"-poll", "10s", "/profiles"}, 182 SecurityContext: &v1.SecurityContext{ 183 Privileged: &True, 184 }, 185 VolumeMounts: []v1.VolumeMount{{ 186 Name: "sys", 187 MountPath: "/sys", 188 ReadOnly: true, 189 }, { 190 Name: "apparmor-includes", 191 MountPath: "/etc/apparmor.d", 192 ReadOnly: true, 193 }, { 194 Name: "profiles", 195 MountPath: "/profiles", 196 ReadOnly: true, 197 }}, 198 }}, 199 Volumes: []v1.Volume{{ 200 Name: "sys", 201 VolumeSource: v1.VolumeSource{ 202 HostPath: &v1.HostPathVolumeSource{ 203 Path: "/sys", 204 }, 205 }, 206 }, { 207 Name: "apparmor-includes", 208 VolumeSource: v1.VolumeSource{ 209 HostPath: &v1.HostPathVolumeSource{ 210 Path: "/etc/apparmor.d", 211 }, 212 }, 213 }, { 214 Name: "profiles", 215 VolumeSource: v1.VolumeSource{ 216 ConfigMap: &v1.ConfigMapVolumeSource{ 217 LocalObjectReference: v1.LocalObjectReference{ 218 Name: "apparmor-profiles", 219 }, 220 }, 221 }, 222 }}, 223 }, 224 }, 225 }, 226 } 227 _, err := clientset.CoreV1().ReplicationControllers(nsName).Create(ctx, loader, metav1.CreateOptions{}) 228 framework.ExpectNoError(err, "Failed to create apparmor-loader ReplicationController") 229 230 // Wait for loader to be ready. 231 getRunningLoaderPod(ctx, nsName, clientset) 232 } 233 234 func getRunningLoaderPod(ctx context.Context, nsName string, clientset clientset.Interface) *v1.Pod { 235 label := labels.SelectorFromSet(labels.Set(map[string]string{loaderLabelKey: loaderLabelValue})) 236 pods, err := e2epod.WaitForPodsWithLabelScheduled(ctx, clientset, nsName, label) 237 framework.ExpectNoError(err, "Failed to schedule apparmor-loader Pod") 238 pod := &pods.Items[0] 239 framework.ExpectNoError(e2epod.WaitForPodRunningInNamespace(ctx, clientset, pod), "Failed to run apparmor-loader Pod") 240 return pod 241 }