Change:
What happened: Pods with invalid container-level seccomp profiles allow its init containers to be executed. Pods with unloaded apparmor profiles blocks the entire pod. What you expected to happen: Consistency between both. In one hand AppArmor blocks all pod execution regardless of the containers having or not a profile attached to it. On the other, Seccomp allows for an init container to deploy its own profile - as currently done on . How to reproduce it (as minimally and precisely as possible): Apply the yaml below: Resulting in only the init container from the being executed: ! Anything else we need to know?: AppArmor is currently being target to GA on 1.21 (). Environment: Kubernetes version (use ): Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.0", GitCommit:"", GitTreeState:"clean", BuildDate:"2020-12-08T17:59:43Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"} Server Version: version.Info{Major:"1", Minor:"21+", GitVersion:"v1.21.0-alpha.0.292+", GitCommit:"", GitTreeState:"clean", BuildDate:"2020-12-12T23:13:21Z", GoVersion:"go1.15.6", Compiler:"gc", Platform:"linux/amd64"} Cloud provider or hardware configuration: OS (e.g: ): Kernel (e.g. ):Install tools: Network plugin and version (if this is a network-related bug): Others: /sig node /area security
/cc
/sig security /triage accepted
I think we can consider this as a bug, right? Then we could work on a fix for v1.21 interdependently of the AppArmor GA graduation.
Sounds like a good plan :+1:
Issues go stale after 90d of inactivity. Mark the issue as fresh with . Stale issues rot after an additional 30d of inactivity and eventually close. If this issue is safe to close now please do so with . Send feedback to sig-contributor-experience at . /lifecycle stale
/remove-lifecycle stale
Issues go stale after 90d of inactivity. Mark the issue as fresh with . Stale issues rot after an additional 30d of inactivity and eventually close. If this issue is safe to close now please do so with . Send feedback to sig-contributor-experience at . /lifecycle stale
/remove-lifecycle stale
The Kubernetes project currently lacks enough contributors to adequately respond to all issues and PRs. This bot triages issues and PRs according to the following rules: After 90d of inactivity, is applied After 30d of inactivity since was applied, is applied After 30d of inactivity since was applied, the issue is closed You can: Mark this issue or PR as fresh with Mark this issue or PR as rotten with Close this issue or PR with Offer to help out with [Issue Triage][1] Please send feedback to sig-contributor-experience at . /lifecycle stale [1]:
/remove-lifecycle stale
/assign

Discussion:
return v.validateHostErr } <del> loadedProfiles, err := v.getLoadedProfiles() if err != nil { return fmt.Errorf("could not read loaded profiles: %v", err) } </del> var retErr error podutil.VisitContainers(&pod.Spec, podutil.AllContainers, func(container *v1.Container, containerType podutil.ContainerType) bool { <del> retErr = validateProfile(GetProfileName(pod, container.Name), loadedProfiles) </del> <ins> retErr = ValidateProfileFormat(GetProfileName(pod, container.Name)) </ins> if retErr != nil { return false }

return nil } <del> // Verify that the profile is valid and loaded. func validateProfile(profile string, loadedProfiles map[string]bool) error { if err := ValidateProfileFormat(profile); err != nil { return err } if strings.HasPrefix(profile, v1.AppArmorBetaProfileNamePrefix) { profileName := strings.TrimPrefix(profile, v1.AppArmorBetaProfileNamePrefix) if !loadedProfiles[profileName] { return fmt.Errorf("profile %q is not loaded", profileName) } } return nil } </del> // ValidateProfileFormat checks the format of the profile. func ValidateProfileFormat(profile string) error { if profile == "" || profile == v1.AppArmorBetaProfileRuntimeDefault || profile == v1.AppArmorBetaProfileNameUnconfined {

return nil } <del> func (v *validator) getLoadedProfiles() (map[string]bool, error) { profilesPath := path.Join(v.appArmorFS, "profiles") profilesFile, err := os.Open(profilesPath) if err != nil { return nil, fmt.Errorf("failed to open %s: %v", profilesPath, err) } defer profilesFile.Close() profiles := map[string]bool{} scanner := bufio.NewScanner(profilesFile) for scanner.Scan() { profileName := parseProfileName(scanner.Text()) if profileName == "" { // Unknown line format; skip it. continue } profiles[profileName] = true } return profiles, nil } // The profiles file is formatted with one profile per line, matching a form: //   namespace://profile-name (mode) //   profile-name (mode) // Where mode is {enforce, complain, kill}. The "namespace://" is only included for namespaced // profiles. For the purposes of Kubernetes, we consider the namespace part of the profile name. func parseProfileName(profileLine string) string { modeIndex := strings.IndexRune(profileLine, '(') if modeIndex < 0 { return "" } return strings.TrimSpace(profileLine[:modeIndex]) } </del> func getAppArmorFS() (string, error) { mountsFile, err := os.Open("/proc/mounts") if err != nil {

"fmt" "testing" <del> "k8s.io/api/core/v1" </del> <ins> v1 "k8s.io/api/core/v1" </ins> metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/stretchr/testify/assert"

assert.Error(t, validateHost("rkt")) } <del> func TestValidateProfile(t *testing.T) { loadedProfiles := map[string]bool{ "docker-default": true, "foo-bar":        true, "baz":            true, "/usr/sbin/ntpd": true, "/usr/lib/connman/scripts/dhclient-script":      true, "/usr/lib/NetworkManager/nm-dhcp-client.action": true, "/usr/bin/evince-previewer//sanitized_helper":   true, } </del> <ins> func TestValidateProfileFormat(t *testing.T) { </ins> tests := []struct { profile     string expectValid bool

{"baz", false}, // Missing local prefix. {v1.AppArmorBetaProfileNamePrefix + "/usr/sbin/ntpd", true}, {v1.AppArmorBetaProfileNamePrefix + "foo-bar", true}, <del> {v1.AppArmorBetaProfileNamePrefix + "unloaded", false}, // Not loaded. {v1.AppArmorBetaProfileNamePrefix + "", false}, </del> } for _, test := range tests { <del> err := validateProfile(test.profile, loadedProfiles) </del> <ins> err := ValidateProfileFormat(test.profile) </ins> if test.expectValid { assert.NoError(t, err, "Profile %s should be valid", test.profile) } else {

{v1.AppArmorBetaProfileNamePrefix + "foo-container", true}, {v1.AppArmorBetaProfileNamePrefix + "/usr/sbin/ntpd", true}, {"docker-default", false}, <del> {v1.AppArmorBetaProfileNamePrefix + "foo", false}, {v1.AppArmorBetaProfileNamePrefix + "", false}, </del> } for _, test := range tests {

}, } assert.NoError(t, v.Validate(pod), "Multi-container pod should validate") <del> for k, val := range pod.Annotations { pod.Annotations[k] = val + "-bad" assert.Error(t, v.Validate(pod), fmt.Sprintf("Multi-container pod with invalid profile %s:%s", k, pod.Annotations[k])) pod.Annotations[k] = val // Restore. } } func TestParseProfileName(t *testing.T) { tests := []struct{ line, expected string }{ {"foo://bar/baz (kill)", "foo://bar/baz"}, {"foo-bar (enforce)", "foo-bar"}, {"/usr/foo/bar/baz (complain)", "/usr/foo/bar/baz"}, } for _, test := range tests { name := parseProfileName(test.line) assert.Equal(t, test.expected, name, "Parsing %s", test.line) } </del> } func getPodWithProfile(profile string) *v1.Pod {