From 75aa21331834939676e7b64c41d52f2b6b1dada6 Mon Sep 17 00:00:00 2001 From: SreeeS Date: Mon, 2 Oct 2023 09:20:23 -0700 Subject: [PATCH] Add apparmor support --- ecs-init/apparmor/apparmor.go | 91 ++++++++++++++++ ecs-init/apparmor/apparmor_test.go | 101 ++++++++++++++++++ ecs-init/apparmor/apparmor_utils.go | 68 ++++++++++++ ecs-init/config/development.go | 51 --------- ecs-init/config/release.go | 3 - ecs-init/docker/docker_config.go | 8 ++ ecs-init/engine/engine.go | 27 ++++- ecs-init/engine/engine_test.go | 49 +++++++++ ecs-init/go.mod | 4 +- .../containerd/pkg/apparmor/apparmor.go | 28 +++++ .../containerd/pkg/apparmor/apparmor_linux.go | 45 ++++++++ .../pkg/apparmor/apparmor_unsupported.go | 24 +++++ ecs-init/vendor/modules.txt | 1 + scripts/gobuild.sh | 2 +- 14 files changed, 442 insertions(+), 60 deletions(-) create mode 100644 ecs-init/apparmor/apparmor.go create mode 100644 ecs-init/apparmor/apparmor_test.go create mode 100644 ecs-init/apparmor/apparmor_utils.go delete mode 100644 ecs-init/config/development.go create mode 100644 ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor.go create mode 100644 ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor_linux.go create mode 100644 ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor_unsupported.go diff --git a/ecs-init/apparmor/apparmor.go b/ecs-init/apparmor/apparmor.go new file mode 100644 index 00000000000..cb446d5e7f5 --- /dev/null +++ b/ecs-init/apparmor/apparmor.go @@ -0,0 +1,91 @@ +package apparmor + +import ( + "fmt" + "os" + "path/filepath" +) + +const ( + ECSDefaultProfileName = "ecs-default" + appArmorProfileDir = "/etc/apparmor.d" +) + +const ecsDefaultProfile = ` +#include + +profile ecs-default flags=(attach_disconnected,mediate_deleted) { + #include + + network inet, # Allow IPv4 traffic + network inet6, # Allow IPv6 traffic + + capability net_admin, # Allow network configuration + capability sys_admin, # Allow ECS Agent to invoke the setns system call + + file, + umount, + # Host (privileged) processes may send signals to container processes. + signal (receive) peer=unconfined, + # Container processes may send signals amongst themselves. + signal (send,receive) peer=ecs-default, + + # ECS agent requires DBUS send + dbus (send) bus=system, + + deny @{PROC}/* w, # deny write for all files directly in /proc (not in a subdir) + # deny write to files not in /proc//** or /proc/sys/** + deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9/]*}/** w, + deny @{PROC}/sys/[^k]** w, # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel) + deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w, # deny everything except shm* in /proc/sys/kernel/ + deny @{PROC}/sysrq-trigger 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/** rwklx, + deny /sys/kernel/security/** rwklx, + + # suppress ptrace denials when using 'docker ps' or using 'ps' inside a container + ptrace (trace,read,tracedby,readby) peer=ecs-default, +} +` + +var ( + isProfileLoaded = isLoaded + loadPath = load + createFile = os.Create +) + +// LoadDefaultProfile ensures the default profile to be loaded with the given name. +// Returns nil error if the profile is already loaded. +func LoadDefaultProfile(profileName string) error { + yes, err := isProfileLoaded(profileName) + if err != nil { + return err + } + if yes { + return nil + } + + f, err := createFile(filepath.Join(appArmorProfileDir, profileName)) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(ecsDefaultProfile) + if err != nil { + return err + } + path := f.Name() + + if err := loadPath(path); err != nil { + return fmt.Errorf("error loading apparmor profile %s: %w", path, err) + } + return nil +} diff --git a/ecs-init/apparmor/apparmor_test.go b/ecs-init/apparmor/apparmor_test.go new file mode 100644 index 00000000000..7c0e0263fc3 --- /dev/null +++ b/ecs-init/apparmor/apparmor_test.go @@ -0,0 +1,101 @@ +// Copyright 2015-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 apparmor + +import ( + "errors" + "github.com/containerd/containerd/pkg/apparmor" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadDefaultProfile(t *testing.T) { + testCases := []struct { + name string + profileName string + isLoadedResponse bool + isLoadedError error + loadError error + expectedError error + }{ + { + name: "ProfileIsAlreadyLoaded", + profileName: "testProfile.txt", + isLoadedResponse: true, + isLoadedError: nil, + loadError: nil, + expectedError: nil, + }, + { + name: "ProfileNotLoaded", + profileName: "testProfile.txt", + isLoadedResponse: false, + isLoadedError: nil, + loadError: nil, + expectedError: nil, + }, + { + name: "IsLoadedError", + profileName: "testProfile.txt", + isLoadedResponse: false, + isLoadedError: errors.New("mock isLoaded error"), + loadError: nil, + expectedError: errors.New("mock isLoaded error"), + }, + { + name: "LoadProfileError", + profileName: "testProfile.txt", + isLoadedResponse: false, + isLoadedError: nil, + loadError: errors.New("mock load error"), + expectedError: errors.New("mock load error"), + }, + } + defer func() { + isProfileLoaded = isLoaded + loadPath = load + createFile = os.Create + }() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if !apparmor.HostSupports() { + t.Skip() + } + tmpdir := os.TempDir() + filePath, err := os.MkdirTemp(tmpdir, "test") + require.NoError(t, err) + createFile = func(profileName string) (*os.File, error) { + f, err := os.Create(filepath.Join(filePath, tc.profileName)) + return f, err + } + defer os.RemoveAll(filePath) + isProfileLoaded = func(profileName string) (bool, error) { + return tc.isLoadedResponse, tc.isLoadedError + } + loadPath = func(profile string) error { + return tc.loadError + } + err = LoadDefaultProfile(tc.profileName) + if tc.loadError == nil { + assert.Equal(t, tc.expectedError, err) + } else { + assert.Error(t, err) + } + }) + } +} diff --git a/ecs-init/apparmor/apparmor_utils.go b/ecs-init/apparmor/apparmor_utils.go new file mode 100644 index 00000000000..0829909a2d2 --- /dev/null +++ b/ecs-init/apparmor/apparmor_utils.go @@ -0,0 +1,68 @@ +/* + Copyright The docker Authors. + Copyright The Moby Authors. + 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 apparmor + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + exec "golang.org/x/sys/execabs" +) + +// NOTE: This code is copied from . +// If you plan to make any changes, please make sure they are also sent +// upstream. + +func load(path string) error { + out, err := aaParser("-Kr", path) + if err != nil { + return fmt.Errorf("parser error(%q): %w", strings.TrimSpace(out), err) + } + return nil +} + +func aaParser(args ...string) (string, error) { + out, err := exec.Command("apparmor_parser", args...).CombinedOutput() + return string(out), err +} + +func isLoaded(name string) (bool, error) { + f, err := os.Open("/sys/kernel/security/apparmor/profiles") + if err != nil { + return false, err + } + defer f.Close() + r := bufio.NewReader(f) + for { + p, err := r.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + return false, err + } + if strings.HasPrefix(p, name+" ") { + return true, nil + } + } + return false, nil +} diff --git a/ecs-init/config/development.go b/ecs-init/config/development.go deleted file mode 100644 index c4afbd4c427..00000000000 --- a/ecs-init/config/development.go +++ /dev/null @@ -1,51 +0,0 @@ -//go:build development -// +build development - -// Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"). You may -// not use this file except in compliance with the License. A copy of the -// License is located at -// -// http://aws.amazon.com/apache2.0/ -// -// or in the "license" file accompanying this file. This file 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 config - -import ( - "fmt" - "os" -) - -var directoryPrefix string -var s3Bucket string - -func init() { - fmt.Println("****************") - fmt.Println("DEVELOPMENT MODE") - directoryPrefix = getDirectoryPrefix() - s3Bucket = getS3Bucket() - fmt.Println("****************") -} - -func getDirectoryPrefix() string { - return getEnvWithDefault("PATH_PREFIX", "/tmp") -} - -func getS3Bucket() string { - return getEnvWithDefault("S3_BUCKET_OVERRIDE", "amazon-ecs-agent") -} - -func getEnvWithDefault(environmentVariable, defaultIfMissing string) string { - env := os.Getenv(environmentVariable) - if env == "" { - fmt.Printf("%s not set, using %s\n", environmentVariable, defaultIfMissing) - return defaultIfMissing - } - fmt.Printf("%s set as %s\n", environmentVariable, env) - return env -} diff --git a/ecs-init/config/release.go b/ecs-init/config/release.go index ee182136e89..0daf354ac43 100644 --- a/ecs-init/config/release.go +++ b/ecs-init/config/release.go @@ -1,6 +1,3 @@ -//go:build !development -// +build !development - // Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may diff --git a/ecs-init/docker/docker_config.go b/ecs-init/docker/docker_config.go index f86a83d4c2c..c7435feedd0 100644 --- a/ecs-init/docker/docker_config.go +++ b/ecs-init/docker/docker_config.go @@ -14,7 +14,11 @@ package docker import ( + "fmt" + + "github.com/aws/amazon-ecs-agent/ecs-init/apparmor" "github.com/aws/amazon-ecs-agent/ecs-init/config" + ctrdapparmor "github.com/containerd/containerd/pkg/apparmor" godocker "github.com/fsouza/go-dockerclient" ) @@ -61,6 +65,10 @@ func createHostConfig(binds []string) *godocker.HostConfig { Init: true, } + if ctrdapparmor.HostSupports() { + hostConfig.SecurityOpt = []string{fmt.Sprintf("apparmor:%s", apparmor.ECSDefaultProfileName)} + } + if config.RunPrivileged() { hostConfig.Privileged = true } diff --git a/ecs-init/engine/engine.go b/ecs-init/engine/engine.go index 87130cd3f9f..2a82e310fcd 100644 --- a/ecs-init/engine/engine.go +++ b/ecs-init/engine/engine.go @@ -20,6 +20,7 @@ import ( "math" "time" + "github.com/aws/amazon-ecs-agent/ecs-init/apparmor" "github.com/aws/amazon-ecs-agent/ecs-init/backoff" "github.com/aws/amazon-ecs-agent/ecs-init/cache" "github.com/aws/amazon-ecs-agent/ecs-init/config" @@ -30,6 +31,7 @@ import ( "github.com/aws/amazon-ecs-agent/ecs-init/gpu" log "github.com/cihub/seelog" + ctrdapparmor "github.com/containerd/containerd/pkg/apparmor" ) const ( @@ -47,9 +49,13 @@ const ( ) // Injection point for testing purposes -var getDockerClient = func() (dockerClient, error) { - return docker.Client() -} +var ( + getDockerClient = func() (dockerClient, error) { + return docker.Client() + } + hostSupports = ctrdapparmor.HostSupports + loadDefaultProfile = apparmor.LoadDefaultProfile +) func dockerError(err error) error { return engineError("could not create docker client", err) @@ -111,6 +117,11 @@ func (e *Engine) PreStart() error { if err != nil { return err } + // setup AppArmor if necessary + err = e.PreStartAppArmor() + if err != nil { + return err + } // Enable use of loopback addresses for local routing purposes log.Info("pre-start: enabling loopback routing") err = e.loopbackRouting.Enable() @@ -188,6 +199,16 @@ func (e *Engine) PreStartGPU() error { return nil } +// PreStartAppArmor sets up the ecs-default AppArmor profile if we're running +// on an AppArmor-enabled system. +func (e *Engine) PreStartAppArmor() error { + if hostSupports() { + log.Infof("pre-start: setting up %s AppArmor profile", apparmor.ECSDefaultProfileName) + return loadDefaultProfile(apparmor.ECSDefaultProfileName) + } + return nil +} + // ReloadCache reloads the cached image of the ECS Agent into Docker func (e *Engine) ReloadCache() error { docker, err := getDockerClient() diff --git a/ecs-init/engine/engine_test.go b/ecs-init/engine/engine_test.go index 029c48aa9ba..46b92295b64 100644 --- a/ecs-init/engine/engine_test.go +++ b/ecs-init/engine/engine_test.go @@ -24,9 +24,13 @@ import ( "os" "testing" + "github.com/aws/amazon-ecs-agent/ecs-init/apparmor" "github.com/aws/amazon-ecs-agent/ecs-init/cache" "github.com/aws/amazon-ecs-agent/ecs-init/gpu" "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + ctrdapparmor "github.com/containerd/containerd/pkg/apparmor" ) // getDockerClientMock backs up getDockerClient package-level function and replaces it with the mock passed as @@ -584,3 +588,48 @@ func TestPostStopCredentialsProxyRouteRemoveError(t *testing.T) { t.Errorf("engine post-stop error: %v", err) } } + +func TestPreStartAppArmorSetup(t *testing.T) { + testCases := []struct { + name string + hostSupports bool + loadProfileError error + expectedError error + }{ + { + name: "HostNotSupported", + hostSupports: false, + loadProfileError: nil, + expectedError: nil, + }, + { + name: "HostSupportedNoError", + hostSupports: true, + loadProfileError: nil, + expectedError: nil, + }, + { + name: "HostSupportedWithError", + hostSupports: true, + loadProfileError: errors.New("error loading apparmor profile"), + expectedError: errors.New("error loading apparmor profile"), + }, + } + defer func() { + hostSupports = ctrdapparmor.HostSupports + loadDefaultProfile = apparmor.LoadDefaultProfile + }() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + hostSupports = func() bool { + return tc.hostSupports + } + loadDefaultProfile = func(profile string) error { + return tc.loadProfileError + } + engine := &Engine{} + err := engine.PreStartAppArmor() + assert.Equal(t, tc.expectedError, err) + }) + } +} diff --git a/ecs-init/go.mod b/ecs-init/go.mod index 819d5d2f0b1..dcf4bc86703 100644 --- a/ecs-init/go.mod +++ b/ecs-init/go.mod @@ -6,17 +6,18 @@ require ( github.com/NVIDIA/gpu-monitoring-tools v0.0.0-20180829222009-86f2a9fac6c5 github.com/aws/aws-sdk-go v1.36.0 github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 + github.com/containerd/containerd v1.6.18 github.com/docker/go-plugins-helpers v0.0.0-20181025120712-1e6269c305b8 github.com/fsouza/go-dockerclient v0.0.0-20170830181106-98edf3edfae6 github.com/golang/mock v1.6.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.7.0 + golang.org/x/sys v0.6.0 ) require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Microsoft/go-winio v0.5.2 // indirect - github.com/containerd/containerd v1.6.18 // indirect github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/docker v23.0.3+incompatible // indirect @@ -38,7 +39,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect golang.org/x/net v0.8.0 // indirect - golang.org/x/sys v0.6.0 // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.3.0 // indirect diff --git a/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor.go b/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor.go new file mode 100644 index 00000000000..293f8ba499b --- /dev/null +++ b/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor.go @@ -0,0 +1,28 @@ +/* + 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 apparmor + +// HostSupports returns true if apparmor is enabled for the host: +// - On Linux returns true if apparmor is enabled, apparmor_parser is +// present, and if we are not running docker-in-docker. +// - On non-Linux returns false. +// +// This is derived from libcontainer/apparmor.IsEnabled(), with the addition +// of checks for apparmor_parser to be present and docker-in-docker. +func HostSupports() bool { + return hostSupports() +} diff --git a/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor_linux.go b/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor_linux.go new file mode 100644 index 00000000000..c96de6a2688 --- /dev/null +++ b/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor_linux.go @@ -0,0 +1,45 @@ +/* + 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 apparmor + +import ( + "os" + "sync" +) + +var ( + appArmorSupported bool + checkAppArmor sync.Once +) + +// hostSupports returns true if apparmor is enabled for the host, if +// apparmor_parser is enabled, and if we are not running docker-in-docker. +// +// This is derived from libcontainer/apparmor.IsEnabled(), with the addition +// of checks for apparmor_parser to be present and docker-in-docker. +func hostSupports() bool { + checkAppArmor.Do(func() { + // see https://github.com/opencontainers/runc/blob/0d49470392206f40eaab3b2190a57fe7bb3df458/libcontainer/apparmor/apparmor_linux.go + if _, err := os.Stat("/sys/kernel/security/apparmor"); err == nil && os.Getenv("container") == "" { + if _, err = os.Stat("/sbin/apparmor_parser"); err == nil { + buf, err := os.ReadFile("/sys/module/apparmor/parameters/enabled") + appArmorSupported = err == nil && len(buf) > 1 && buf[0] == 'Y' + } + } + }) + return appArmorSupported +} diff --git a/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor_unsupported.go b/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor_unsupported.go new file mode 100644 index 00000000000..833170338ec --- /dev/null +++ b/ecs-init/vendor/github.com/containerd/containerd/pkg/apparmor/apparmor_unsupported.go @@ -0,0 +1,24 @@ +//go:build !linux +// +build !linux + +/* + 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 apparmor + +func hostSupports() bool { + return false +} diff --git a/ecs-init/vendor/modules.txt b/ecs-init/vendor/modules.txt index e3343e2235c..5f23ab8ae21 100644 --- a/ecs-init/vendor/modules.txt +++ b/ecs-init/vendor/modules.txt @@ -66,6 +66,7 @@ github.com/cihub/seelog/archive/tar github.com/cihub/seelog/archive/zip # github.com/containerd/containerd v1.6.18 ## explicit; go 1.17 +github.com/containerd/containerd/pkg/apparmor github.com/containerd/containerd/pkg/userns # github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e => github.com/coreos/go-systemd/v22 v22.0.0 ## explicit; go 1.12 diff --git a/scripts/gobuild.sh b/scripts/gobuild.sh index a2ef793f411..894bec4154e 100755 --- a/scripts/gobuild.sh +++ b/scripts/gobuild.sh @@ -42,7 +42,7 @@ mkdir -p "${SRCPATH}" ln -s "${TOPWD}/ecs-init" "${SRCPATH}" cd "${SRCPATH}/ecs-init" if [[ "$1" == "dev" ]]; then - CGO_ENABLED=1 CGO_LDFLAGS_ALLOW='-Wl,--unresolved-symbols=ignore-in-object-files' go build -tags 'development' -ldflags "${VERSION_FLAG} ${GIT_HASH_FLAG} ${GIT_DIRTY_FLAG}" \ + CGO_ENABLED=1 CGO_LDFLAGS_ALLOW='-Wl,--unresolved-symbols=ignore-in-object-files' go build -ldflags "${VERSION_FLAG} ${GIT_HASH_FLAG} ${GIT_DIRTY_FLAG}" \ -o "${TOPWD}/amazon-ecs-init" else tags=""