diff --git a/ecs-init/apparmor/apparmor.go b/ecs-init/apparmor/apparmor.go new file mode 100644 index 00000000000..418eaeaf03f --- /dev/null +++ b/ecs-init/apparmor/apparmor.go @@ -0,0 +1,81 @@ +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, + capability, + 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, +} +` + +// 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 := isLoaded(profileName) + if err != nil { + return err + } + if yes { + return nil + } + + f, err := os.Create(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 := load(path); err != nil { + return fmt.Errorf("load apparmor profile %s: %w", path, err) + } + return nil +} 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 e2cc3fa9d70..0515edd026c 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" ) @@ -62,6 +66,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 f313678295c..344bc857d29 100644 --- a/ecs-init/engine/engine.go +++ b/ecs-init/engine/engine.go @@ -21,6 +21,7 @@ import ( "os" "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" @@ -31,6 +32,7 @@ import ( "github.com/aws/amazon-ecs-agent/ecs-init/gpu" log "github.com/cihub/seelog" + ctrdapparmor "github.com/containerd/containerd/pkg/apparmor" ) const ( @@ -113,6 +115,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() @@ -195,6 +202,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 ctrdapparmor.HostSupports() { + log.Infof("pre-start: setting up %s AppArmor profile", apparmor.ECSDefaultProfileName) + return apparmor.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/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=""