Skip to content

Commit

Permalink
gMSA support on Linux - capability advertisement
Browse files Browse the repository at this point in the history
*changes to ecs-init to establish communication of agent with
credentials-fetcher daemon
*changes to agent to advertise gMSA capability on Linux
  • Loading branch information
saikiranakula-amzn committed Oct 25, 2022
1 parent 32fcc33 commit e2e5955
Show file tree
Hide file tree
Showing 16 changed files with 354 additions and 56 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ additional details on each available environment variable.
| `ECS_ENABLE_GPU_SUPPORT` | `true` | Whether you use container instances with GPU support. This parameter is specified for the agent. You must also configure your task definitions for GPU. For more information | `false` | `Not applicable` |
| `HTTP_PROXY` | `10.0.0.131:3128` | The hostname (or IP address) and port number of an HTTP proxy to use for the Amazon ECS agent to connect to the internet. For example, this proxy will be used if your container instances do not have external network access through an Amazon VPC internet gateway or NAT gateway or instance. If this variable is set, you must also set the NO_PROXY variable to filter Amazon EC2 instance metadata and Docker daemon traffic from the proxy. | `null` | `null` |
| `NO_PROXY` | <For Linux: 169.254.169.254,169.254.170.2,/var/run/docker.sock &#124; For Windows: 169.254.169.254,169.254.170.2,\\.\pipe\docker_engine> | The HTTP traffic that should not be forwarded to the specified HTTP_PROXY. You must specify 169.254.169.254,/var/run/docker.sock to filter Amazon EC2 instance metadata and Docker daemon traffic from the proxy. | `null` | `null` |
| `CREDENTIALS_FETCHER_HOST` | `unix:///var/credentials-fetcher/socket/credentials_fetcher.sock` | Used to create a connection to the [credentials-fetcher daemon](https://github.com/aws/credentials-fetcher); to support gMSA on Linux. The default is fine for most users, only needs to be modified if user is configuring a custom credentials-fetcher socket path, ie, [CF_UNIX_DOMAIN_SOCKET_DIR](https://github.com/aws/credentials-fetcher#default-environment-variables). | `unix:///var/credentials-fetcher/socket/credentials_fetcher.sock` | Not Applicable |

### Persistence

Expand Down
8 changes: 8 additions & 0 deletions agent/app/agent_capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@ func (agent *ecsAgent) appendDockerDependentCapabilities(capabilities []*ecs.Att
return capabilities
}

func (agent *ecsAgent) appendGMSACapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
if agent.cfg.GMSACapable {
return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityGMSA)
}

return capabilities
}

func (agent *ecsAgent) appendLoggingDriverCapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
knownVersions := make(map[dockerclient.DockerVersion]struct{})
// Determine known API versions. Known versions are used exclusively for logging-driver enablement, since none of
Expand Down
26 changes: 26 additions & 0 deletions agent/app/agent_capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1416,3 +1416,29 @@ func TestAppendAndRemoveAttributes(t *testing.T) {
Name: aws.String("cap-2"),
})
}

func TestAppendGMSACapabilities(t *testing.T) {
var inputCapabilities []*ecs.Attribute
var expectedCapabilities []*ecs.Attribute

expectedCapabilities = append(expectedCapabilities,
[]*ecs.Attribute{
{
Name: aws.String(attributePrefix + capabilityGMSA),
},
}...)

agent := &ecsAgent{
cfg: &config.Config{
GMSACapable: true,
},
}

capabilities := agent.appendGMSACapabilities(inputCapabilities)

assert.Equal(t, len(expectedCapabilities), len(capabilities))
for i, expected := range expectedCapabilities {
assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(capabilities[i].Name))
assert.Equal(t, aws.StringValue(expected.Value), aws.StringValue(capabilities[i].Value))
}
}
4 changes: 0 additions & 4 deletions agent/app/agent_capability_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,6 @@ func (agent *ecsAgent) appendFirelensConfigCapabilities(capabilities []*ecs.Attr
return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityFirelensConfigS3)
}

func (agent *ecsAgent) appendGMSACapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
return capabilities
}

func (agent *ecsAgent) appendIPv6Capability(capabilities []*ecs.Attribute) []*ecs.Attribute {
return appendNameOnlyAttribute(capabilities, attributePrefix+taskENIIPv6AttributeSuffix)
}
Expand Down
10 changes: 0 additions & 10 deletions agent/app/agent_capability_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -913,16 +913,6 @@ func TestFirelensConfigCapabilitiesUnix(t *testing.T) {
assert.Contains(t, capabilities, &ecs.Attribute{Name: aws.String(attributePrefix + capabilityFirelensConfigS3)})
}

func TestAppendGMSACapabilities(t *testing.T) {
var inputCapabilities []*ecs.Attribute

agent := &ecsAgent{}

capabilities := agent.appendGMSACapabilities(inputCapabilities)
assert.Equal(t, len(inputCapabilities), len(capabilities))
assert.EqualValues(t, capabilities, inputCapabilities)
}

func TestAppendFSxWindowsFileServerCapabilities(t *testing.T) {
var inputCapabilities []*ecs.Attribute

Expand Down
8 changes: 0 additions & 8 deletions agent/app/agent_capability_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,6 @@ func (agent *ecsAgent) appendFirelensConfigCapabilities(capabilities []*ecs.Attr
return capabilities
}

func (agent *ecsAgent) appendGMSACapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
if agent.cfg.GMSACapable {
return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityGMSA)
}

return capabilities
}

func (agent *ecsAgent) appendEFSVolumePluginCapabilities(capabilities []*ecs.Attribute, pluginCapability string) []*ecs.Attribute {
return capabilities
}
Expand Down
26 changes: 0 additions & 26 deletions agent/app/agent_capability_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,32 +225,6 @@ func TestSupportedCapabilitiesWindows(t *testing.T) {
}
}

func TestAppendGMSACapabilities(t *testing.T) {
var inputCapabilities []*ecs.Attribute
var expectedCapabilities []*ecs.Attribute

expectedCapabilities = append(expectedCapabilities,
[]*ecs.Attribute{
{
Name: aws.String(attributePrefix + capabilityGMSA),
},
}...)

agent := &ecsAgent{
cfg: &config.Config{
GMSACapable: true,
},
}

capabilities := agent.appendGMSACapabilities(inputCapabilities)

assert.Equal(t, len(expectedCapabilities), len(capabilities))
for i, expected := range expectedCapabilities {
assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(capabilities[i].Name))
assert.Equal(t, aws.StringValue(expected.Value), aws.StringValue(capabilities[i].Value))
}
}

func TestAppendGMSACapabilitiesFalse(t *testing.T) {
var inputCapabilities []*ecs.Attribute
var expectedCapabilities []*ecs.Attribute
Expand Down
2 changes: 1 addition & 1 deletion agent/config/config_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func DefaultConfig() Config {
PollingMetricsWaitDuration: DefaultPollingMetricsWaitDuration,
NvidiaRuntime: DefaultNvidiaRuntime,
CgroupCPUPeriod: defaultCgroupCPUPeriod,
GMSACapable: false,
GMSACapable: parseGMSACapability(),
FSxWindowsFileServerCapable: false,
RuntimeStatsLogFile: defaultRuntimeStatsLogFile,
EnableRuntimeStats: BooleanDefaultFalse{Value: NotSet},
Expand Down
7 changes: 7 additions & 0 deletions agent/config/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import (
cnitypes "github.com/containernetworking/cni/pkg/types"
)

const (
// envSkipDomainJoinCheck is an environment setting that can be used to skip
// domain join check validation. This is useful for integration and
// functional-tests but should not be set for any non-test use-case.
envSkipDomainJoinCheck = "ZZZ_SKIP_DOMAIN_JOIN_CHECK_NOT_SUPPORTED_IN_PRODUCTION"
)

func parseCheckpoint(dataDir string) BooleanDefaultFalse {
checkPoint := parseBooleanDefaultFalseConfig("ECS_CHECKPOINT")
if dataDir != "" {
Expand Down
35 changes: 34 additions & 1 deletion agent/config/parse_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// httpaws.amazon.com/apache2.0/
// 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
Expand All @@ -18,10 +18,43 @@ package config

import (
"errors"
"os"
"strings"

"github.com/aws/amazon-ecs-agent/agent/utils"
"github.com/cihub/seelog"
)

func parseGMSACapability() bool {
envStatus := utils.ParseBool(os.Getenv("ECS_GMSA_SUPPORTED"), true)
if envStatus {
// Check if domain join check override is present
skipDomainJoinCheck := utils.ParseBool(os.Getenv(envSkipDomainJoinCheck), false)
if skipDomainJoinCheck {
seelog.Infof("Skipping domain join validation based on environment override")
return true
}

// check if the credentials fetcher socket is created and exists
// this env variable is set in ecs-init module
if credentialsfetcherHostDir := os.Getenv("CREDENTIALS_FETCHER_HOST_DIR"); credentialsfetcherHostDir != "" {
_, err := os.Stat(credentialsfetcherHostDir)
if err != nil {
if os.IsNotExist(err) {
seelog.Errorf("CREDENTIALS_FETCHER_HOST_DIR not found, err: %v", err)
return false
}
}
// returns true if the container instance is domain joined
// this env variable is set in ecs-init module
isDomainJoined := utils.ParseBool(os.Getenv("ECS_DOMAIN_JOINED_LINUX_INSTANCE"), false)

if !isDomainJoined {
seelog.Error("gMSA on linux requires domain joined instance. Did not find expected env var ECS_DOMAIN_JOINED_LINUX_INSTANCE=true")
}
return isDomainJoined
}
}
return false
}

Expand Down
51 changes: 51 additions & 0 deletions agent/config/parse_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//go:build linux && unit
// +build linux,unit

// Copyright 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 (
"testing"

"github.com/stretchr/testify/assert"
)

func TestParseGMSACapabilitySupported(t *testing.T) {
t.Setenv("ECS_GMSA_SUPPORTED", "True")
t.Setenv("ECS_DOMAIN_JOINED_LINUX_INSTANCE", "True")
t.Setenv("CREDENTIALS_FETCHER_HOST_DIR", "/var/run")

assert.True(t, parseGMSACapability())
}

func TestParseGMSACapabilityNonDomainJoined(t *testing.T) {
t.Setenv("ECS_GMSA_SUPPORTED", "True")
t.Setenv("ECS_DOMAIN_JOINED_LINUX_INSTANCE", "False")

assert.False(t, parseGMSACapability())
}

func TestParseGMSACapabilityUnsupported(t *testing.T) {
t.Setenv("ECS_GMSA_SUPPORTED", "False")

assert.False(t, parseGMSACapability())
}

func TestSkipDomainJoinCheckParseGMSACapability(t *testing.T) {
t.Setenv("ECS_GMSA_SUPPORTED", "True")
t.Setenv("ZZZ_SKIP_DOMAIN_JOIN_CHECK_NOT_SUPPORTED_IN_PRODUCTION", "True")

assert.True(t, parseGMSACapability())
}
5 changes: 0 additions & 5 deletions agent/config/parse_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ import (
)

const (
// envSkipDomainJoinCheck is an environment setting that can be used to skip
// domain join check validation. This is useful for integration and
// functional-tests but should not be set for any non-test use-case.
envSkipDomainJoinCheck = "ZZZ_SKIP_DOMAIN_JOIN_CHECK_NOT_SUPPORTED_IN_PRODUCTION"

// envSkipWindowsServerVersionCheck is an environment setting that can be used
// to skip the windows server version check. This is useful for testing and
// should not be set for any non-test use-case.
Expand Down
28 changes: 28 additions & 0 deletions ecs-init/config/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ const (

// DefaultRegionEnvVar is the environment variable for specifying the default AWS region to use.
DefaultRegionEnvVar = "AWS_DEFAULT_REGION"

// ECSGMSASupportEnvVar indicates that the gMSA is supported
ECSGMSASupportEnvVar = "ECS_GMSA_SUPPORTED"

// CredentialsFetcherHostEnvVar is the environment variable that specifies the location of the credentials-fetcher daemon socket.
CredentialsFetcherHostEnvVar = "CREDENTIALS_FETCHER_HOST"

// this socket is exposed by credentials-fetcher (daemon for gMSA support on Linux)
// 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"
)

// partitionBucketRegion provides the "partitional" bucket region
Expand Down Expand Up @@ -212,6 +223,23 @@ func DockerUnixSocket() (string, bool) {
return "/var/run", false
}

// CredentialsFetcherUnixSocketHostPath returns the credentials fetcher daemon socket endpoint and whether it reads from CredentialsFetcherEnvVar
func CredentialsFetcherUnixSocket() string {
if credentialsFetcherHost := os.Getenv(CredentialsFetcherHostEnvVar); strings.HasPrefix(credentialsFetcherHost, UnixSocketPrefix) {
return strings.TrimPrefix(credentialsFetcherHost, UnixSocketPrefix)
}

return DefaultCredentialsFetcherSocketPath
}

// HostCredentialsFetcherPath() returns the daemon socket location if it is available
func HostCredentialsFetcherPath() (string, bool) {
if credentialsFetcherHost := CredentialsFetcherUnixSocket(); len(credentialsFetcherHost) > 0 {
return credentialsFetcherHost, true
}
return "", false
}

// CgroupMountpoint returns the cgroup mountpoint for the system
func CgroupMountpoint() string {
return cgroupMountpoint
Expand Down
32 changes: 32 additions & 0 deletions ecs-init/config/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,35 @@ func TestAgentRunningInExternal(t *testing.T) {
defer os.Unsetenv(ExternalEnvVar)
assert.True(t, RunningInExternal())
}

func TestCredentialsFetcherUnixSocketWithoutCredentialsFetcherHost(t *testing.T) {
credentialsFetcherUnixSocketSourcePath := CredentialsFetcherUnixSocket()

if credentialsFetcherUnixSocketSourcePath != "/var/credentials-fetcher/socket/credentials_fetcher.sock" {
t.Error("CredentialsFetcherUnixSocket() should be \"/var/credentials-fetcher/socket\"")
}

}

func TestCredentialsFetcherUnixSocketWithCredentialsFetcherHost(t *testing.T) {
t.Setenv("CREDENTIALS_FETCHER_HOST", "unix:///foo/bar")

credentialsFetcherUnixSocketSourcePath := CredentialsFetcherUnixSocket()
if credentialsFetcherUnixSocketSourcePath != "/foo/bar" {
t.Error("CredentialsFetcherUnixSocket() should be \"/foo/bar\"")
}
}

func TestHostCredentialsFetcherPath(t *testing.T) {
t.Setenv("CREDENTIALS_FETCHER_HOST", "unix:///foo/bar")

credentialsFetcherHost, ok := HostCredentialsFetcherPath()
assert.True(t, ok)
assert.Equal(t, "/foo/bar", credentialsFetcherHost)
}

func TestHostCredentialsFetcherPathHostNotFound(t *testing.T) {
credentialsFetcherHost, ok := HostCredentialsFetcherPath()
assert.True(t, ok)
assert.Equal(t, "/var/credentials-fetcher/socket/credentials_fetcher.sock", credentialsFetcherHost)
}
Loading

0 comments on commit e2e5955

Please sign in to comment.