diff --git a/KubeArmor/core/containerdHandler.go b/KubeArmor/core/containerdHandler.go index c0886d635a..f7e58ddf24 100644 --- a/KubeArmor/core/containerdHandler.go +++ b/KubeArmor/core/containerdHandler.go @@ -11,6 +11,8 @@ import ( "strconv" "time" + "golang.org/x/exp/slices" + kl "github.com/kubearmor/KubeArmor/KubeArmor/common" cfg "github.com/kubearmor/KubeArmor/KubeArmor/config" kg "github.com/kubearmor/KubeArmor/KubeArmor/log" @@ -29,6 +31,26 @@ import ( // == Containerd Handler == // // ======================== // +// DefaultCaps contains all the default capabilities given to a +// container by containerd runtime +// Taken from - https://github.com/containerd/containerd/blob/main/oci/spec.go +var defaultCaps = []string{ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_FSETID", + "CAP_FOWNER", + "CAP_MKNOD", + "CAP_NET_RAW", + "CAP_SETGID", + "CAP_SETUID", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_CHROOT", + "CAP_KILL", + "CAP_AUDIT_WRITE", +} + // Containerd Handler var Containerd *ContainerdHandler @@ -142,6 +164,11 @@ func (ch *ContainerdHandler) GetContainerInfo(ctx context.Context, containerID s spec := iface.(*specs.Spec) container.AppArmorProfile = spec.Process.ApparmorProfile + // if a container has additional caps than default, we mark it as privileged + if spec.Process.Capabilities != nil && slices.Compare(spec.Process.Capabilities.Permitted, defaultCaps) >= 0 { + container.Privileged = true + } + // == // taskReq := pt.ListPidsRequest{ContainerID: container.ContainerID} @@ -282,6 +309,10 @@ func (dm *KubeArmorDaemon) UpdateContainerdContainer(ctx context.Context, contai dm.EndPoints[idx].AppArmorProfiles = append(dm.EndPoints[idx].AppArmorProfiles, container.AppArmorProfile) } + if container.Privileged && dm.EndPoints[idx].PrivilegedContainers != nil { + dm.EndPoints[idx].PrivilegedContainers[container.ContainerName] = struct{}{} + } + break } } diff --git a/KubeArmor/core/crioHandler.go b/KubeArmor/core/crioHandler.go index 988bfdd1e1..ec9b7a3b77 100644 --- a/KubeArmor/core/crioHandler.go +++ b/KubeArmor/core/crioHandler.go @@ -119,6 +119,7 @@ func (ch *CrioHandler) GetContainerInfo(ctx context.Context, containerID string) // path to container's root storage container.AppArmorProfile = containerInfo.RuntimeSpec.Process.ApparmorProfile + container.Privileged = containerInfo.Privileged pid := strconv.Itoa(containerInfo.Pid) @@ -240,6 +241,10 @@ func (dm *KubeArmorDaemon) UpdateCrioContainer(ctx context.Context, containerID, dm.EndPoints[idx].AppArmorProfiles = append(dm.EndPoints[idx].AppArmorProfiles, container.AppArmorProfile) } + if container.Privileged && dm.EndPoints[idx].PrivilegedContainers != nil { + dm.EndPoints[idx].PrivilegedContainers[container.ContainerName] = struct{}{} + } + break } } diff --git a/KubeArmor/core/dockerHandler.go b/KubeArmor/core/dockerHandler.go index 0ffeea9e3b..c097f26fd8 100644 --- a/KubeArmor/core/dockerHandler.go +++ b/KubeArmor/core/dockerHandler.go @@ -115,6 +115,9 @@ func (dh *DockerHandler) GetContainerInfo(containerID string) (tp.Container, err } container.AppArmorProfile = inspect.AppArmorProfile + if inspect.HostConfig != nil { + container.Privileged = inspect.HostConfig.Privileged + } // == // @@ -241,6 +244,10 @@ func (dm *KubeArmorDaemon) GetAlreadyDeployedDockerContainers() { dm.EndPoints[idx].AppArmorProfiles = append(dm.EndPoints[idx].AppArmorProfiles, container.AppArmorProfile) } + if container.Privileged && dm.EndPoints[idx].PrivilegedContainers != nil { + dm.EndPoints[idx].PrivilegedContainers[container.ContainerName] = struct{}{} + } + break } } diff --git a/KubeArmor/core/kubeUpdate.go b/KubeArmor/core/kubeUpdate.go index 65594976e9..13994d2d25 100644 --- a/KubeArmor/core/kubeUpdate.go +++ b/KubeArmor/core/kubeUpdate.go @@ -268,6 +268,11 @@ func (dm *KubeArmorDaemon) UpdateEndPointWithPod(action string, pod tp.K8sPod) { newPoint.AppArmorProfiles = append(newPoint.AppArmorProfiles, container.AppArmorProfile) } + // if container is privileged + if _, ok := pod.PrivilegedContainers[container.ContainerName]; ok { + container.Privileged = true + } + dm.Containers[containerID] = container // in case if container runtime detect the container and emit that event before pod event then @@ -433,6 +438,11 @@ func (dm *KubeArmorDaemon) UpdateEndPointWithPod(action string, pod tp.K8sPod) { newEndPoint.AppArmorProfiles = append(newEndPoint.AppArmorProfiles, container.AppArmorProfile) } + // if container is privileged + if _, ok := pod.PrivilegedContainers[container.ContainerName]; ok { + container.Privileged = true + } + dm.Containers[containerID] = container // in case if container runtime detect the container and emit that event before pod event then // the container id will be added to NsMap with "Unknown" namespace @@ -688,6 +698,8 @@ func (dm *KubeArmorDaemon) WatchK8sPods() { } } + pod.PrivilegedContainers = make(map[string]struct{}) + pod.PrivilegedAppArmorProfiles = make(map[string]struct{}) if dm.RuntimeEnforcer != nil && dm.RuntimeEnforcer.EnforcerType == "AppArmor" { appArmorAnnotations := map[string]string{} updateAppArmor := false @@ -723,7 +735,7 @@ func (dm *KubeArmorDaemon) WatchK8sPods() { } } } else if pod.Metadata["owner.controller"] == "Pod" { - pod, err := K8s.K8sClient.CoreV1().Pods("default").Get(context.Background(), "my-pod", metav1.GetOptions{}) + pod, err := K8s.K8sClient.CoreV1().Pods(pod.Metadata["namespaceName"]).Get(context.Background(), podOwnerName, metav1.GetOptions{}) if err == nil { for _, c := range pod.Spec.Containers { containers = append(containers, c.Name) @@ -746,16 +758,34 @@ func (dm *KubeArmorDaemon) WatchK8sPods() { } } + var privileged bool for _, container := range event.Object.Spec.Containers { + // store privileged containers + if container.SecurityContext != nil && + ((container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged) || + (container.SecurityContext.Capabilities != nil && len(container.SecurityContext.Capabilities.Add) > 0)) { + pod.PrivilegedContainers[container.Name] = struct{}{} + privileged = true + } + if _, ok := appArmorAnnotations[container.Name]; !ok && kl.ContainsElement(containers, container.Name) { - appArmorAnnotations[container.Name] = "kubearmor-" + pod.Metadata["namespaceName"] + "-" + podOwnerName + "-" + container.Name + profileName := "kubearmor-" + pod.Metadata["namespaceName"] + "-" + podOwnerName + "-" + container.Name + appArmorAnnotations[container.Name] = profileName updateAppArmor = true + + // if the container is privileged or it has more than one capabilities added + // handle the apparmor profile generation with privileged rules + if privileged { + // container name is unique for all containers in a pod + pod.PrivilegedAppArmorProfiles[profileName] = struct{}{} + } + } } if event.Type == "ADDED" { // update apparmor profiles - dm.RuntimeEnforcer.UpdateAppArmorProfiles(pod.Metadata["podName"], "ADDED", appArmorAnnotations) + dm.RuntimeEnforcer.UpdateAppArmorProfiles(pod.Metadata["podName"], "ADDED", appArmorAnnotations, pod.PrivilegedAppArmorProfiles) if updateAppArmor && pod.Annotations["kubearmor-policy"] == "enabled" { if deploymentName, ok := pod.Metadata["owner.controllerName"]; ok { @@ -794,7 +824,7 @@ func (dm *KubeArmorDaemon) WatchK8sPods() { } } else if event.Type == "DELETED" { // update apparmor profiles - dm.RuntimeEnforcer.UpdateAppArmorProfiles(pod.Metadata["podName"], "DELETED", appArmorAnnotations) + dm.RuntimeEnforcer.UpdateAppArmorProfiles(pod.Metadata["podName"], "DELETED", appArmorAnnotations, pod.PrivilegedAppArmorProfiles) } } diff --git a/KubeArmor/core/unorchestratedUpdates.go b/KubeArmor/core/unorchestratedUpdates.go index f3453f0b1c..0690802998 100644 --- a/KubeArmor/core/unorchestratedUpdates.go +++ b/KubeArmor/core/unorchestratedUpdates.go @@ -430,9 +430,16 @@ func (dm *KubeArmorDaemon) ParseAndUpdateContainerSecurityPolicy(event tp.K8sKub endPointIndex := -1 newPoint := tp.EndPoint{} + var privilegedProfiles map[string]struct{} for idx, endPoint := range dm.EndPoints { endPointIndex++ + /* + if _, ok := endPoint.PrivilegedContainers[containername]; ok { + privilegedProfiles[appArmorAnnotations[containername]] = struct{}{} + } + */ + // update container rules if there exists another container with same policy.Metadata["policyName"] for policyIndex, policy := range endPoint.SecurityPolicies { if policy.Metadata["namespaceName"] == secPolicy.Metadata["namespaceName"] && policy.Metadata["policyName"] == secPolicy.Metadata["policyName"] && endPoint.EndPointName != containername { @@ -509,7 +516,7 @@ func (dm *KubeArmorDaemon) ParseAndUpdateContainerSecurityPolicy(event tp.K8sKub } if event.Type == "ADDED" { - dm.RuntimeEnforcer.UpdateAppArmorProfiles(containername, "ADDED", appArmorAnnotations) + dm.RuntimeEnforcer.UpdateAppArmorProfiles(containername, "ADDED", appArmorAnnotations, privilegedProfiles) newPoint.SecurityPolicies = append(newPoint.SecurityPolicies, secPolicy) if i < 0 { @@ -524,12 +531,20 @@ func (dm *KubeArmorDaemon) ParseAndUpdateContainerSecurityPolicy(event tp.K8sKub newPoint.NetworkVisibilityEnabled = true newPoint.CapabilitiesVisibilityEnabled = true newPoint.Containers = []string{} + + newPoint.PrivilegedContainers = map[string]struct{}{} + dm.ContainersLock.Lock() for idx, ctr := range dm.Containers { if ctr.ContainerName == containername { newPoint.Containers = append(newPoint.Containers, ctr.ContainerID) ctr.NamespaceName = newPoint.NamespaceName ctr.EndPointName = newPoint.EndPointName + /* + if ctr.Privileged { + newPoint.PrivilegedContainers[containername] = struct{}{} + } + */ dm.Containers[idx] = ctr } } diff --git a/KubeArmor/enforcer/appArmorEnforcer.go b/KubeArmor/enforcer/appArmorEnforcer.go index 99cc8a12eb..28ae94b3c3 100644 --- a/KubeArmor/enforcer/appArmorEnforcer.go +++ b/KubeArmor/enforcer/appArmorEnforcer.go @@ -29,6 +29,8 @@ type AppArmorEnforcer struct { // default profile ApparmorDefault string + // default privileged profile + ApparmorDefaultPrivileged string // host profile HostProfile string @@ -37,6 +39,10 @@ type AppArmorEnforcer struct { AppArmorProfiles map[string][]string AppArmorProfilesLock *sync.RWMutex + // to keep track of privileged profiles for clean deletion + AppArmorPrivilegedProfiles map[string]struct{} + AppArmorPrivilegedProfilesLock *sync.RWMutex + // Regex used to get profile Names rgx *regexp.Regexp } @@ -50,38 +56,39 @@ func NewAppArmorEnforcer(node tp.Node, logger *fd.Feeder) *AppArmorEnforcer { // default profile ae.ApparmorDefault = `## == Managed by KubeArmor == ## - #include + profile apparmor-default flags=(attach_disconnected,mediate_deleted) { -## == PRE START == ## -#include -umount, -file, -network, -capability, +## == PRE START == ## +` + AppArmorDefaultPreStart + + ` +## == PRE END == ## + +## == POLICY START == ## +## == POLICY END == ## + +## == POST START == ## +` + AppArmorDefaultPostStart + + ` +## == POST END == ## +} +` + + ae.ApparmorDefaultPrivileged = `## == Managed by KubeArmor == ## +#include + +profile apparmor-default flags=(attach_disconnected,mediate_deleted) { +## == PRE START == ## +` + AppArmorPrivilegedPreStart + + ` ## == PRE END == ## ## == POLICY START == ## ## == POLICY END == ## ## == POST START == ## -/lib/x86_64-linux-gnu/{*,**} rm, - -deny @{PROC}/{*,**^[0-9*],sys/kernel/shm*} wkx, -deny @{PROC}/sysrq-trigger rwklx, -deny @{PROC}/mem rwklx, -deny @{PROC}/kmem rwklx, -deny @{PROC}/kcore rwklx, - -deny mount, - -deny /sys/[^f]*/** wklx, -deny /sys/f[^s]*/** wklx, -deny /sys/fs/[^c]*/** wklx, -deny /sys/fs/c[^g]*/** wklx, -deny /sys/fs/cg[^r]*/** wklx, -deny /sys/firmware/efi/efivars/** rwklx, -deny /sys/kernel/security/** rwklx, +` + AppArmorPrivilegedPostStart + + ` ## == POST END == ## } ` @@ -96,6 +103,9 @@ deny /sys/kernel/security/** rwklx, ae.AppArmorProfiles = map[string][]string{} ae.AppArmorProfilesLock = &sync.RWMutex{} + ae.AppArmorPrivilegedProfiles = map[string]struct{}{} + ae.AppArmorPrivilegedProfilesLock = new(sync.RWMutex) + files, err := os.ReadDir("/etc/apparmor.d") if err != nil { ae.Logger.Errf("Failed to read /etc/apparmor.d (%s)", err.Error()) @@ -173,7 +183,8 @@ func (ae *AppArmorEnforcer) DestroyAppArmorEnforcer() error { } for profile := range ae.AppArmorProfiles { - ae.UnregisterAppArmorProfile("", profile) + _, privileged := ae.AppArmorPrivilegedProfiles[profile] + ae.UnregisterAppArmorProfile("", profile, privileged) } if cfg.GlobalCfg.HostPolicy { @@ -190,7 +201,7 @@ func (ae *AppArmorEnforcer) DestroyAppArmorEnforcer() error { // ================================= // // RegisterAppArmorProfile Function -func (ae *AppArmorEnforcer) RegisterAppArmorProfile(podName, profileName string) bool { +func (ae *AppArmorEnforcer) RegisterAppArmorProfile(podName, profileName string, privileged bool) bool { // skip if AppArmorEnforcer is not active if ae == nil { return true @@ -216,7 +227,15 @@ func (ae *AppArmorEnforcer) RegisterAppArmorProfile(podName, profileName string) return true } - newProfile := strings.Replace(ae.ApparmorDefault, "apparmor-default", profileName, -1) + // generate a profile with basic allows if a privileged container + var newProfile string + if privileged { + newProfile = strings.Replace(ae.ApparmorDefaultPrivileged, "apparmor-default", profileName, -1) + ae.AppArmorPrivilegedProfiles[profileName] = struct{}{} + ae.Logger.Printf("Added an AppArmor profile for a privileged container (%s, %s)", podName, profileName) + } else { + newProfile = strings.Replace(ae.ApparmorDefault, "apparmor-default", profileName, -1) + } newFile, err := os.Create(filepath.Clean("/etc/apparmor.d/" + profileName)) if err != nil { @@ -247,7 +266,7 @@ func (ae *AppArmorEnforcer) RegisterAppArmorProfile(podName, profileName string) } // UnregisterAppArmorProfile Function -func (ae *AppArmorEnforcer) UnregisterAppArmorProfile(podName, profileName string) bool { +func (ae *AppArmorEnforcer) UnregisterAppArmorProfile(podName, profileName string, privileged bool) bool { // skip if AppArmorEnforcer is not active if ae == nil { return true @@ -285,7 +304,12 @@ func (ae *AppArmorEnforcer) UnregisterAppArmorProfile(podName, profileName strin return false } - newProfile := strings.Replace(ae.ApparmorDefault, "apparmor-default", profileName, -1) + var newProfile string + if privileged { + newProfile = strings.Replace(ae.ApparmorDefaultPrivileged, "apparmor-default", profileName, -1) + } else { + newProfile = strings.Replace(ae.ApparmorDefault, "apparmor-default", profileName, -1) + } newFile, err := os.Create(filepath.Clean("/etc/apparmor.d/" + profileName)) if err != nil { @@ -454,7 +478,12 @@ func (ae *AppArmorEnforcer) UnregisterAppArmorHostProfile() bool { // UpdateAppArmorProfile Function func (ae *AppArmorEnforcer) UpdateAppArmorProfile(endPoint tp.EndPoint, appArmorProfile string, securityPolicies []tp.SecurityPolicy) { - if policyCount, newProfile, ok := ae.GenerateAppArmorProfile(appArmorProfile, securityPolicies, endPoint.DefaultPosture); ok { + + ae.AppArmorPrivilegedProfilesLock.Lock() + _, privileged := ae.AppArmorPrivilegedProfiles[appArmorProfile] + ae.AppArmorPrivilegedProfilesLock.Unlock() + + if policyCount, newProfile, ok := ae.GenerateAppArmorProfile(appArmorProfile, securityPolicies, endPoint.DefaultPosture, privileged); ok { newfile, err := os.Create(filepath.Clean("/etc/apparmor.d/" + appArmorProfile)) if err != nil { ae.Logger.Warnf("Unable to open an AppArmor profile (%s, %s)", appArmorProfile, err.Error()) diff --git a/KubeArmor/enforcer/appArmorProfile.go b/KubeArmor/enforcer/appArmorProfile.go index 191c812775..5cac28276f 100644 --- a/KubeArmor/enforcer/appArmorProfile.go +++ b/KubeArmor/enforcer/appArmorProfile.go @@ -295,7 +295,7 @@ func (ae *AppArmorEnforcer) SetCapabilitiesMatchCapabilities(cap tp.Capabilities // == // // GenerateProfileBody Function -func (ae *AppArmorEnforcer) GenerateProfileBody(securityPolicies []tp.SecurityPolicy, defaultPosture tp.DefaultPosture) (int, Profile) { +func (ae *AppArmorEnforcer) GenerateProfileBody(securityPolicies []tp.SecurityPolicy, defaultPosture tp.DefaultPosture, privileged bool) (int, Profile) { // preparation count := 0 @@ -303,6 +303,10 @@ func (ae *AppArmorEnforcer) GenerateProfileBody(securityPolicies []tp.SecurityPo var profile Profile profile.Init() + if privileged { + profile.Privileged = true + } + for _, secPolicy := range securityPolicies { if len(secPolicy.Spec.AppArmor) > 0 { scanner := bufio.NewScanner(strings.NewReader(secPolicy.Spec.AppArmor)) @@ -449,7 +453,7 @@ func (ae *AppArmorEnforcer) GenerateProfileBody(securityPolicies []tp.SecurityPo // == // // GenerateAppArmorProfile Function -func (ae *AppArmorEnforcer) GenerateAppArmorProfile(appArmorProfile string, securityPolicies []tp.SecurityPolicy, defaultPosture tp.DefaultPosture) (int, string, bool) { +func (ae *AppArmorEnforcer) GenerateAppArmorProfile(appArmorProfile string, securityPolicies []tp.SecurityPolicy, defaultPosture tp.DefaultPosture, privileged bool) (int, string, bool) { // check apparmor profile if _, err := os.Stat(filepath.Clean("/etc/apparmor.d/" + appArmorProfile)); os.IsNotExist(err) { @@ -466,7 +470,7 @@ func (ae *AppArmorEnforcer) GenerateAppArmorProfile(appArmorProfile string, secu // generate a profile body - count, newProfile := ae.GenerateProfileBody(securityPolicies, defaultPosture) + count, newProfile := ae.GenerateProfileBody(securityPolicies, defaultPosture, privileged) newProfile.Name = appArmorProfile diff --git a/KubeArmor/enforcer/appArmorTemplate.go b/KubeArmor/enforcer/appArmorTemplate.go index 9ec64272a5..36d13d5e91 100644 --- a/KubeArmor/enforcer/appArmorTemplate.go +++ b/KubeArmor/enforcer/appArmorTemplate.go @@ -5,7 +5,7 @@ package enforcer // ProfileHeader contain sAppArmor Profile/SubProfile header config type ProfileHeader struct { - File, Network, Capabilities bool + File, Network, Capabilities, Privileged bool } // Init sets the presence of Entity headers to true by default @@ -13,6 +13,7 @@ func (h *ProfileHeader) Init() { h.File = true h.Network = true h.Capabilities = true + h.Privileged = false } // RuleConfig contains details for individual apparmor rules @@ -126,9 +127,11 @@ profile {{.Name}} flags=(attach_disconnected,mediate_deleted) { deny @{PROC}/mem rwklx, deny @{PROC}/kmem rwklx, deny @{PROC}/kcore rwklx, - + + {{ if not .Privileged }} deny mount, - + {{end}} + deny /sys/[^f]*/** wklx, deny /sys/f[^s]*/** wklx, deny /sys/fs/[^c]*/** wklx, @@ -203,9 +206,17 @@ profile {{$.Name}}-{{$source}} { {{define "pre-section"}} ## == PRE START == ## #include - {{ if .File}} file,{{end}} - {{ if .Network}} network,{{end}} - {{ if .Capabilities}} capability,{{end}} + {{ if .Privileged }} + ## == For privileged workloads == ## + umount, + mount, + signal, + unix, + ptrace, + {{end}} + {{ if .File}}file,{{end}} + {{ if .Network}}network,{{end}} + {{ if .Capabilities}}capability,{{end}} ## == PRE END == ## {{- end}} @@ -270,21 +281,23 @@ profile {{$.Name}}-{{$source}} { {{- end}} {{- end}} {{- end}} - ## == File/Dir END == ## + ## == File/Dir END == ## {{- end}} {{ define "post-section"}} ## == POST START == ## /lib/x86_64-linux-gnu/{*,**} rm, - + deny @{PROC}/{*,**^[0-9*],sys/kernel/shm*} wkx, deny @{PROC}/sysrq-trigger rwklx, deny @{PROC}/mem rwklx, deny @{PROC}/kmem rwklx, deny @{PROC}/kcore rwklx, - + + {{ if not .Privileged }} deny mount, - + {{end}} + deny /sys/[^f]*/** wklx, deny /sys/f[^s]*/** wklx, deny /sys/fs/[^c]*/** wklx, diff --git a/KubeArmor/enforcer/apparmorContainerProfile.go b/KubeArmor/enforcer/apparmorContainerProfile.go new file mode 100644 index 0000000000..7b618b6b53 --- /dev/null +++ b/KubeArmor/enforcer/apparmorContainerProfile.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + +package enforcer + +const ( + AppArmorDefaultPreStart = ` + +#include +umount, +file, +network, +capability, + +` + AppArmorPrivilegedPreStart = AppArmorDefaultPreStart + + ` + +## == For privileged workloads == ## +mount, +signal, +unix, +ptrace, + +` + + AppArmorPrivilegedPostStart = ` + +/lib/x86_64-linux-gnu/{*,**} rm, + +deny @{PROC}/{*,**^[0-9*],sys/kernel/shm*} wkx, +deny @{PROC}/sysrq-trigger rwklx, +deny @{PROC}/mem rwklx, +deny @{PROC}/kmem rwklx, +deny @{PROC}/kcore rwklx, + +deny /sys/[^f]*/** wklx, +deny /sys/f[^s]*/** wklx, +deny /sys/fs/[^c]*/** wklx, +deny /sys/fs/c[^g]*/** wklx, +deny /sys/fs/cg[^r]*/** wklx, +deny /sys/firmware/efi/efivars/** rwklx, +deny /sys/kernel/security/** rwklx, + +` + + AppArmorDefaultPostStart = AppArmorPrivilegedPostStart + + ` + +deny mount, + +` +) diff --git a/KubeArmor/enforcer/runtimeEnforcer.go b/KubeArmor/enforcer/runtimeEnforcer.go index c480142291..ca4bc54114 100644 --- a/KubeArmor/enforcer/runtimeEnforcer.go +++ b/KubeArmor/enforcer/runtimeEnforcer.go @@ -200,7 +200,7 @@ func (re *RuntimeEnforcer) UnregisterContainer(containerID string) { } // UpdateAppArmorProfiles Function -func (re *RuntimeEnforcer) UpdateAppArmorProfiles(podName, action string, profiles map[string]string) { +func (re *RuntimeEnforcer) UpdateAppArmorProfiles(podName string, action string, profiles map[string]string, privilegedProfiles map[string]struct{}) { // skip if runtime enforcer is not active if re == nil { return @@ -212,10 +212,12 @@ func (re *RuntimeEnforcer) UpdateAppArmorProfiles(podName, action string, profil continue } + _, privileged := privilegedProfiles[profile] + if action == "ADDED" { - re.appArmorEnforcer.RegisterAppArmorProfile(podName, profile) + re.appArmorEnforcer.RegisterAppArmorProfile(podName, profile, privileged) } else if action == "DELETED" { - re.appArmorEnforcer.UnregisterAppArmorProfile(podName, profile) + re.appArmorEnforcer.UnregisterAppArmorProfile(podName, profile, privileged) } } } diff --git a/KubeArmor/go.mod b/KubeArmor/go.mod index 10de20e116..149251677e 100644 --- a/KubeArmor/go.mod +++ b/KubeArmor/go.mod @@ -38,6 +38,7 @@ require ( github.com/opencontainers/runtime-spec v1.1.0-rc.2 github.com/spf13/viper v1.15.0 go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 golang.org/x/sys v0.10.0 google.golang.org/grpc v1.56.3 k8s.io/api v0.27.1 @@ -103,7 +104,6 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.9.0 // indirect - golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect diff --git a/KubeArmor/types/types.go b/KubeArmor/types/types.go index f40fd30d0b..041c67a7e8 100644 --- a/KubeArmor/types/types.go +++ b/KubeArmor/types/types.go @@ -29,6 +29,7 @@ type Container struct { Labels string `json:"labels"` AppArmorProfile string `json:"apparmorProfile"` + Privileged bool `json:"privileged"` // == // @@ -69,6 +70,9 @@ type EndPoint struct { SecurityPolicies []SecurityPolicy `json:"securityPolicies"` + // only needed for unorchestrated containers + PrivilegedContainers map[string]struct{} `json:"privilegdContainers"` + // == // PolicyEnabled int `json:"policyEnabled"` @@ -126,6 +130,13 @@ type K8sPod struct { Labels map[string]string Containers map[string]string ContainerImages map[string]string + + // using two maps here as it is inefficent to + // obtain either from just one + // for storing privilegd container names + PrivilegedContainers map[string]struct{} + // for storing privileged apparmor profile names + PrivilegedAppArmorProfiles map[string]struct{} } // K8sPodEvent Structure diff --git a/tests/k8s_env/privileged/manifests/priv-container-block-ls.yaml b/tests/k8s_env/privileged/manifests/priv-container-block-ls.yaml new file mode 100644 index 0000000000..8d12aac3e4 --- /dev/null +++ b/tests/k8s_env/privileged/manifests/priv-container-block-ls.yaml @@ -0,0 +1,17 @@ +apiVersion: security.kubearmor.com/v1 +kind: KubeArmorPolicy +metadata: + name: privileged-container-block-ls + namespace: privileged +spec: + severity: 2 + selector: + matchLabels: + deployment: privileged + kubearmor.io/container.name: "[priv-container]" + process: + matchPaths: + - path: /bin/ls + # ls + action: + Block diff --git a/tests/k8s_env/privileged/manifests/privileged-deploy.yaml b/tests/k8s_env/privileged/manifests/privileged-deploy.yaml new file mode 100644 index 0000000000..2603647bc6 --- /dev/null +++ b/tests/k8s_env/privileged/manifests/privileged-deploy.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: privileged +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: privileged-deployment + namespace: privileged + labels: + deployment: privileged +spec: + replicas: 1 + selector: + matchLabels: + deployment: privileged + template: + metadata: + labels: + deployment: privileged + spec: + containers: + - name: priv-container + image: kubearmor/ubuntu-w-utils:0.1 + securityContext: + privileged: true + - name: unpriv-container + image: kubearmor/ubuntu-w-utils:0.1 diff --git a/tests/k8s_env/privileged/privileged_suite_test.go b/tests/k8s_env/privileged/privileged_suite_test.go new file mode 100644 index 0000000000..f9fbcaff4b --- /dev/null +++ b/tests/k8s_env/privileged/privileged_suite_test.go @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + +package privileged + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestSmoke(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Privileged Suite") +} diff --git a/tests/k8s_env/privileged/privileged_test.go b/tests/k8s_env/privileged/privileged_test.go new file mode 100644 index 0000000000..149229b7da --- /dev/null +++ b/tests/k8s_env/privileged/privileged_test.go @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + +package privileged + +import ( + "fmt" + "strings" + "time" + + "github.com/kubearmor/KubeArmor/protobuf" + . "github.com/kubearmor/KubeArmor/tests/util" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = BeforeSuite(func() { + + // configure multiubuntu deployment + err := K8sApplyFile("manifests/privileged-deploy.yaml") + Expect(err).To(BeNil()) + + // delete all KSPs + err = DeleteAllKsp() + Expect(err).To(BeNil()) + + // enable kubearmor port forwarding + err = KubearmorPortForward() + Expect(err).To(BeNil()) + +}) + +var _ = AfterSuite(func() { + // delete multiubuntu deployment + err := K8sDelete([]string{"manifests/privileged-deploy.yaml"}) + Expect(err).To(BeNil()) + + KubearmorPortForwardStop() +}) + +func getUbuntuPod(name string, ant string) string { + pods, err := K8sGetPods(name, "privileged", []string{ant}, 60) + Expect(err).To(BeNil()) + Expect(len(pods)).To(Equal(1)) + return pods[0] +} + +var _ = Describe("Ksp", func() { + var priv string + BeforeEach(func() { + priv = getUbuntuPod("privileged-deployment", "kubearmor-policy: enabled") + }) + + AfterEach(func() { + KarmorLogStop() + err := DeleteAllKsp() + Expect(err).To(BeNil()) + }) + + Describe("Privileged containers test", func() { + It("policies work for privileged containers", func() { + err := K8sApply([]string{"manifests/priv-container-block-ls.yaml"}) + Expect(err).To(BeNil()) + + err = KarmorLogStart("policy", "privileged", "Process", priv) + Expect(err).To(BeNil()) + + // execute ls inside priv-container - would block + sout, _, err := K8sExecInPodWithContainer(priv, "privileged", "priv-container", + []string{"bash", "-c", "ls /"}) + Expect(err).To(BeNil()) + fmt.Printf("OUTPUT: %s\n", sout) + Expect(sout).To(MatchRegexp(".*Permission denied")) + + // alert should be present for the above block + expect := &protobuf.Alert{ + NamespaceName: "privileged", + ContainerName: "priv-container", + PolicyName: "privileged-container-block-ls", + Action: "Block", + Result: "Permission denied", + } + + logs, err := KarmorGetTargetAlert(5*time.Second, expect) + Expect(err).To(BeNil()) + Expect(logs.Found).To(BeTrue()) + }) + + It("won't block mount by default for privileged containers", func() { + // execute mount inside priv-container - would pass + + err := KarmorLogStart("policy", "privileged", "Syscall", priv) + Expect(err).To(BeNil()) + + sout, _, err := K8sExecInPodWithContainer(priv, "privileged", "priv-container", + []string{"bash", "-c", "mkdir /mnt/test"}) + Expect(err).To(BeNil()) + sout, _, err = K8sExecInPodWithContainer(priv, "privileged", "priv-container", + []string{"bash", "-c", "mount /dev/loop0 /mnt/test"}) + Expect(err).To(BeNil()) + fmt.Printf("OUTPUT: %s\n", sout) + + // TODO: match syscall alerts for mount once syscall audit events are fixed + /* + expect := protobuf.Alert{ + PolicyName: "DefaultPosture", + Result: "Passed", + Data: "syscall=SYS_MOUNT", + } + */ + + // no alert should be present for the above + expect := &protobuf.Alert{ + NamespaceName: "privileged", + ContainerName: "priv-container", + } + + logs, err := KarmorGetTargetAlert(5*time.Second, expect) + Expect(err).To(BeNil()) + Expect(logs.Found).To(BeFalse()) + }) + + It("won't block umount by default for privileged containers", func() { + // execute umount inside priv-container - would pass + // Start KubeArmor Logs + if strings.Contains(K8sRuntimeEnforcer(), "bpf") { + Skip("Skipping due to apparmor specific policy") + } + + err := KarmorLogStart("policy", "privileged", "Syscall", priv) + Expect(err).To(BeNil()) + + sout, _, err := K8sExecInPodWithContainer(priv, "privileged", "priv-container", + []string{"bash", "-c", "umount /var/run/secrets/kubernetes.io/serviceaccount"}) + Expect(err).To(BeNil()) + fmt.Printf("OUTPUT: %s\n", sout) + + // no alert should be present for the above + expect := &protobuf.Alert{ + NamespaceName: "privileged", + ContainerName: "priv-container", + } + + logs, err := KarmorGetTargetAlert(5*time.Second, expect) + Expect(err).To(BeNil()) + Expect(logs.Found).To(BeFalse()) + + // execute umount inside unpriv-container - would fail + sout, _, err = K8sExecInPodWithContainer(priv, "privileged", "unpriv-container", + []string{"bash", "-c", "umount /var/run/secrets/kubernetes.io/serviceaccount"}) + Expect(err).To(BeNil()) + fmt.Printf("OUTPUT: %s\n", sout) + + expect = &protobuf.Alert{ + PolicyName: "DefaultPosture", + Action: "Block", + Result: "Operation not permitted", + Data: "syscall=SYS_UMOUNT2", + } + + res, err := KarmorGetTargetAlert(5*time.Second, expect) + Expect(err).To(BeNil()) + Expect(res.Found).To(BeTrue()) + }) + + }) +})