Skip to content

Commit ccf878c

Browse files
authored
Merge pull request #1583 from PrimalPimmy/apparmor-le
feat: lenient apparmor profiles
2 parents 1705dfd + 6a21c5b commit ccf878c

26 files changed

+722
-120
lines changed

KubeArmor/core/containerdHandler.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ package core
77
import (
88
"context"
99
"fmt"
10+
"github.com/containerd/typeurl/v2"
1011
"os"
1112
"strconv"
1213
"strings"
1314
"time"
1415

16+
"golang.org/x/exp/slices"
17+
1518
kl "github.com/kubearmor/KubeArmor/KubeArmor/common"
1619
cfg "github.com/kubearmor/KubeArmor/KubeArmor/config"
1720
kg "github.com/kubearmor/KubeArmor/KubeArmor/log"
@@ -21,7 +24,6 @@ import (
2124
pb "github.com/containerd/containerd/api/services/containers/v1"
2225
pt "github.com/containerd/containerd/api/services/tasks/v1"
2326
"github.com/containerd/containerd/namespaces"
24-
"github.com/containerd/typeurl/v2"
2527
"google.golang.org/grpc"
2628

2729
specs "github.com/opencontainers/runtime-spec/specs-go"
@@ -31,6 +33,26 @@ import (
3133
// == Containerd Handler == //
3234
// ======================== //
3335

36+
// DefaultCaps contains all the default capabilities given to a
37+
// container by containerd runtime
38+
// Taken from - https://github.com/containerd/containerd/blob/main/oci/spec.go
39+
var defaultCaps = []string{
40+
"CAP_CHOWN",
41+
"CAP_DAC_OVERRIDE",
42+
"CAP_FSETID",
43+
"CAP_FOWNER",
44+
"CAP_MKNOD",
45+
"CAP_NET_RAW",
46+
"CAP_SETGID",
47+
"CAP_SETUID",
48+
"CAP_SETFCAP",
49+
"CAP_SETPCAP",
50+
"CAP_NET_BIND_SERVICE",
51+
"CAP_SYS_CHROOT",
52+
"CAP_KILL",
53+
"CAP_AUDIT_WRITE",
54+
}
55+
3456
// Containerd Handler
3557
var Containerd *ContainerdHandler
3658

@@ -148,6 +170,11 @@ func (ch *ContainerdHandler) GetContainerInfo(ctx context.Context, containerID s
148170
spec := iface.(*specs.Spec)
149171
container.AppArmorProfile = spec.Process.ApparmorProfile
150172

173+
// if a container has additional caps than default, we mark it as privileged
174+
if spec.Process.Capabilities != nil && slices.Compare(spec.Process.Capabilities.Permitted, defaultCaps) >= 0 {
175+
container.Privileged = true
176+
}
177+
151178
// == //
152179

153180
taskReq := pt.ListPidsRequest{ContainerID: container.ContainerID}
@@ -305,6 +332,10 @@ func (dm *KubeArmorDaemon) UpdateContainerdContainer(ctx context.Context, contai
305332
dm.EndPoints[idx].AppArmorProfiles = append(dm.EndPoints[idx].AppArmorProfiles, container.AppArmorProfile)
306333
}
307334

335+
if container.Privileged && dm.EndPoints[idx].PrivilegedContainers != nil {
336+
dm.EndPoints[idx].PrivilegedContainers[container.ContainerName] = struct{}{}
337+
}
338+
308339
break
309340
}
310341
}

KubeArmor/core/crioHandler.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ func (ch *CrioHandler) GetContainerInfo(ctx context.Context, containerID string)
119119

120120
// path to container's root storage
121121
container.AppArmorProfile = containerInfo.RuntimeSpec.Process.ApparmorProfile
122+
container.Privileged = containerInfo.Privileged
122123

123124
pid := strconv.Itoa(containerInfo.Pid)
124125

@@ -240,6 +241,10 @@ func (dm *KubeArmorDaemon) UpdateCrioContainer(ctx context.Context, containerID,
240241
dm.EndPoints[idx].AppArmorProfiles = append(dm.EndPoints[idx].AppArmorProfiles, container.AppArmorProfile)
241242
}
242243

244+
if container.Privileged && dm.EndPoints[idx].PrivilegedContainers != nil {
245+
dm.EndPoints[idx].PrivilegedContainers[container.ContainerName] = struct{}{}
246+
}
247+
243248
break
244249
}
245250
}

KubeArmor/core/dockerHandler.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ func (dh *DockerHandler) GetContainerInfo(containerID string) (tp.Container, err
126126
}
127127

128128
container.AppArmorProfile = inspect.AppArmorProfile
129+
if inspect.HostConfig != nil {
130+
container.Privileged = inspect.HostConfig.Privileged
131+
}
129132

130133
// == //
131134

@@ -294,6 +297,10 @@ func (dm *KubeArmorDaemon) GetAlreadyDeployedDockerContainers() {
294297
dm.EndPoints[idx].AppArmorProfiles = append(dm.EndPoints[idx].AppArmorProfiles, container.AppArmorProfile)
295298
}
296299

300+
if container.Privileged && dm.EndPoints[idx].PrivilegedContainers != nil {
301+
dm.EndPoints[idx].PrivilegedContainers[container.ContainerName] = struct{}{}
302+
}
303+
297304
break
298305
}
299306
}

KubeArmor/core/kubeUpdate.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ func (dm *KubeArmorDaemon) UpdateEndPointWithPod(action string, pod tp.K8sPod) {
268268
newPoint.AppArmorProfiles = append(newPoint.AppArmorProfiles, container.AppArmorProfile)
269269
}
270270

271+
// if container is privileged
272+
if _, ok := pod.PrivilegedContainers[container.ContainerName]; ok {
273+
container.Privileged = true
274+
}
275+
271276
dm.Containers[containerID] = container
272277

273278
// 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) {
433438
newEndPoint.AppArmorProfiles = append(newEndPoint.AppArmorProfiles, container.AppArmorProfile)
434439
}
435440

441+
// if container is privileged
442+
if _, ok := pod.PrivilegedContainers[container.ContainerName]; ok {
443+
container.Privileged = true
444+
}
445+
436446
dm.Containers[containerID] = container
437447
// in case if container runtime detect the container and emit that event before pod event then
438448
// the container id will be added to NsMap with "Unknown" namespace
@@ -688,6 +698,8 @@ func (dm *KubeArmorDaemon) WatchK8sPods() {
688698
}
689699
}
690700

701+
pod.PrivilegedContainers = make(map[string]struct{})
702+
pod.PrivilegedAppArmorProfiles = make(map[string]struct{})
691703
if dm.RuntimeEnforcer != nil && dm.RuntimeEnforcer.EnforcerType == "AppArmor" {
692704
appArmorAnnotations := map[string]string{}
693705
updateAppArmor := false
@@ -723,7 +735,7 @@ func (dm *KubeArmorDaemon) WatchK8sPods() {
723735
}
724736
}
725737
} else if pod.Metadata["owner.controller"] == "Pod" {
726-
pod, err := K8s.K8sClient.CoreV1().Pods("default").Get(context.Background(), "my-pod", metav1.GetOptions{})
738+
pod, err := K8s.K8sClient.CoreV1().Pods(pod.Metadata["namespaceName"]).Get(context.Background(), podOwnerName, metav1.GetOptions{})
727739
if err == nil {
728740
for _, c := range pod.Spec.Containers {
729741
containers = append(containers, c.Name)
@@ -747,15 +759,30 @@ func (dm *KubeArmorDaemon) WatchK8sPods() {
747759
}
748760

749761
for _, container := range event.Object.Spec.Containers {
762+
var privileged bool
763+
// store privileged containers
764+
if container.SecurityContext != nil &&
765+
((container.SecurityContext.Privileged != nil && *container.SecurityContext.Privileged) ||
766+
(container.SecurityContext.Capabilities != nil && len(container.SecurityContext.Capabilities.Add) > 0)) {
767+
pod.PrivilegedContainers[container.Name] = struct{}{}
768+
privileged = true
769+
}
770+
profileName := "kubearmor-" + pod.Metadata["namespaceName"] + "-" + podOwnerName + "-" + container.Name
750771
if _, ok := appArmorAnnotations[container.Name]; !ok && kl.ContainsElement(containers, container.Name) {
751-
appArmorAnnotations[container.Name] = "kubearmor-" + pod.Metadata["namespaceName"] + "-" + podOwnerName + "-" + container.Name
772+
appArmorAnnotations[container.Name] = profileName
752773
updateAppArmor = true
774+
// if the container is privileged or it has more than one capabilities added
775+
// handle the apparmor profile generation with privileged rules
776+
}
777+
if privileged {
778+
// container name is unique for all containers in a pod
779+
pod.PrivilegedAppArmorProfiles[profileName] = struct{}{}
753780
}
754781
}
755782

756783
if event.Type == "ADDED" {
757784
// update apparmor profiles
758-
dm.RuntimeEnforcer.UpdateAppArmorProfiles(pod.Metadata["podName"], "ADDED", appArmorAnnotations)
785+
dm.RuntimeEnforcer.UpdateAppArmorProfiles(pod.Metadata["podName"], "ADDED", appArmorAnnotations, pod.PrivilegedAppArmorProfiles)
759786

760787
if updateAppArmor && pod.Annotations["kubearmor-policy"] == "enabled" {
761788
if deploymentName, ok := pod.Metadata["owner.controllerName"]; ok {
@@ -794,7 +821,7 @@ func (dm *KubeArmorDaemon) WatchK8sPods() {
794821
}
795822
} else if event.Type == "DELETED" {
796823
// update apparmor profiles
797-
dm.RuntimeEnforcer.UpdateAppArmorProfiles(pod.Metadata["podName"], "DELETED", appArmorAnnotations)
824+
dm.RuntimeEnforcer.UpdateAppArmorProfiles(pod.Metadata["podName"], "DELETED", appArmorAnnotations, pod.PrivilegedAppArmorProfiles)
798825
}
799826
}
800827

KubeArmor/core/unorchestratedUpdates.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ func (dm *KubeArmorDaemon) ParseAndUpdateContainerSecurityPolicy(event tp.K8sKub
430430
endPointIndex := -1
431431
newPoint := tp.EndPoint{}
432432

433+
var privilegedProfiles map[string]struct{}
433434
for idx, endPoint := range dm.EndPoints {
434435
endPointIndex++
435436

@@ -509,7 +510,7 @@ func (dm *KubeArmorDaemon) ParseAndUpdateContainerSecurityPolicy(event tp.K8sKub
509510
}
510511

511512
if event.Type == "ADDED" {
512-
dm.RuntimeEnforcer.UpdateAppArmorProfiles(containername, "ADDED", appArmorAnnotations)
513+
dm.RuntimeEnforcer.UpdateAppArmorProfiles(containername, "ADDED", appArmorAnnotations, privilegedProfiles)
513514

514515
newPoint.SecurityPolicies = append(newPoint.SecurityPolicies, secPolicy)
515516
if i < 0 {
@@ -524,6 +525,9 @@ func (dm *KubeArmorDaemon) ParseAndUpdateContainerSecurityPolicy(event tp.K8sKub
524525
newPoint.NetworkVisibilityEnabled = true
525526
newPoint.CapabilitiesVisibilityEnabled = true
526527
newPoint.Containers = []string{}
528+
529+
newPoint.PrivilegedContainers = map[string]struct{}{}
530+
527531
dm.ContainersLock.Lock()
528532
for idx, ctr := range dm.Containers {
529533
if ctr.ContainerName == containername {

KubeArmor/enforcer/appArmorEnforcer.go

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type AppArmorEnforcer struct {
2929

3030
// default profile
3131
ApparmorDefault string
32+
// default privileged profile
33+
ApparmorDefaultPrivileged string
3234

3335
// host profile
3436
HostProfile string
@@ -37,6 +39,10 @@ type AppArmorEnforcer struct {
3739
AppArmorProfiles map[string][]string
3840
AppArmorProfilesLock *sync.RWMutex
3941

42+
// to keep track of privileged profiles for clean deletion
43+
AppArmorPrivilegedProfiles map[string]struct{}
44+
AppArmorPrivilegedProfilesLock *sync.RWMutex
45+
4046
// Regex used to get profile Names
4147
rgx *regexp.Regexp
4248
}
@@ -50,38 +56,39 @@ func NewAppArmorEnforcer(node tp.Node, logger *fd.Feeder) *AppArmorEnforcer {
5056

5157
// default profile
5258
ae.ApparmorDefault = `## == Managed by KubeArmor == ##
53-
5459
#include <tunables/global>
60+
5561
profile apparmor-default flags=(attach_disconnected,mediate_deleted) {
56-
## == PRE START == ##
57-
#include <abstractions/base>
58-
umount,
59-
file,
60-
network,
61-
capability,
62+
## == PRE START == ##
63+
` + AppArmorDefaultPreStart +
64+
`
65+
## == PRE END == ##
66+
67+
## == POLICY START == ##
68+
## == POLICY END == ##
69+
70+
## == POST START == ##
71+
` + AppArmorDefaultPostStart +
72+
`
73+
## == POST END == ##
74+
}
75+
`
76+
77+
ae.ApparmorDefaultPrivileged = `## == Managed by KubeArmor == ##
78+
#include <tunables/global>
79+
80+
profile apparmor-default flags=(attach_disconnected,mediate_deleted) {
81+
## == PRE START == ##
82+
` + AppArmorPrivilegedPreStart +
83+
`
6284
## == PRE END == ##
6385
6486
## == POLICY START == ##
6587
## == POLICY END == ##
6688
6789
## == POST START == ##
68-
/lib/x86_64-linux-gnu/{*,**} rm,
69-
70-
deny @{PROC}/{*,**^[0-9*],sys/kernel/shm*} wkx,
71-
deny @{PROC}/sysrq-trigger rwklx,
72-
deny @{PROC}/mem rwklx,
73-
deny @{PROC}/kmem rwklx,
74-
deny @{PROC}/kcore rwklx,
75-
76-
deny mount,
77-
78-
deny /sys/[^f]*/** wklx,
79-
deny /sys/f[^s]*/** wklx,
80-
deny /sys/fs/[^c]*/** wklx,
81-
deny /sys/fs/c[^g]*/** wklx,
82-
deny /sys/fs/cg[^r]*/** wklx,
83-
deny /sys/firmware/efi/efivars/** rwklx,
84-
deny /sys/kernel/security/** rwklx,
90+
` + AppArmorPrivilegedPostStart +
91+
`
8592
## == POST END == ##
8693
}
8794
`
@@ -96,6 +103,9 @@ deny /sys/kernel/security/** rwklx,
96103
ae.AppArmorProfiles = map[string][]string{}
97104
ae.AppArmorProfilesLock = &sync.RWMutex{}
98105

106+
ae.AppArmorPrivilegedProfiles = map[string]struct{}{}
107+
ae.AppArmorPrivilegedProfilesLock = new(sync.RWMutex)
108+
99109
files, err := os.ReadDir("/etc/apparmor.d")
100110
if err != nil {
101111
ae.Logger.Errf("Failed to read /etc/apparmor.d (%s)", err.Error())
@@ -173,7 +183,8 @@ func (ae *AppArmorEnforcer) DestroyAppArmorEnforcer() error {
173183
}
174184

175185
for profile := range ae.AppArmorProfiles {
176-
ae.UnregisterAppArmorProfile("", profile)
186+
_, privileged := ae.AppArmorPrivilegedProfiles[profile]
187+
ae.UnregisterAppArmorProfile("", profile, privileged)
177188
}
178189

179190
if cfg.GlobalCfg.HostPolicy {
@@ -190,7 +201,7 @@ func (ae *AppArmorEnforcer) DestroyAppArmorEnforcer() error {
190201
// ================================= //
191202

192203
// RegisterAppArmorProfile Function
193-
func (ae *AppArmorEnforcer) RegisterAppArmorProfile(podName, profileName string) bool {
204+
func (ae *AppArmorEnforcer) RegisterAppArmorProfile(podName, profileName string, privileged bool) bool {
194205
// skip if AppArmorEnforcer is not active
195206
if ae == nil {
196207
return true
@@ -216,7 +227,15 @@ func (ae *AppArmorEnforcer) RegisterAppArmorProfile(podName, profileName string)
216227
return true
217228
}
218229

219-
newProfile := strings.Replace(ae.ApparmorDefault, "apparmor-default", profileName, -1)
230+
// generate a profile with basic allows if a privileged container
231+
var newProfile string
232+
if privileged {
233+
newProfile = strings.Replace(ae.ApparmorDefaultPrivileged, "apparmor-default", profileName, -1)
234+
ae.AppArmorPrivilegedProfiles[profileName] = struct{}{}
235+
ae.Logger.Printf("Added an AppArmor profile for a privileged container (%s, %s)", podName, profileName)
236+
} else {
237+
newProfile = strings.Replace(ae.ApparmorDefault, "apparmor-default", profileName, -1)
238+
}
220239

221240
newFile, err := os.Create(filepath.Clean("/etc/apparmor.d/" + profileName))
222241
if err != nil {
@@ -247,7 +266,7 @@ func (ae *AppArmorEnforcer) RegisterAppArmorProfile(podName, profileName string)
247266
}
248267

249268
// UnregisterAppArmorProfile Function
250-
func (ae *AppArmorEnforcer) UnregisterAppArmorProfile(podName, profileName string) bool {
269+
func (ae *AppArmorEnforcer) UnregisterAppArmorProfile(podName, profileName string, privileged bool) bool {
251270
// skip if AppArmorEnforcer is not active
252271
if ae == nil {
253272
return true
@@ -285,7 +304,12 @@ func (ae *AppArmorEnforcer) UnregisterAppArmorProfile(podName, profileName strin
285304
return false
286305
}
287306

288-
newProfile := strings.Replace(ae.ApparmorDefault, "apparmor-default", profileName, -1)
307+
var newProfile string
308+
if privileged {
309+
newProfile = strings.Replace(ae.ApparmorDefaultPrivileged, "apparmor-default", profileName, -1)
310+
} else {
311+
newProfile = strings.Replace(ae.ApparmorDefault, "apparmor-default", profileName, -1)
312+
}
289313

290314
newFile, err := os.Create(filepath.Clean("/etc/apparmor.d/" + profileName))
291315
if err != nil {
@@ -454,7 +478,15 @@ func (ae *AppArmorEnforcer) UnregisterAppArmorHostProfile() bool {
454478

455479
// UpdateAppArmorProfile Function
456480
func (ae *AppArmorEnforcer) UpdateAppArmorProfile(endPoint tp.EndPoint, appArmorProfile string, securityPolicies []tp.SecurityPolicy) {
457-
if policyCount, newProfile, ok := ae.GenerateAppArmorProfile(appArmorProfile, securityPolicies, endPoint.DefaultPosture); ok {
481+
482+
/* For privileged profiles, we maintain a separate map so that privileged pods
483+
are identified separately and their profiles are generated accordingly
484+
*/
485+
ae.AppArmorPrivilegedProfilesLock.Lock()
486+
_, privileged := ae.AppArmorPrivilegedProfiles[appArmorProfile]
487+
ae.AppArmorPrivilegedProfilesLock.Unlock()
488+
489+
if policyCount, newProfile, ok := ae.GenerateAppArmorProfile(appArmorProfile, securityPolicies, endPoint.DefaultPosture, privileged); ok {
458490
newfile, err := os.Create(filepath.Clean("/etc/apparmor.d/" + appArmorProfile))
459491
if err != nil {
460492
ae.Logger.Warnf("Unable to open an AppArmor profile (%s, %s)", appArmorProfile, err.Error())

0 commit comments

Comments
 (0)