Skip to content

Commit

Permalink
Add apparmor support
Browse files Browse the repository at this point in the history
  • Loading branch information
SreeeS committed Oct 3, 2023
1 parent 4f42396 commit a483e19
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 11 deletions.
22 changes: 16 additions & 6 deletions ecs-init/apparmor/apparmor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ const ecsDefaultProfile = `
profile ecs-default flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network,
capability,
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.
Expand Down Expand Up @@ -52,18 +56,24 @@ profile ecs-default flags=(attach_disconnected,mediate_deleted) {
}
`

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 := isLoaded(profileName)
yes, err := isProfileLoaded(profileName)
if err != nil {
return err
}
if yes {
return nil
}

f, err := os.Create(filepath.Join(appArmorProfileDir, profileName))
f, err := createFile(filepath.Join(appArmorProfileDir, profileName))
if err != nil {
return err
}
Expand All @@ -74,8 +84,8 @@ func LoadDefaultProfile(profileName string) error {
}
path := f.Name()

if err := load(path); err != nil {
return fmt.Errorf("load apparmor profile %s: %w", path, err)
if err := loadPath(path); err != nil {
return fmt.Errorf("error loading apparmor profile %s: %w", path, err)
}
return nil
}
101 changes: 101 additions & 0 deletions ecs-init/apparmor/apparmor_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
14 changes: 9 additions & 5 deletions ecs-init/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,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)
Expand Down Expand Up @@ -198,9 +202,9 @@ func (e *Engine) PreStartGPU() error {
// 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() {
if hostSupports() {
log.Infof("pre-start: setting up %s AppArmor profile", apparmor.ECSDefaultProfileName)
return apparmor.LoadDefaultProfile(apparmor.ECSDefaultProfileName)
return loadDefaultProfile(apparmor.ECSDefaultProfileName)
}
return nil
}
Expand Down
49 changes: 49 additions & 0 deletions ecs-init/engine/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
})
}
}

0 comments on commit a483e19

Please sign in to comment.