Skip to content

Commit 06c88f7

Browse files
Advertise Domainless gMSA capability on Windows
1 parent f41cc6a commit 06c88f7

10 files changed

+197
-0
lines changed

agent/app/agent_capability.go

+12
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const (
6666
capabilityFirelensConfigS3 = "firelens.options.config.s3"
6767
capabilityFullTaskSync = "full-sync"
6868
capabilityGMSA = "gmsa"
69+
capabilityGMSADomainless = "gmsa-domainless"
6970
capabilityEFS = "efs"
7071
capabilityEFSAuth = "efsAuth"
7172
capabilityEnvFilesS3 = "env-files.s3"
@@ -271,6 +272,9 @@ func (agent *ecsAgent) capabilities() ([]*ecs.Attribute, error) {
271272
// support GMSA capabilities
272273
capabilities = agent.appendGMSACapabilities(capabilities)
273274

275+
// support GMSA domainless capabilities
276+
capabilities = agent.appendGMSADomainlessCapabilities(capabilities)
277+
274278
// support efs auth on ecs capabilities
275279
for _, cap := range agent.cfg.VolumePluginCapabilities {
276280
capabilities = agent.appendEFSVolumePluginCapabilities(capabilities, cap)
@@ -319,6 +323,14 @@ func (agent *ecsAgent) appendGMSACapabilities(capabilities []*ecs.Attribute) []*
319323
return capabilities
320324
}
321325

326+
func (agent *ecsAgent) appendGMSADomainlessCapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
327+
if agent.cfg.GMSADomainlessCapable.Enabled() {
328+
return appendNameOnlyAttribute(capabilities, attributePrefix+capabilityGMSADomainless)
329+
}
330+
331+
return capabilities
332+
}
333+
322334
func (agent *ecsAgent) appendLoggingDriverCapabilities(capabilities []*ecs.Attribute) []*ecs.Attribute {
323335
knownVersions := make(map[dockerclient.DockerVersion]struct{})
324336
// Determine known API versions. Known versions are used exclusively for logging-driver enablement, since none of

agent/app/agent_capability_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -1433,3 +1433,47 @@ func TestAppendGMSACapabilities(t *testing.T) {
14331433
assert.Equal(t, aws.StringValue(expected.Value), aws.StringValue(capabilities[i].Value))
14341434
}
14351435
}
1436+
1437+
func TestAppendGMSADomainlessCapabilities(t *testing.T) {
1438+
var inputCapabilities []*ecs.Attribute
1439+
var expectedCapabilities []*ecs.Attribute
1440+
1441+
expectedCapabilities = append(expectedCapabilities,
1442+
[]*ecs.Attribute{
1443+
{
1444+
Name: aws.String(attributePrefix + capabilityGMSADomainless),
1445+
},
1446+
}...)
1447+
1448+
agent := &ecsAgent{
1449+
cfg: &config.Config{
1450+
GMSADomainlessCapable: config.BooleanDefaultFalse{Value: config.ExplicitlyEnabled},
1451+
},
1452+
}
1453+
1454+
capabilities := agent.appendGMSADomainlessCapabilities(inputCapabilities)
1455+
1456+
assert.Equal(t, len(expectedCapabilities), len(capabilities))
1457+
for i, expected := range expectedCapabilities {
1458+
assert.Equal(t, aws.StringValue(expected.Name), aws.StringValue(capabilities[i].Name))
1459+
assert.Equal(t, aws.StringValue(expected.Value), aws.StringValue(capabilities[i].Value))
1460+
}
1461+
}
1462+
1463+
func TestAppendGMSADomainlessCapabilitiesFalse(t *testing.T) {
1464+
var inputCapabilities []*ecs.Attribute
1465+
var expectedCapabilities []*ecs.Attribute
1466+
1467+
expectedCapabilities = append(expectedCapabilities,
1468+
[]*ecs.Attribute{}...)
1469+
1470+
agent := &ecsAgent{
1471+
cfg: &config.Config{
1472+
GMSADomainlessCapable: config.BooleanDefaultFalse{Value: config.ExplicitlyDisabled},
1473+
},
1474+
}
1475+
1476+
capabilities := agent.appendGMSADomainlessCapabilities(inputCapabilities)
1477+
1478+
assert.Equal(t, len(expectedCapabilities), len(capabilities))
1479+
}

agent/config/config.go

+1
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ func environmentConfig() (Config, error) {
587587
CgroupCPUPeriod: parseCgroupCPUPeriod(),
588588
SpotInstanceDrainingEnabled: parseBooleanDefaultFalseConfig("ECS_ENABLE_SPOT_INSTANCE_DRAINING"),
589589
GMSACapable: parseGMSACapability(),
590+
GMSADomainlessCapable: parseGMSADomainlessCapability(),
590591
VolumePluginCapabilities: parseVolumePluginCapabilities(),
591592
FSxWindowsFileServerCapable: parseFSxWindowsFileServerCapability(),
592593
External: parseBooleanDefaultFalseConfig("ECS_EXTERNAL"),

agent/config/config_unix.go

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func DefaultConfig() Config {
102102
NvidiaRuntime: DefaultNvidiaRuntime,
103103
CgroupCPUPeriod: defaultCgroupCPUPeriod,
104104
GMSACapable: parseGMSACapability(),
105+
GMSADomainlessCapable: parseGMSADomainlessCapability(),
105106
FSxWindowsFileServerCapable: BooleanDefaultFalse{Value: ExplicitlyDisabled},
106107
RuntimeStatsLogFile: defaultRuntimeStatsLogFile,
107108
EnableRuntimeStats: BooleanDefaultFalse{Value: NotSet},

agent/config/config_windows.go

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func DefaultConfig() Config {
144144
PollMetrics: BooleanDefaultFalse{Value: NotSet},
145145
PollingMetricsWaitDuration: DefaultPollingMetricsWaitDuration,
146146
GMSACapable: BooleanDefaultFalse{Value: ExplicitlyDisabled},
147+
GMSADomainlessCapable: BooleanDefaultFalse{Value: ExplicitlyDisabled},
147148
FSxWindowsFileServerCapable: BooleanDefaultFalse{Value: ExplicitlyDisabled},
148149
PauseContainerImageName: DefaultPauseContainerImageName,
149150
PauseContainerTag: DefaultPauseContainerTag,

agent/config/parse_linux.go

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ func parseFSxWindowsFileServerCapability() BooleanDefaultFalse {
7272
return BooleanDefaultFalse{Value: ExplicitlyDisabled}
7373
}
7474

75+
func parseGMSADomainlessCapability() BooleanDefaultFalse {
76+
return BooleanDefaultFalse{Value: ExplicitlyDisabled}
77+
}
78+
7579
var IsWindows2016 = func() (bool, error) {
7680
return false, errors.New("unsupported platform")
7781
}

agent/config/parse_unsupported.go

+4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ func parseFSxWindowsFileServerCapability() BooleanDefaultFalse {
2929
return BooleanDefaultFalse{Value: ExplicitlyDisabled}
3030
}
3131

32+
func parseGMSADomainlessCapability() BooleanDefaultFalse {
33+
return BooleanDefaultFalse{Value: ExplicitlyDisabled}
34+
}
35+
3236
var IsWindows2016 = func() (bool, error) {
3337
return false, errors.New("unsupported platform")
3438
}

agent/config/parse_windows.go

+66
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"syscall"
2424
"unsafe"
2525

26+
"golang.org/x/sys/windows/registry"
27+
2628
"github.com/aws/amazon-ecs-agent/agent/utils"
2729
"github.com/cihub/seelog"
2830
)
@@ -32,6 +34,11 @@ const (
3234
// to skip the windows server version check. This is useful for testing and
3335
// should not be set for any non-test use-case.
3436
envSkipWindowsServerVersionCheck = "ZZZ_SKIP_WINDOWS_SERVER_VERSION_CHECK_NOT_SUPPORTED_IN_PRODUCTION"
37+
gmsaPluginGUID = "{859E1386-BDB4-49E8-85C7-3070B13920E1}"
38+
)
39+
40+
var (
41+
fnQueryDomainlessGmsaPluginSubKeys = queryDomainlessGmsaPluginSubKeys
3542
)
3643

3744
// parseGMSACapability is used to determine if gMSA support can be enabled
@@ -40,6 +47,26 @@ func parseGMSACapability() BooleanDefaultFalse {
4047
return checkDomainJoinWithEnvOverride(envStatus)
4148
}
4249

50+
// parseGMSADomainlessCapability is used to determine if gMSA domainless support can be enabled
51+
func parseGMSADomainlessCapability() BooleanDefaultFalse {
52+
envStatus := utils.ParseBool(os.Getenv("ECS_GMSA_SUPPORTED"), false)
53+
if envStatus {
54+
// gmsaDomainless is not supported on Windows 2016
55+
isWindows2016, err := IsWindows2016()
56+
if err != nil || isWindows2016 {
57+
return BooleanDefaultFalse{Value: ExplicitlyDisabled}
58+
}
59+
60+
// gmsaDomainless is not supported if the plugin is not installed on the instance
61+
installed, err := isDomainlessGmsaPluginInstalled()
62+
if err != nil || !installed {
63+
return BooleanDefaultFalse{Value: ExplicitlyDisabled}
64+
}
65+
return BooleanDefaultFalse{Value: ExplicitlyEnabled}
66+
}
67+
return BooleanDefaultFalse{Value: ExplicitlyDisabled}
68+
}
69+
4370
// parseFSxWindowsFileServerCapability is used to determine if fsxWindowsFileServer support can be enabled
4471
func parseFSxWindowsFileServerCapability() BooleanDefaultFalse {
4572
// fsxwindowsfileserver is not supported on Windows 2016 and non-domain-joined container instances
@@ -114,3 +141,42 @@ var IsWindows2016 = func() (bool, error) {
114141

115142
return isWS2016, nil
116143
}
144+
145+
var queryDomainlessGmsaPluginSubKeys = func() ([]string, error) {
146+
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SYSTEM\CurrentControlSet\Control\CCG\COMClasses\`, registry.READ)
147+
if err != nil {
148+
seelog.Errorf("Failed to open registry key SYSTEM\\CurrentControlSet\\Control\\CCG\\COMClasses with error: %v", err)
149+
return nil, err
150+
}
151+
defer k.Close()
152+
stat, err := k.Stat()
153+
if err != nil {
154+
seelog.Errorf("Failed to stat registry key SYSTEM\\CurrentControlSet\\Control\\CCG\\COMClasses with error: %v", err)
155+
return nil, err
156+
}
157+
subKeys, err := k.ReadSubKeyNames(int(stat.SubKeyCount))
158+
if err != nil {
159+
seelog.Errorf("Failed to read subkeys of SYSTEM\\CurrentControlSet\\Control\\CCG\\COMClasses with error: %v", err)
160+
return nil, err
161+
}
162+
163+
seelog.Debugf("gMSA Subkeys are %+v", subKeys)
164+
return subKeys, nil
165+
}
166+
167+
// This function queries all gmsa plugin subkeys to check whether the Amazon ECS Plugin GUID is present.
168+
func isDomainlessGmsaPluginInstalled() (bool, error) {
169+
subKeys, err := fnQueryDomainlessGmsaPluginSubKeys()
170+
if err != nil {
171+
seelog.Errorf("Failed to query gmsa plugin subkeys")
172+
return false, err
173+
}
174+
175+
for _, subKey := range subKeys {
176+
if subKey == gmsaPluginGUID {
177+
return true, nil
178+
}
179+
}
180+
181+
return false, nil
182+
}

agent/config/parse_windows_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,63 @@ func TestParseFSxWindowsFileServerCapability(t *testing.T) {
5151

5252
assert.False(t, parseFSxWindowsFileServerCapability().Enabled())
5353
}
54+
55+
func TestParseDomainlessgMSACapabilityFalseOnW2016(t *testing.T) {
56+
IsWindows2016 = func() (bool, error) {
57+
return true, nil
58+
}
59+
60+
os.Setenv("ECS_GMSA_SUPPORTED", "True")
61+
defer os.Unsetenv("ECS_GMSA_SUPPORTED")
62+
63+
fnQueryDomainlessGmsaPluginSubKeys = func() ([]string, error) {
64+
return []string{}, nil
65+
}
66+
67+
assert.False(t, parseGMSADomainlessCapability().Enabled())
68+
}
69+
70+
func TestParseDomainlessgMSACapabilityFalsePluginNoEnv(t *testing.T) {
71+
IsWindows2016 = func() (bool, error) {
72+
return false, nil
73+
}
74+
75+
os.Setenv("ECS_GMSA_SUPPORTED", "False")
76+
defer os.Unsetenv("ECS_GMSA_SUPPORTED")
77+
78+
fnQueryDomainlessGmsaPluginSubKeys = func() ([]string, error) {
79+
return []string{"{859E1386-BDB4-49E8-85C7-3070B13920E1}"}, nil
80+
}
81+
82+
assert.False(t, parseGMSADomainlessCapability().Enabled())
83+
}
84+
85+
func TestParseDomainlessgMSACapabilityFalseNoPlugin(t *testing.T) {
86+
IsWindows2016 = func() (bool, error) {
87+
return false, nil
88+
}
89+
90+
os.Setenv("ECS_GMSA_SUPPORTED", "True")
91+
defer os.Unsetenv("ECS_GMSA_SUPPORTED")
92+
93+
fnQueryDomainlessGmsaPluginSubKeys = func() ([]string, error) {
94+
return []string{}, nil
95+
}
96+
97+
assert.False(t, parseGMSADomainlessCapability().Enabled())
98+
}
99+
100+
func TestParseDomainlessgMSACapabilityTrue(t *testing.T) {
101+
IsWindows2016 = func() (bool, error) {
102+
return false, nil
103+
}
104+
105+
os.Setenv("ECS_GMSA_SUPPORTED", "True")
106+
defer os.Unsetenv("ECS_GMSA_SUPPORTED")
107+
108+
fnQueryDomainlessGmsaPluginSubKeys = func() ([]string, error) {
109+
return []string{"{859E1386-BDB4-49E8-85C7-3070B13920E1}"}, nil
110+
}
111+
112+
assert.True(t, parseGMSADomainlessCapability().Enabled())
113+
}

agent/config/types.go

+4
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,10 @@ type Config struct {
331331
// It should be enabled by default only if the container instance is part of a valid active directory domain.
332332
GMSACapable BooleanDefaultFalse
333333

334+
// GMSADomainlessCapable is the config option to indicate if gMSA domainless is supported.
335+
// It should be enabled by if the container instance has a plugin to support active directory authentication.
336+
GMSADomainlessCapable BooleanDefaultFalse
337+
334338
// VolumePluginCapabilities specifies the capabilities of the ecs volume plugin.
335339
VolumePluginCapabilities []string
336340

0 commit comments

Comments
 (0)