Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECS AppArmor Support (aka Ubuntu 22 support) #3941

Merged
merged 2 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions ecs-init/apparmor/apparmor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package apparmor

import (
"fmt"
"os"
"path/filepath"

"github.com/docker/docker/pkg/aaparser"
aaprofile "github.com/docker/docker/profiles/apparmor"
)

const (
ECSDefaultProfileName = "ecs-default"
appArmorProfileDir = "/etc/apparmor.d"
)

const ecsDefaultProfile = `
#include <tunables/global>

profile ecs-default flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>

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
capability dac_override, # Allow ECS Agent to file read, write, and execute permission
SreeeS marked this conversation as resolved.
Show resolved Hide resolved

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/<number>/** 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 = aaprofile.IsLoaded
loadPath = aaparser.LoadProfile
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 yes {
SreeeS marked this conversation as resolved.
Show resolved Hide resolved
return nil
}
if err != nil {
return err
}

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
}
103 changes: 103 additions & 0 deletions ecs-init/apparmor/apparmor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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"
"os"
"path/filepath"
"testing"

"github.com/containerd/containerd/pkg/apparmor"
"github.com/docker/docker/pkg/aaparser"
aaprofile "github.com/docker/docker/profiles/apparmor"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

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 = aaprofile.IsLoaded
loadPath = aaparser.LoadProfile
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)
}
})
}
}
51 changes: 0 additions & 51 deletions ecs-init/config/development.go

This file was deleted.

3 changes: 0 additions & 3 deletions ecs-init/config/release.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 8 additions & 0 deletions ecs-init/docker/docker_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
Expand Down
27 changes: 24 additions & 3 deletions ecs-init/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 (
Expand All @@ -49,9 +51,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)
Expand Down Expand Up @@ -113,6 +119,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()
Expand Down Expand Up @@ -195,6 +206,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()
Expand Down
Loading