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  }