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 Agent AppArmor Support (aka Ubuntu 22 support) #4052

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ Additionally, the following environment variable(s) can be used to configure the
| `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` | <true | false> | By default, the ecs-init service adds an iptable rule to block access to ECS Agent's introspection port from off-host (or containers in awsvpc network mode), and removes the rule upon stop. If `ECS_ALLOW_OFFHOST_INTROSPECTION_ACCESS` is set to true, this rule will not be added/removed. | false |
| `ECS_OFFHOST_INTROSPECTION_INTERFACE_NAME` | `eth0` | Primary network interface name to be used for blocking offhost agent introspection port access. By default, this value is `eth0` | `eth0` |
| `ECS_AGENT_LABELS` | `{"test.label.1":"value1","test.label.2":"value2"}` | The labels to add to the ECS Agent container. | |
| `ECS_AGENT_APPARMOR_PROFILE` | `unconfined` | Specifies the name of the AppArmor profile to run the ecs-agent container under. This only applies to AppArmor-enabled systems, such as Ubuntu, Debian, and SUSE. If unset, defaults to the profile written out by ecs-init (ecs-agent-default). | `ecs-agent-default` |



Expand Down
92 changes: 92 additions & 0 deletions ecs-init/apparmor/apparmor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package apparmor

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

"github.com/aws/amazon-ecs-agent/ecs-init/config"
"github.com/docker/docker/pkg/aaparser"
aaprofile "github.com/docker/docker/profiles/apparmor"
)

const (
ECSAgentDefaultProfileName = config.ECSAgentAppArmorDefaultProfileName
appArmorProfileDir = "/etc/apparmor.d"
)

const ecsAgentDefaultProfile = `
#include <tunables/global>

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

network inet,
network inet6,
network netlink,
network unix,
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-agent-default,

# ECS agent requires DBUS send
dbus (send,receive) 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-agent-default,
ptrace (trace,read,tracedby,readby) peer=docker-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 {
_, err := isProfileLoaded(profileName)
if err != nil {
return err
}

f, err := createFile(filepath.Join(appArmorProfileDir, profileName))
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(ecsAgentDefaultProfile)
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)
}
})
}
}
16 changes: 15 additions & 1 deletion ecs-init/config/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ const (
// defaultCredentialsFetcherSocketPath is set to /var/credentials-fetcher/socket/credentials_fetcher.sock
// in case path is not passed in the env variable
DefaultCredentialsFetcherSocketPath = "/var/credentials-fetcher/socket/credentials_fetcher.sock"

// ECSAgentAppArmorProfileNameEnvVar specifies the AppArmor profile name to use. Only applies
// on AppArmor-enabled platforms (such as Ubuntu and Debian).
ECSAgentAppArmorProfileNameEnvVar = "ECS_AGENT_APPARMOR_PROFILE"
ECSAgentAppArmorDefaultProfileName = "ecs-agent-default"
)

// partitionBucketRegion provides the "partitional" bucket region
Expand Down Expand Up @@ -249,7 +254,7 @@ func CgroupMountpoint() string {

// MountDirectoryEBS returns the location on disk where EBS volumes will be mounted
func MountDirectoryEBS() string {
return directoryPrefix + "/mnt/ecs/ebs"
return directoryPrefix + "/mnt/ecs/ebs"
}

// HostCertsDirPath() returns the CA store path on the host
Expand Down Expand Up @@ -336,6 +341,15 @@ func RunningInExternal() bool {
return envVar == "true"
}

// ECSAgentApparmorProfileName returns the name of the AppArmor profile to use.
func ECSAgentAppArmorProfileName() string {
envVar := os.Getenv(ECSAgentAppArmorProfileNameEnvVar)
if len(strings.TrimSpace(envVar)) == 0 {
return ECSAgentAppArmorDefaultProfileName
}
return envVar
}

func agentArtifactName(version string, arch string) (string, error) {
var interpose string
switch arch {
Expand Down
10 changes: 10 additions & 0 deletions ecs-init/config/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ func TestDockerUnixSocketWithDockerHost(t *testing.T) {
}
}

func TestECSAgentAppArmorProfileName(t *testing.T) {
profile := ECSAgentAppArmorProfileName()
assert.Equal(t, profile, "ecs-agent-default")

os.Setenv(ECSAgentAppArmorProfileNameEnvVar, "docker-default")
defer os.Unsetenv(ECSAgentAppArmorProfileNameEnvVar)
profile = ECSAgentAppArmorProfileName()
assert.Equal(t, profile, "docker-default")
}

func TestGetAgentPartitionBucketRegion(t *testing.T) {
testCases := []struct {
region string
Expand Down
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
7 changes: 7 additions & 0 deletions ecs-init/docker/docker_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
package docker

import (
"fmt"

"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 +65,10 @@ func createHostConfig(binds []string) *godocker.HostConfig {
Init: true,
}

if ctrdapparmor.HostSupports() {
hostConfig.SecurityOpt = []string{fmt.Sprintf("apparmor:%s", config.ECSAgentAppArmorProfileName())}
}

if config.RunPrivileged() {
hostConfig.Privileged = true
}
Expand Down
Loading