diff --git a/Dockerfile b/Dockerfile index 59f4b54f7f6..04bd04d8c43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -189,6 +189,7 @@ FROM ubuntu:${UBUNTU_VERSION} AS base # fuse3 is required by stargz snapshotter RUN apt-get update && \ apt-get install -qq -y --no-install-recommends \ + apparmor \ ca-certificates curl \ iproute2 iptables \ dbus systemd systemd-sysv \ diff --git a/Dockerfile.d/test-integration-rootless.sh b/Dockerfile.d/test-integration-rootless.sh index d5177f792ba..a768e04d1ef 100755 --- a/Dockerfile.d/test-integration-rootless.sh +++ b/Dockerfile.d/test-integration-rootless.sh @@ -16,6 +16,12 @@ set -eux -o pipefail if [[ "$(id -u)" = "0" ]]; then + if [ -e /sys/kernel/security/apparmor/profiles ]; then + # Load the "nerdctl-default" profile for TestRunApparmor + nerdctl apparmor load + fi + + # Switch to the rootless user via SSH systemctl start sshd exec ssh -o StrictHostKeyChecking=no rootless@localhost "$0" "$@" else diff --git a/README.md b/README.md index 7bb4d76c0a5..dbfd73fbfd1 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,8 @@ Minor: - Connecting a container to multiple networks at once: `nerdctl run --net foo --net bar` - Running [FreeBSD jails](./docs/freebsd.md). - Better multi-platform support, e.g., `nerdctl pull --all-platforms IMAGE` +- Applying an (existing) AppArmor profile to rootless containers: `nerdctl run --security-opt apparmor=`. + Use `sudo nerdctl apparmor load` to load the `nerdctl-default` profile. Trivial: - Inspecting raw OCI config: `nerdctl container inspect --mode=native` . @@ -253,6 +255,11 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl volume rm](#whale-nerdctl-volume-rm) - [Namespace management](#namespace-management) - [:nerd_face: :blue_square: nerdctl namespace ls](#nerd_face-blue_square-nerdctl-namespace-ls) + - [AppArmor profile management](#apparmor-profile-management) + - [:nerd_face: nerdctl apparmor inspect](#nerd_face-nerdctl-apparmor-inspect) + - [:nerd_face: nerdctl apparmor load](#nerd_face-nerdctl-apparmor-load) + - [:nerd_face: nerdctl apparmor ls](#nerd_face-nerdctl-apparmor-ls) + - [:nerd_face: nerdctl apparmor unload](#nerd_face-nerdctl-apparmor-unload) - [System](#system) - [:whale: nerdctl events](#whale-nerdctl-events) - [:whale: nerdctl info](#whale-nerdctl-info) @@ -922,6 +929,31 @@ Usage: `nerdctl namespace ls [OPTIONS]` Flags: - `-q, --quiet`: Only display namespace names +## AppArmor profile management +### :nerd_face: nerdctl apparmor inspect +Display the default AppArmor profile "nerdctl-default". Other profiles cannot be displayed with this command. + +Usage: `nerdctl apparmor inspect` + +### :nerd_face: nerdctl apparmor load +Load the default AppArmor profile "nerdctl-default". Requires root. + +Usage: `nerdctl apparmor load` + +### :nerd_face: nerdctl apparmor ls +List the loaded AppArmor profile + +Usage: `nerdctl apparmor ls [OPTIONS]` + +Flags: +- `-q, --quiet`: Only display volume names +- `--format`: Format the output using the given Go template, e.g, `{{json .}}` + +### :nerd_face: nerdctl apparmor unload +Unload an AppArmor profile. The target profile name defaults to "nerdctl-default". Requires root. + +Usage: `nerdctl apparmor unload [PROFILE]` + ## System ### :whale: nerdctl events Get real time events from the server. diff --git a/cmd/nerdctl/apparmor_inspect_linux.go b/cmd/nerdctl/apparmor_inspect_linux.go new file mode 100644 index 00000000000..80edf09a8bd --- /dev/null +++ b/cmd/nerdctl/apparmor_inspect_linux.go @@ -0,0 +1,46 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + + "github.com/containerd/containerd/contrib/apparmor" + "github.com/containerd/nerdctl/pkg/defaults" + "github.com/spf13/cobra" +) + +func newApparmorInspectCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "inspect", + Short: fmt.Sprintf("Display the default AppArmor profile %q. Other profiles cannot be displayed with this command.", defaults.AppArmorProfileName), + Args: cobra.NoArgs, + RunE: apparmorInspectAction, + SilenceUsage: true, + SilenceErrors: true, + } + return cmd +} + +func apparmorInspectAction(cmd *cobra.Command, args []string) error { + b, err := apparmor.DumpDefaultProfile(defaults.AppArmorProfileName) + if err != nil { + return err + } + _, err = fmt.Fprintf(cmd.OutOrStdout(), b) + return err +} diff --git a/cmd/nerdctl/apparmor_linux.go b/cmd/nerdctl/apparmor_linux.go new file mode 100644 index 00000000000..7c4806a13a9 --- /dev/null +++ b/cmd/nerdctl/apparmor_linux.go @@ -0,0 +1,39 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "github.com/spf13/cobra" +) + +func newApparmorCommand() *cobra.Command { + cmd := &cobra.Command{ + Category: CategoryManagement, + Use: "apparmor", + Short: "Manage AppArmor profiles", + RunE: unknownSubcommandAction, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.AddCommand( + newApparmorLsCommand(), + newApparmorInspectCommand(), + newApparmorLoadCommand(), + newApparmorUnloadCommand(), + ) + return cmd +} diff --git a/cmd/nerdctl/apparmor_load_linux.go b/cmd/nerdctl/apparmor_load_linux.go new file mode 100644 index 00000000000..f295faf795c --- /dev/null +++ b/cmd/nerdctl/apparmor_load_linux.go @@ -0,0 +1,43 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + + "github.com/containerd/containerd/contrib/apparmor" + "github.com/containerd/nerdctl/pkg/defaults" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func newApparmorLoadCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "load", + Short: fmt.Sprintf("Load the default AppArmor profile %q. Requires root.", defaults.AppArmorProfileName), + Args: cobra.NoArgs, + RunE: apparmorLoadAction, + SilenceUsage: true, + SilenceErrors: true, + } + return cmd +} + +func apparmorLoadAction(cmd *cobra.Command, args []string) error { + logrus.Infof("Loading profile %q", defaults.AppArmorProfileName) + return apparmor.LoadDefaultProfile(defaults.AppArmorProfileName) +} diff --git a/cmd/nerdctl/apparmor_ls_linux.go b/cmd/nerdctl/apparmor_ls_linux.go new file mode 100644 index 00000000000..140b5bee1ef --- /dev/null +++ b/cmd/nerdctl/apparmor_ls_linux.go @@ -0,0 +1,103 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "bytes" + "errors" + "fmt" + "text/tabwriter" + "text/template" + + "github.com/containerd/nerdctl/pkg/apparmorutil" + "github.com/spf13/cobra" +) + +func newApparmorLsCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List the loaded AppArmor profiles", + Args: cobra.NoArgs, + RunE: apparmorLsAction, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().BoolP("quiet", "q", false, "Only display profile names") + // Alias "-f" is reserved for "--filter" + cmd.Flags().String("format", "", "Format the output using the given go template") + cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"json"}, cobra.ShellCompDirectiveNoFileComp + }) + return cmd +} + +func apparmorLsAction(cmd *cobra.Command, args []string) error { + quiet, err := cmd.Flags().GetBool("quiet") + if err != nil { + return err + } + w := cmd.OutOrStdout() + var tmpl *template.Template + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } + switch format { + case "", "table": + w = tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) + if !quiet { + fmt.Fprintln(w, "NAME\tMODE") + } + case "raw": + return errors.New("unsupported format: \"raw\"") + default: + if quiet { + return errors.New("format and quiet must not be specified together") + } + var err error + tmpl, err = parseTemplate(format) + if err != nil { + return err + } + } + + profiles, err := apparmorutil.Profiles() + if err != nil { + return err + } + + for _, f := range profiles { + if tmpl != nil { + var b bytes.Buffer + if err := tmpl.Execute(&b, f); err != nil { + return err + } + if _, err = fmt.Fprintf(w, b.String()+"\n"); err != nil { + return err + } + } else if quiet { + fmt.Fprintln(w, f.Name) + } else { + fmt.Fprintf(w, "%s\t%s\n", f.Name, f.Mode) + } + } + if f, ok := w.(Flusher); ok { + return f.Flush() + } + return nil +} diff --git a/cmd/nerdctl/apparmor_unload_linux.go b/cmd/nerdctl/apparmor_unload_linux.go new file mode 100644 index 00000000000..15154f3b95a --- /dev/null +++ b/cmd/nerdctl/apparmor_unload_linux.go @@ -0,0 +1,52 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + + "github.com/containerd/nerdctl/pkg/apparmorutil" + "github.com/containerd/nerdctl/pkg/defaults" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func newApparmorUnloadCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "unload [PROFILE]", + Short: fmt.Sprintf("Unload an AppArmor profile. The target profile name defaults to %q. Requires root.", defaults.AppArmorProfileName), + Args: cobra.MaximumNArgs(1), + RunE: apparmorUnloadAction, + ValidArgsFunction: apparmorUnloadShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + return cmd +} + +func apparmorUnloadAction(cmd *cobra.Command, args []string) error { + target := defaults.AppArmorProfileName + if len(args) > 0 { + target = args[0] + } + logrus.Infof("Unloading profile %q", target) + return apparmorutil.Unload(target) +} + +func apparmorUnloadShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return shellCompleteApparmorProfiles(cmd) +} diff --git a/cmd/nerdctl/completion_linux.go b/cmd/nerdctl/completion_linux.go new file mode 100644 index 00000000000..e89d5e40960 --- /dev/null +++ b/cmd/nerdctl/completion_linux.go @@ -0,0 +1,34 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "github.com/containerd/nerdctl/pkg/apparmorutil" + "github.com/spf13/cobra" +) + +func shellCompleteApparmorProfiles(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) { + profiles, err := apparmorutil.Profiles() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string // nolint: prealloc + for _, f := range profiles { + names = append(names, f.Name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index e82d417e156..d397d3757a0 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -244,6 +244,7 @@ func newApp() *cobra.Command { // Compose newComposeCommand(), ) + addApparmorCommand(rootCmd) return rootCmd } diff --git a/cmd/nerdctl/main_freebsd.go b/cmd/nerdctl/main_freebsd.go index a76ff02290a..451a0547145 100644 --- a/cmd/nerdctl/main_freebsd.go +++ b/cmd/nerdctl/main_freebsd.go @@ -27,3 +27,7 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } + +func addApparmorCommand(rootCmd *cobra.Command) { + // NOP +} diff --git a/cmd/nerdctl/main_linux.go b/cmd/nerdctl/main_linux.go index 27fe2a85a2c..3283cafa083 100644 --- a/cmd/nerdctl/main_linux.go +++ b/cmd/nerdctl/main_linux.go @@ -37,7 +37,9 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { return true } switch commands[1] { - case "", "completion", "login", "logout": + // completion, login, logout: false, because it shouldn't require the daemon to be running + // apparmor: false, because it requires the initial mount namespace to access /sys/kernel/security + case "", "completion", "login", "logout", "apparmor": return false } return true @@ -53,3 +55,7 @@ func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComple } return candidates, cobra.ShellCompDirectiveNoFileComp } + +func addApparmorCommand(rootCmd *cobra.Command) { + rootCmd.AddCommand(newApparmorCommand()) +} diff --git a/cmd/nerdctl/main_windows.go b/cmd/nerdctl/main_windows.go index 365d6286bda..a39914984d0 100644 --- a/cmd/nerdctl/main_windows.go +++ b/cmd/nerdctl/main_windows.go @@ -35,3 +35,7 @@ func shellCompleteSnapshotterNames(cmd *cobra.Command, args []string, toComplete func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp } + +func addApparmorCommand(rootCmd *cobra.Command) { + // NOP +} diff --git a/cmd/nerdctl/run_security.go b/cmd/nerdctl/run_security_linux.go similarity index 85% rename from cmd/nerdctl/run_security.go rename to cmd/nerdctl/run_security_linux.go index 73591b6ae86..312199e6eb5 100644 --- a/cmd/nerdctl/run_security.go +++ b/cmd/nerdctl/run_security_linux.go @@ -25,7 +25,7 @@ import ( "github.com/containerd/containerd/contrib/apparmor" "github.com/containerd/containerd/contrib/seccomp" "github.com/containerd/containerd/oci" - pkgapparmor "github.com/containerd/containerd/pkg/apparmor" + "github.com/containerd/nerdctl/pkg/apparmorutil" "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/strutil" @@ -53,20 +53,28 @@ func generateSecurityOpts(securityOptsMap map[string]string) ([]oci.SpecOpts, er opts = append(opts, seccomp.WithDefaultProfile()) } - aaSupported := pkgapparmor.HostSupports() + canLoadNewAppArmor := apparmorutil.CanLoadNewProfile() + canApplyExistingProfile := apparmorutil.CanApplyExistingProfile() if aaProfile, ok := securityOptsMap["apparmor"]; ok { if aaProfile == "" { return nil, errors.New("invalid security-opt \"apparmor\"") } if aaProfile != "unconfined" { - if !aaSupported { + if !canApplyExistingProfile { logrus.Warnf("The host does not support AppArmor. Ignoring profile %q", aaProfile) } else { opts = append(opts, apparmor.WithProfile(aaProfile)) } } - } else if aaSupported { - opts = append(opts, apparmor.WithDefaultProfile(defaults.AppArmorProfileName)) + } else { + if canLoadNewAppArmor { + if err := apparmor.LoadDefaultProfile(defaults.AppArmorProfileName); err != nil { + return nil, err + } + } + if apparmorutil.CanApplySpecificExistingProfile(defaults.AppArmorProfileName) { + opts = append(opts, apparmor.WithProfile(defaults.AppArmorProfileName)) + } } nnp := false diff --git a/cmd/nerdctl/run_security_test.go b/cmd/nerdctl/run_security_test.go index 0d0c48e8771..00281d59d0f 100644 --- a/cmd/nerdctl/run_security_test.go +++ b/cmd/nerdctl/run_security_test.go @@ -18,10 +18,12 @@ package main import ( "fmt" + "os" "strconv" "strings" "testing" + "github.com/containerd/nerdctl/pkg/apparmorutil" "github.com/containerd/nerdctl/pkg/testutil" "gotest.tools/v3/assert" @@ -150,3 +152,20 @@ func TestRunSecurityOptSeccomp(t *testing.T) { }) } } + +func TestRunApparmor(t *testing.T) { + base := testutil.NewBase(t) + defaultProfile := fmt.Sprintf("%s-default", base.Target) + if !apparmorutil.CanLoadNewProfile() && !apparmorutil.CanApplySpecificExistingProfile(defaultProfile) { + t.Skipf("needs to be able to apply %q profile", defaultProfile) + } + attrCurrentPath := "/proc/self/attr/apparmor/current" + if _, err := os.Stat(attrCurrentPath); err != nil { + attrCurrentPath = "/proc/self/attr/current" + } + attrCurrentEnforceExpected := fmt.Sprintf("%s (enforce)\n", defaultProfile) + base.Cmd("run", "--rm", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected) + base.Cmd("run", "--rm", "--security-opt", "apparmor="+defaultProfile, testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected) + base.Cmd("run", "--rm", "--security-opt", "apparmor=unconfined", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly("unconfined\n") + base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly("unconfined\n") +} diff --git a/pkg/apparmorutil/apparmorutil_linux.go b/pkg/apparmorutil/apparmorutil_linux.go new file mode 100644 index 00000000000..901940180d0 --- /dev/null +++ b/pkg/apparmorutil/apparmorutil_linux.go @@ -0,0 +1,133 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package apparmorutil + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/containerd/containerd/pkg/apparmor" + "github.com/containerd/containerd/pkg/userns" + "github.com/sirupsen/logrus" +) + +// CanLoadNewProfile returns whether the current process can load a new AppArmor profile. +// +// CanLoadNewProfile needs root. +// +// CanLoadNewProfile checks both /sys/module/apparmor/parameters/enabled and /sys/kernel/security. +// +// Related: https://gitlab.com/apparmor/apparmor/-/blob/v3.0.3/libraries/libapparmor/src/kernel.c#L311 +func CanLoadNewProfile() bool { + return !userns.RunningInUserNS() && os.Geteuid() == 0 && apparmor.HostSupports() +} + +var ( + paramEnabled bool + paramEnabledOnce sync.Once +) + +// CanApplyExistingProfile returns whether the current process can apply an existing AppArmor profile +// to processes. +// +// CanApplyExistingProfile does NOT need root. +// +// CanApplyExistingProfile checks /sys/module/apparmor/parameters/enabled ,but does NOT check /sys/kernel/security/apparmor , +// which might not be accessible from user namespaces (because securityfs cannot be mounted in a user namespace) +// +// Related: https://gitlab.com/apparmor/apparmor/-/blob/v3.0.3/libraries/libapparmor/src/kernel.c#L311 +func CanApplyExistingProfile() bool { + paramEnabledOnce.Do(func() { + buf, err := os.ReadFile("/sys/module/apparmor/parameters/enabled") + paramEnabled = err == nil && len(buf) > 1 && buf[0] == 'Y' + }) + return paramEnabled +} + +// CanApplySpecificExistingProfile attempts to run `aa-exec -p -- true` to check whether +// the profile can be applied. +// +// CanApplySpecificExistingProfile does NOT depend on /sys/kernel/security/apparmor/profiles , +// which might not be accessible from user namespaces (because securityfs cannot be mounted in a user namespace) +func CanApplySpecificExistingProfile(profileName string) bool { + if !CanApplyExistingProfile() { + return false + } + cmd := exec.Command("aa-exec", "-p", profileName, "--", "true") + out, err := cmd.CombinedOutput() + if err != nil { + logrus.WithError(err).Debugf("failed to run %v: %q", cmd.Args, string(out)) + return false + } + return true +} + +type Profile struct { + Name string `json:"Name"` // e.g., "nerdctl-default" + Mode string `json:"Mode,omitempty"` // e.g., "enforce" +} + +// Profiles return profiles. +// +// Profiles does not need the root but needs access to /sys/kernel/security/apparmor/policy/profiles, +// which might not be accessible from user namespaces (because securityfs cannot be mounted in a user namespace) +// +// So, Profiles cannot be called from rootless child. +func Profiles() ([]Profile, error) { + const profilesPath = "/sys/kernel/security/apparmor/policy/profiles" + ents, err := os.ReadDir(profilesPath) + if err != nil { + return nil, err + } + res := make([]Profile, len(ents)) + for i, ent := range ents { + namePath := filepath.Join(profilesPath, ent.Name(), "name") + b, err := os.ReadFile(namePath) + if err != nil { + logrus.WithError(err).Warnf("failed to read %q", namePath) + continue + } + profile := Profile{ + Name: strings.TrimSpace(string(b)), + } + modePath := filepath.Join(profilesPath, ent.Name(), "mode") + b, err = os.ReadFile(modePath) + if err != nil { + logrus.WithError(err).Warnf("failed to read %q", namePath) + } else { + profile.Mode = strings.TrimSpace(string(b)) + } + res[i] = profile + } + return res, nil +} + +// Unload unloads a profile. Needs access to /sys/kernel/security/apparmor/.remove . +func Unload(target string) error { + remover, err := os.OpenFile("/sys/kernel/security/apparmor/.remove", os.O_RDWR|os.O_TRUNC, 0644) + if err != nil { + return err + } + if _, err := remover.Write([]byte(target)); err != nil { + remover.Close() + return err + } + return remover.Close() +} diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go index d50b6489642..e86c3fa513e 100644 --- a/pkg/infoutil/infoutil.go +++ b/pkg/infoutil/infoutil.go @@ -24,11 +24,8 @@ import ( "time" "github.com/containerd/containerd" - "github.com/containerd/containerd/pkg/apparmor" "github.com/containerd/containerd/services/introspection" - "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" - "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/containerd/nerdctl/pkg/version" ptypes "github.com/gogo/protobuf/types" ) @@ -67,16 +64,6 @@ func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupMan return nil, err } info.ServerVersion = daemonVersion.Version - if apparmor.HostSupports() { - info.SecurityOptions = append(info.SecurityOptions, "name=apparmor") - } - info.SecurityOptions = append(info.SecurityOptions, "name=seccomp,profile=default") - if defaults.CgroupnsMode() == "private" { - info.SecurityOptions = append(info.SecurityOptions, "name=cgroupns") - } - if rootlessutil.IsRootlessChild() { - info.SecurityOptions = append(info.SecurityOptions, "name=rootless") - } fulfillPlatformInfo(&info) return &info, nil } diff --git a/pkg/infoutil/infoutil_linux.go b/pkg/infoutil/infoutil_linux.go index 02ce61e9b0e..b5608ef5c66 100644 --- a/pkg/infoutil/infoutil_linux.go +++ b/pkg/infoutil/infoutil_linux.go @@ -18,8 +18,11 @@ package infoutil import ( "fmt" + "strings" "github.com/containerd/cgroups" + "github.com/containerd/nerdctl/pkg/apparmorutil" + "github.com/containerd/nerdctl/pkg/defaults" "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/pkg/rootlessutil" "github.com/docker/docker/pkg/sysinfo" @@ -36,11 +39,31 @@ func CgroupsVersion() string { return "1" } +func fulfillSecurityOptions(info *dockercompat.Info) { + if apparmorutil.CanApplyExistingProfile() { + info.SecurityOptions = append(info.SecurityOptions, "name=apparmor") + if rootlessutil.IsRootless() && !apparmorutil.CanApplySpecificExistingProfile(defaults.AppArmorProfileName) { + info.Warnings = append(info.Warnings, fmt.Sprintf(strings.TrimSpace(` +WARNING: AppArmor profile %q is not loaded. + Use 'sudo nerdctl apparmor load' if you prefer to use AppArmor with rootless mode. + This warning is negligible if you do not intend to use AppArmor.`), defaults.AppArmorProfileName)) + } + } + info.SecurityOptions = append(info.SecurityOptions, "name=seccomp,profile=default") + if defaults.CgroupnsMode() == "private" { + info.SecurityOptions = append(info.SecurityOptions, "name=cgroupns") + } + if rootlessutil.IsRootlessChild() { + info.SecurityOptions = append(info.SecurityOptions, "name=rootless") + } +} + // fulfillPlatformInfo fulfills cgroup and kernel info. // // fulfillPlatformInfo requires the following fields to be set: -// CgroupDriver, CgroupVersion +// SecurityOptions, CgroupDriver, CgroupVersion func fulfillPlatformInfo(info *dockercompat.Info) { + fulfillSecurityOptions(info) var mobySysInfoOpts []sysinfo.Opt if info.CgroupDriver == "systemd" && info.CgroupVersion == "2" && rootlessutil.IsRootless() { g := fmt.Sprintf("/user.slice/user-%d.slice", rootlessutil.ParentEUID()) diff --git a/pkg/ocihook/ocihook.go b/pkg/ocihook/ocihook.go index 869f1f95721..5edb21ba270 100644 --- a/pkg/ocihook/ocihook.go +++ b/pkg/ocihook/ocihook.go @@ -28,7 +28,6 @@ import ( "strings" "github.com/containerd/containerd/cmd/ctr/commands" - pkgapparmor "github.com/containerd/containerd/pkg/apparmor" gocni "github.com/containerd/go-cni" "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore" "github.com/containerd/nerdctl/pkg/labels" @@ -292,9 +291,7 @@ func getPortMapOpts(opts *handlerOpts) ([]gocni.NamespaceOpts, error) { } func onCreateRuntime(opts *handlerOpts) error { - if pkgapparmor.HostSupports() { - loadAppArmor() - } + loadAppArmor() if opts.cni != nil { portMapOpts, err := getPortMapOpts(opts) diff --git a/pkg/ocihook/ocihook_linux.go b/pkg/ocihook/ocihook_linux.go index 2c6b71ee9b5..eb8d531533b 100644 --- a/pkg/ocihook/ocihook_linux.go +++ b/pkg/ocihook/ocihook_linux.go @@ -18,15 +18,22 @@ package ocihook import ( "github.com/containerd/containerd/contrib/apparmor" - + "github.com/containerd/nerdctl/pkg/apparmorutil" "github.com/containerd/nerdctl/pkg/defaults" "github.com/sirupsen/logrus" ) func loadAppArmor() { + if !apparmorutil.CanLoadNewProfile() { + return + } // ensure that the default profile is loaded to the host if err := apparmor.LoadDefaultProfile(defaults.AppArmorProfileName); err != nil { logrus.WithError(err).Errorf("failed to load AppArmor profile %q", defaults.AppArmorProfileName) + // We do not abort here. This is by design, and not a security issue. + // + // If the container is configured to use the default AppArmor profile + // but the profile was not actually loaded, runc will fail. } }