Skip to content

Commit 87e5301

Browse files
Inject domainless gmsa cred spec into Windows Container
1 parent e4e9eed commit 87e5301

File tree

2 files changed

+1155
-272
lines changed

2 files changed

+1155
-272
lines changed

agent/taskresource/credentialspec/credentialspec_windows.go

+240-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package credentialspec
1818

1919
import (
2020
"crypto/sha256"
21+
"encoding/json"
2122
"fmt"
2223
"os"
2324
"path/filepath"
@@ -29,11 +30,14 @@ import (
2930
s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory"
3031
"github.com/aws/amazon-ecs-agent/agent/ssm"
3132
ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory"
33+
"github.com/aws/amazon-ecs-agent/agent/utils"
3234
"github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper"
3335
"github.com/aws/amazon-ecs-agent/agent/utils/oswrapper"
3436
"github.com/aws/aws-sdk-go/aws/arn"
37+
3538
"github.com/cihub/seelog"
3639
"github.com/pkg/errors"
40+
"golang.org/x/sys/windows/registry"
3741
)
3842

3943
const (
@@ -46,14 +50,38 @@ const (
4650
// Environment variables to setup resource location
4751
envProgramData = "ProgramData"
4852
dockerCredentialSpecDataDir = "docker/credentialspecs"
53+
ecsCcgPluginRegistryKeyRoot = `System\CurrentControlSet\Services\AmazonECSCCGPlugin`
54+
regKeyPathFormat = `HKEY_LOCAL_MACHINE\` + ecsCcgPluginRegistryKeyRoot + `\%s`
55+
56+
credentialSpecParseErrorMsgTemplate = "Unable to parse %s from credential spec"
57+
untypedMarshallErrorMsgTemplate = "Unable to marshal untyped object %s to type %s"
4958
)
5059

60+
var (
61+
// For ease of unit testing
62+
osWriteFileImpl = os.WriteFile
63+
osReadFileImpl = os.ReadFile
64+
osRemoveImpl = os.Remove
65+
readCredentialSpecImpl = readCredentialSpec
66+
writeCredentialSpecImpl = writeCredentialSpec
67+
readWriteDomainlessCredentialSpecImpl = readWriteDomainlessCredentialSpec
68+
setTaskExecutionCredentialsRegKeysImpl = setTaskExecutionCredentialsRegKeys
69+
handleNonFileDomainlessGMSACredSpecImpl = handleNonFileDomainlessGMSACredSpec
70+
deleteTaskExecutionCredentialsRegKeysImpl = deleteTaskExecutionCredentialsRegKeys
71+
)
72+
73+
type PluginInput struct {
74+
CredentialArn string `json:"credentialArn,omitempty"`
75+
RegKeyPath string `json:"regKeyPath,omitempty"`
76+
}
77+
5178
// CredentialSpecResource is the abstraction for credentialspec resources
5279
type CredentialSpecResource struct {
5380
*CredentialSpecResourceCommon
5481
ioutil ioutilwrapper.IOUtil
5582
// credentialSpecResourceLocation is the location for all the tasks' credentialspec artifacts
5683
credentialSpecResourceLocation string
84+
domainlessGMSATask bool
5785
}
5886

5987
// NewCredentialSpecResource creates a new CredentialSpecResource object
@@ -75,7 +103,8 @@ func NewCredentialSpecResource(taskARN, region string,
75103
CredSpecMap: make(map[string]string),
76104
credentialSpecContainerMap: credentialSpecContainerMap,
77105
},
78-
ioutil: ioutilwrapper.NewIOUtil(),
106+
ioutil: ioutilwrapper.NewIOUtil(),
107+
domainlessGMSATask: false,
79108
}
80109

81110
err := s.setCredentialSpecResourceLocation()
@@ -98,11 +127,15 @@ func (cs *CredentialSpecResource) Create() error {
98127
}
99128

100129
for credSpecStr := range cs.credentialSpecContainerMap {
101-
credSpecSplit := strings.SplitAfterN(credSpecStr, "credentialspec:", 2)
130+
credSpecSplit := strings.SplitAfterN(credSpecStr, ":", 2)
102131
if len(credSpecSplit) != 2 {
103132
seelog.Errorf("Invalid credentialspec: %s", credSpecStr)
104133
continue
105134
}
135+
credSpecPrefix := credSpecSplit[0]
136+
if credSpecPrefix == "credentialspecdomainless:" {
137+
cs.domainlessGMSATask = true
138+
}
106139
credSpecValue := credSpecSplit[1]
107140

108141
if strings.HasPrefix(credSpecValue, "file://") {
@@ -143,22 +176,55 @@ func (cs *CredentialSpecResource) Create() error {
143176
}
144177
}
145178

179+
if cs.domainlessGMSATask {
180+
// The domainless gMSA Windows Plugin needs the execution role credentials to pull customer secrets
181+
err = setTaskExecutionCredentialsRegKeysImpl(iamCredentials, cs.CredentialSpecResourceCommon.taskARN)
182+
if err != nil {
183+
cs.setTerminalReason(err.Error())
184+
return err
185+
}
186+
}
187+
146188
return nil
147189
}
148190

149191
func (cs *CredentialSpecResource) handleCredentialspecFile(credentialspec string) error {
150-
credSpecSplit := strings.SplitAfterN(credentialspec, "credentialspec:", 2)
192+
credSpecSplit := strings.SplitAfterN(credentialspec, ":", 2)
151193
if len(credSpecSplit) != 2 {
152194
seelog.Errorf("Invalid credentialspec: %s", credentialspec)
153195
return errors.New("invalid credentialspec file specification")
154196
}
197+
credSpecPrefix := credSpecSplit[0]
155198
credSpecFile := credSpecSplit[1]
156199

157200
if !strings.HasPrefix(credSpecFile, "file://") {
158201
return errors.New("invalid credentialspec file specification")
159202
}
160203

161-
dockerHostconfigSecOptCredSpec := strings.Replace(credentialspec, "credentialspec:", "credentialspec=", 1)
204+
if credSpecPrefix == "credentialspecdomainless:" {
205+
relativeFilePath := strings.TrimPrefix(credSpecFile, "file://")
206+
dir, originalFileName := filepath.Split(relativeFilePath)
207+
208+
// Generate unique filename using taskId, containerName, credspecfile original name
209+
taskId, err := utils.TaskIdFromArn(cs.taskARN)
210+
if err != nil {
211+
cs.setTerminalReason(err.Error())
212+
return err
213+
}
214+
containerName := cs.credentialSpecContainerMap[credentialspec]
215+
// We need a different outfile in order to avoid modifying the customers original credentialspec
216+
outFile := fmt.Sprintf("%s_%s_%s", taskId, containerName, originalFileName)
217+
credSpecFile = "file://" + filepath.Join(dir, outFile)
218+
219+
// Fill in appropriate domainless gMSA fields
220+
err = readWriteDomainlessCredentialSpecImpl(filepath.Join(cs.credentialSpecResourceLocation, dir, originalFileName), filepath.Join(cs.credentialSpecResourceLocation, dir, outFile), cs.taskARN)
221+
if err != nil {
222+
cs.setTerminalReason(err.Error())
223+
return err
224+
}
225+
}
226+
227+
dockerHostconfigSecOptCredSpec := "credentialspec=" + credSpecFile
162228
cs.updateCredSpecMapping(credentialspec, dockerHostconfigSecOptCredSpec)
163229

164230
return nil
@@ -205,6 +271,12 @@ func (cs *CredentialSpecResource) handleS3CredentialspecFile(originalCredentials
205271
return err
206272
}
207273

274+
err = handleNonFileDomainlessGMSACredSpecImpl(originalCredentialspec, localCredSpecFilePath, cs.taskARN)
275+
if err != nil {
276+
cs.setTerminalReason(err.Error())
277+
return err
278+
}
279+
208280
dockerHostconfigSecOptCredSpec := fmt.Sprintf("credentialspec=file://%s", filepath.Base(localCredSpecFilePath))
209281
cs.updateCredSpecMapping(originalCredentialspec, dockerHostconfigSecOptCredSpec)
210282

@@ -267,6 +339,13 @@ func (cs *CredentialSpecResource) handleSSMCredentialspecFile(originalCredential
267339
cs.setTerminalReason(err.Error())
268340
return err
269341
}
342+
343+
err = handleNonFileDomainlessGMSACredSpecImpl(originalCredentialspec, localCredSpecFilePath, cs.taskARN)
344+
if err != nil {
345+
cs.setTerminalReason(err.Error())
346+
return err
347+
}
348+
270349
dockerHostconfigSecOptCredSpec := fmt.Sprintf("credentialspec=file://%s", customCredSpecFileName)
271350
cs.updateCredSpecMapping(originalCredentialspec, dockerHostconfigSecOptCredSpec)
272351

@@ -315,11 +394,15 @@ func (cs *CredentialSpecResource) updateCredSpecMapping(credSpecInput, targetCre
315394
// Cleanup removes the credentialspec created for the task
316395
func (cs *CredentialSpecResource) Cleanup() error {
317396
cs.clearCredentialSpec()
397+
if cs.domainlessGMSATask {
398+
err := cs.deleteTaskExecutionCredentialsRegKeys()
399+
if err != nil {
400+
return err
401+
}
402+
}
318403
return nil
319404
}
320405

321-
var remove = os.Remove
322-
323406
// clearCredentialSpec cycles through the collection of credentialspec data and
324407
// removes them from the task
325408
func (cs *CredentialSpecResource) clearCredentialSpec() {
@@ -332,14 +415,14 @@ func (cs *CredentialSpecResource) clearCredentialSpec() {
332415
continue
333416
}
334417
// Split credentialspec to obtain local file-name
335-
credSpecSplit := strings.SplitAfterN(value, "credentialspec=file://", 2)
418+
credSpecSplit := strings.SplitAfterN(value, "file://", 2)
336419
if len(credSpecSplit) != 2 {
337420
seelog.Warnf("Unable to parse target credentialspec: %s", value)
338421
continue
339422
}
340423
localCredentialSpecFile := credSpecSplit[1]
341424
localCredentialSpecFilePath := filepath.Join(cs.credentialSpecResourceLocation, localCredentialSpecFile)
342-
err := remove(localCredentialSpecFilePath)
425+
err := osRemoveImpl(localCredentialSpecFilePath)
343426
if err != nil {
344427
seelog.Warnf("Unable to clear local credential spec file %s for task %s", localCredentialSpecFile, cs.taskARN)
345428
}
@@ -348,6 +431,30 @@ func (cs *CredentialSpecResource) clearCredentialSpec() {
348431
}
349432
}
350433

434+
func (cs *CredentialSpecResource) deleteTaskExecutionCredentialsRegKeys() error {
435+
cs.lock.Lock()
436+
defer cs.lock.Unlock()
437+
438+
return deleteTaskExecutionCredentialsRegKeysImpl(cs.taskARN)
439+
}
440+
441+
func deleteTaskExecutionCredentialsRegKeys(taskARN string) error {
442+
k, err := registry.OpenKey(registry.LOCAL_MACHINE, ecsCcgPluginRegistryKeyRoot, registry.ALL_ACCESS)
443+
if err != nil {
444+
// Early exit with success case, if the registry key doesn't exist then there are no task execution role creds to cleanup
445+
return nil
446+
}
447+
defer k.Close()
448+
449+
err = registry.DeleteKey(k, taskARN)
450+
if err != nil {
451+
seelog.Errorf("Error deleting %s key: %s", ecsCcgPluginRegistryKeyRoot+"\\"+taskARN, err)
452+
return err
453+
}
454+
seelog.Infof("Deleted Task Execution Credential Registry key for task: %s", taskARN)
455+
return nil
456+
}
457+
351458
func (cs *CredentialSpecResource) setCredentialSpecResourceLocation() error {
352459
// TODO: Use registry to setup credentialspec resource location
353460
// This should always be available on Windows instances
@@ -376,3 +483,128 @@ func (cs *CredentialSpecResource) MarshallPlatformSpecificFields(credentialSpecR
376483
func (cs *CredentialSpecResource) UnmarshallPlatformSpecificFields(credentialSpecResourceJSON CredentialSpecResourceJSON) {
377484
return
378485
}
486+
487+
func setTaskExecutionCredentialsRegKeys(taskCredentials credentials.IAMRoleCredentials, taskArn string) error {
488+
if taskCredentials == (credentials.IAMRoleCredentials{}) {
489+
err := errors.New("Unable to find execution role credentials while setting registry key for task " + taskArn)
490+
return err
491+
}
492+
493+
taskRegistryKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, ecsCcgPluginRegistryKeyRoot+"\\"+taskArn, registry.WRITE)
494+
if err != nil {
495+
seelog.Errorf("Error creating registry key root %s for task %s: %s", ecsCcgPluginRegistryKeyRoot, taskArn, err)
496+
return err
497+
}
498+
defer taskRegistryKey.Close()
499+
500+
err = taskRegistryKey.SetStringValue("AKID", taskCredentials.AccessKeyID)
501+
if err != nil {
502+
seelog.Errorf("Error creating AKID child value for task %s:%s", taskArn, err)
503+
return err
504+
}
505+
err = taskRegistryKey.SetStringValue("SKID", taskCredentials.SecretAccessKey)
506+
if err != nil {
507+
seelog.Errorf("Error creating SKID child value for task %s:%s", taskArn, err)
508+
return err
509+
}
510+
err = taskRegistryKey.SetStringValue("SESSIONTOKEN", taskCredentials.SessionToken)
511+
if err != nil {
512+
seelog.Errorf("Error creating SESSIONTOKEN child value for task %s:%s", taskArn, err)
513+
return err
514+
}
515+
516+
return nil
517+
}
518+
519+
func handleNonFileDomainlessGMSACredSpec(originalCredSpec, localCredSpecFilePath, taskARN string) error {
520+
// Exit early for non domainless gMSA cred specs
521+
if !strings.HasPrefix(originalCredSpec, "credentialspecdomainless:") {
522+
return nil
523+
}
524+
525+
err := readWriteDomainlessCredentialSpecImpl(localCredSpecFilePath, localCredSpecFilePath, taskARN)
526+
if err != nil {
527+
return err
528+
}
529+
return nil
530+
}
531+
532+
func readWriteDomainlessCredentialSpec(filePath, outFilePath, taskARN string) error {
533+
credSpec, err := readCredentialSpecImpl(filePath)
534+
if err != nil {
535+
return err
536+
}
537+
err = writeCredentialSpecImpl(credSpec, outFilePath, taskARN)
538+
if err != nil {
539+
return err
540+
}
541+
return nil
542+
}
543+
544+
func readCredentialSpec(filePath string) (map[string]interface{}, error) {
545+
byteResult, err := osReadFileImpl(filePath)
546+
if err != nil {
547+
return nil, err
548+
}
549+
var credSpec map[string]interface{}
550+
err = json.Unmarshal(byteResult, &credSpec)
551+
if err != nil {
552+
return nil, err
553+
}
554+
return credSpec, nil
555+
}
556+
557+
func writeCredentialSpec(credSpec map[string]interface{}, outFilePath string, taskARN string) error {
558+
activeDirectoryConfigUntyped, ok := credSpec["ActiveDirectoryConfig"]
559+
if !ok {
560+
return errors.New(fmt.Sprintf(credentialSpecParseErrorMsgTemplate, "ActiveDirectoryConfig"))
561+
}
562+
activeDirectoryConfig, ok := activeDirectoryConfigUntyped.(map[string]interface{})
563+
if !ok {
564+
return errors.New(fmt.Sprintf(untypedMarshallErrorMsgTemplate, "activeDirectoryConfigUntyped", "map[string]interface{}"))
565+
}
566+
567+
hostAccountConfigUntyped, ok := activeDirectoryConfig["HostAccountConfig"]
568+
if !ok {
569+
return errors.New(fmt.Sprintf(credentialSpecParseErrorMsgTemplate, "HostAccountConfig"))
570+
}
571+
hostAccountConfig, ok := hostAccountConfigUntyped.(map[string]interface{})
572+
if !ok {
573+
return errors.New(fmt.Sprintf(untypedMarshallErrorMsgTemplate, "hostAccountConfigUntyped", "map[string]interface{}"))
574+
}
575+
576+
pluginInputStringUntyped, ok := hostAccountConfig["PluginInput"]
577+
if !ok {
578+
return errors.New(fmt.Sprintf(credentialSpecParseErrorMsgTemplate, "PluginInput"))
579+
}
580+
var pluginInputParsed PluginInput
581+
pluginInputString, ok := pluginInputStringUntyped.(string)
582+
if !ok {
583+
return errors.New(fmt.Sprintf(untypedMarshallErrorMsgTemplate, "pluginInputStringUntyped", "string"))
584+
}
585+
err := json.Unmarshal([]byte(pluginInputString), &pluginInputParsed)
586+
if err != nil {
587+
return err
588+
}
589+
590+
pluginInputParsed.RegKeyPath = fmt.Sprintf(regKeyPathFormat, taskARN)
591+
592+
pluginInputBytes, err := json.Marshal(pluginInputParsed)
593+
if err != nil {
594+
return err
595+
}
596+
597+
hostAccountConfig["PluginInput"] = string(pluginInputBytes)
598+
599+
jsonBytes, err := json.Marshal(credSpec)
600+
if err != nil {
601+
return err
602+
}
603+
604+
err = osWriteFileImpl(outFilePath, jsonBytes, filePerm)
605+
if err != nil {
606+
return err
607+
}
608+
609+
return nil
610+
}

0 commit comments

Comments
 (0)