@@ -18,6 +18,7 @@ package credentialspec
18
18
19
19
import (
20
20
"crypto/sha256"
21
+ "encoding/json"
21
22
"fmt"
22
23
"os"
23
24
"path/filepath"
@@ -30,11 +31,14 @@ import (
30
31
s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory"
31
32
"github.com/aws/amazon-ecs-agent/agent/ssm"
32
33
ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory"
34
+ "github.com/aws/amazon-ecs-agent/agent/utils"
33
35
"github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper"
34
36
"github.com/aws/amazon-ecs-agent/agent/utils/oswrapper"
35
37
"github.com/aws/aws-sdk-go/aws/arn"
38
+
36
39
"github.com/cihub/seelog"
37
40
"github.com/pkg/errors"
41
+ "golang.org/x/sys/windows/registry"
38
42
)
39
43
40
44
const (
@@ -47,14 +51,38 @@ const (
47
51
// Environment variables to setup resource location
48
52
envProgramData = "ProgramData"
49
53
dockerCredentialSpecDataDir = "docker/credentialspecs"
54
+ ecsCcgPluginRegistryKeyRoot = `System\CurrentControlSet\Services\AmazonECSCCGPlugin`
55
+ regKeyPathFormat = `HKEY_LOCAL_MACHINE\` + ecsCcgPluginRegistryKeyRoot + `\%s`
56
+
57
+ credentialSpecParseErrorMsgTemplate = "Unable to parse %s from credential spec"
58
+ untypedMarshallErrorMsgTemplate = "Unable to marshal untyped object %s to type %s"
59
+ )
60
+
61
+ var (
62
+ // For ease of unit testing
63
+ osWriteFileImpl = os .WriteFile
64
+ osReadFileImpl = os .ReadFile
65
+ osRemoveImpl = os .Remove
66
+ readCredentialSpecImpl = readCredentialSpec
67
+ writeCredentialSpecImpl = writeCredentialSpec
68
+ readWriteDomainlessCredentialSpecImpl = readWriteDomainlessCredentialSpec
69
+ setTaskExecutionCredentialsRegKeysImpl = setTaskExecutionCredentialsRegKeys
70
+ handleNonFileDomainlessGMSACredSpecImpl = handleNonFileDomainlessGMSACredSpec
71
+ deleteTaskExecutionCredentialsRegKeysImpl = deleteTaskExecutionCredentialsRegKeys
50
72
)
51
73
74
+ type pluginInput struct {
75
+ CredentialArn string `json:"credentialArn,omitempty"`
76
+ RegKeyPath string `json:"regKeyPath,omitempty"`
77
+ }
78
+
52
79
// CredentialSpecResource is the abstraction for credentialspec resources
53
80
type CredentialSpecResource struct {
54
81
* CredentialSpecResourceCommon
55
82
ioutil ioutilwrapper.IOUtil
56
83
// credentialSpecResourceLocation is the location for all the tasks' credentialspec artifacts
57
84
credentialSpecResourceLocation string
85
+ isDomainlessGMSATask bool
58
86
}
59
87
60
88
// NewCredentialSpecResource creates a new CredentialSpecResource object
@@ -77,7 +105,8 @@ func NewCredentialSpecResource(taskARN, region string,
77
105
CredSpecMap : make (map [string ]string ),
78
106
credentialSpecContainerMap : credentialSpecContainerMap ,
79
107
},
80
- ioutil : ioutilwrapper .NewIOUtil (),
108
+ ioutil : ioutilwrapper .NewIOUtil (),
109
+ isDomainlessGMSATask : false ,
81
110
}
82
111
83
112
err := s .setCredentialSpecResourceLocation ()
@@ -100,11 +129,15 @@ func (cs *CredentialSpecResource) Create() error {
100
129
}
101
130
102
131
for credSpecStr := range cs .credentialSpecContainerMap {
103
- credSpecSplit := strings .SplitAfterN (credSpecStr , "credentialspec :" , 2 )
132
+ credSpecSplit := strings .SplitAfterN (credSpecStr , ":" , 2 )
104
133
if len (credSpecSplit ) != 2 {
105
134
seelog .Errorf ("Invalid credentialspec: %s" , credSpecStr )
106
135
continue
107
136
}
137
+ credSpecPrefix := credSpecSplit [0 ]
138
+ if credSpecPrefix == "credentialspecdomainless:" {
139
+ cs .isDomainlessGMSATask = true
140
+ }
108
141
credSpecValue := credSpecSplit [1 ]
109
142
110
143
if strings .HasPrefix (credSpecValue , "file://" ) {
@@ -145,22 +178,59 @@ func (cs *CredentialSpecResource) Create() error {
145
178
}
146
179
}
147
180
181
+ if cs .isDomainlessGMSATask {
182
+ // The domainless gMSA Windows Plugin needs the execution role credentials to pull customer secrets
183
+ err = setTaskExecutionCredentialsRegKeysImpl (iamCredentials , cs .CredentialSpecResourceCommon .taskARN )
184
+ if err != nil {
185
+ cs .setTerminalReason (err .Error ())
186
+ return err
187
+ }
188
+ }
189
+
148
190
return nil
149
191
}
150
192
151
193
func (cs * CredentialSpecResource ) handleCredentialspecFile (credentialspec string ) error {
152
- credSpecSplit := strings .SplitAfterN (credentialspec , "credentialspec :" , 2 )
194
+ credSpecSplit := strings .SplitAfterN (credentialspec , ":" , 2 )
153
195
if len (credSpecSplit ) != 2 {
154
196
seelog .Errorf ("Invalid credentialspec: %s" , credentialspec )
155
197
return errors .New ("invalid credentialspec file specification" )
156
198
}
199
+ credSpecPrefix := credSpecSplit [0 ]
157
200
credSpecFile := credSpecSplit [1 ]
158
201
159
202
if ! strings .HasPrefix (credSpecFile , "file://" ) {
160
203
return errors .New ("invalid credentialspec file specification" )
161
204
}
162
205
163
- dockerHostconfigSecOptCredSpec := strings .Replace (credentialspec , "credentialspec:" , "credentialspec=" , 1 )
206
+ if credSpecPrefix == "credentialspecdomainless:" {
207
+ relativeFilePath := strings .TrimPrefix (credSpecFile , "file://" )
208
+ dir , originalFileName := filepath .Split (relativeFilePath )
209
+
210
+ // Generate unique filename using taskId, containerName, credspecfile original name
211
+ taskId , err := utils .TaskIdFromArn (cs .taskARN )
212
+ if err != nil {
213
+ cs .setTerminalReason (err .Error ())
214
+ return err
215
+ }
216
+ containerName , ok := cs .credentialSpecContainerMap [credentialspec ]
217
+ if ! ok {
218
+ return errors .New (fmt .Sprintf ("Unable to retrieve containerName from credentialSpecContainerMap. No such key %s" , credentialspec ))
219
+ }
220
+
221
+ // We need a different outfile in order to avoid modifying the customers original credentialspec
222
+ outFile := fmt .Sprintf ("%s_%s_%s" , taskId , containerName , originalFileName )
223
+ credSpecFile = "file://" + filepath .Join (dir , outFile )
224
+
225
+ // Fill in appropriate domainless gMSA fields
226
+ err = readWriteDomainlessCredentialSpecImpl (filepath .Join (cs .credentialSpecResourceLocation , dir , originalFileName ), filepath .Join (cs .credentialSpecResourceLocation , dir , outFile ), cs .taskARN )
227
+ if err != nil {
228
+ cs .setTerminalReason (err .Error ())
229
+ return err
230
+ }
231
+ }
232
+
233
+ dockerHostconfigSecOptCredSpec := "credentialspec=" + credSpecFile
164
234
cs .updateCredSpecMapping (credentialspec , dockerHostconfigSecOptCredSpec )
165
235
166
236
return nil
@@ -207,6 +277,12 @@ func (cs *CredentialSpecResource) handleS3CredentialspecFile(originalCredentials
207
277
return err
208
278
}
209
279
280
+ err = handleNonFileDomainlessGMSACredSpecImpl (originalCredentialspec , localCredSpecFilePath , cs .taskARN )
281
+ if err != nil {
282
+ cs .setTerminalReason (err .Error ())
283
+ return err
284
+ }
285
+
210
286
dockerHostconfigSecOptCredSpec := fmt .Sprintf ("credentialspec=file://%s" , filepath .Base (localCredSpecFilePath ))
211
287
cs .updateCredSpecMapping (originalCredentialspec , dockerHostconfigSecOptCredSpec )
212
288
@@ -269,6 +345,13 @@ func (cs *CredentialSpecResource) handleSSMCredentialspecFile(originalCredential
269
345
cs .setTerminalReason (err .Error ())
270
346
return err
271
347
}
348
+
349
+ err = handleNonFileDomainlessGMSACredSpecImpl (originalCredentialspec , localCredSpecFilePath , cs .taskARN )
350
+ if err != nil {
351
+ cs .setTerminalReason (err .Error ())
352
+ return err
353
+ }
354
+
272
355
dockerHostconfigSecOptCredSpec := fmt .Sprintf ("credentialspec=file://%s" , customCredSpecFileName )
273
356
cs .updateCredSpecMapping (originalCredentialspec , dockerHostconfigSecOptCredSpec )
274
357
@@ -317,11 +400,15 @@ func (cs *CredentialSpecResource) updateCredSpecMapping(credSpecInput, targetCre
317
400
// Cleanup removes the credentialspec created for the task
318
401
func (cs * CredentialSpecResource ) Cleanup () error {
319
402
cs .clearCredentialSpec ()
403
+ if cs .isDomainlessGMSATask {
404
+ err := cs .deleteTaskExecutionCredentialsRegKeys ()
405
+ if err != nil {
406
+ return err
407
+ }
408
+ }
320
409
return nil
321
410
}
322
411
323
- var remove = os .Remove
324
-
325
412
// clearCredentialSpec cycles through the collection of credentialspec data and
326
413
// removes them from the task
327
414
func (cs * CredentialSpecResource ) clearCredentialSpec () {
@@ -334,14 +421,14 @@ func (cs *CredentialSpecResource) clearCredentialSpec() {
334
421
continue
335
422
}
336
423
// Split credentialspec to obtain local file-name
337
- credSpecSplit := strings .SplitAfterN (value , "credentialspec= file://" , 2 )
424
+ credSpecSplit := strings .SplitAfterN (value , "file://" , 2 )
338
425
if len (credSpecSplit ) != 2 {
339
426
seelog .Warnf ("Unable to parse target credentialspec: %s" , value )
340
427
continue
341
428
}
342
429
localCredentialSpecFile := credSpecSplit [1 ]
343
430
localCredentialSpecFilePath := filepath .Join (cs .credentialSpecResourceLocation , localCredentialSpecFile )
344
- err := remove (localCredentialSpecFilePath )
431
+ err := osRemoveImpl (localCredentialSpecFilePath )
345
432
if err != nil {
346
433
seelog .Warnf ("Unable to clear local credential spec file %s for task %s" , localCredentialSpecFile , cs .taskARN )
347
434
}
@@ -350,6 +437,33 @@ func (cs *CredentialSpecResource) clearCredentialSpec() {
350
437
}
351
438
}
352
439
440
+ func (cs * CredentialSpecResource ) deleteTaskExecutionCredentialsRegKeys () error {
441
+ cs .lock .Lock ()
442
+ defer cs .lock .Unlock ()
443
+
444
+ return deleteTaskExecutionCredentialsRegKeysImpl (cs .taskARN )
445
+ }
446
+
447
+ // deleteTaskExecutionCredentialsRegKeys deletes the taskExecutionRole IAM credentials in the task registry key
448
+ // after the task has been terminated.
449
+ func deleteTaskExecutionCredentialsRegKeys (taskARN string ) error {
450
+ k , err := registry .OpenKey (registry .LOCAL_MACHINE , ecsCcgPluginRegistryKeyRoot , registry .ALL_ACCESS )
451
+ if err != nil {
452
+ // Early exit with success case, if the registry key doesn't exist then there are no task execution role creds to cleanup
453
+ seelog .Errorf ("Error opening %s key: %s" , ecsCcgPluginRegistryKeyRoot , err )
454
+ return nil
455
+ }
456
+ defer k .Close ()
457
+
458
+ err = registry .DeleteKey (k , taskARN )
459
+ if err != nil {
460
+ seelog .Errorf ("Error deleting %s key: %s" , ecsCcgPluginRegistryKeyRoot + "\\ " + taskARN , err )
461
+ return err
462
+ }
463
+ seelog .Infof ("Deleted Task Execution Credential Registry key for task: %s" , taskARN )
464
+ return nil
465
+ }
466
+
353
467
func (cs * CredentialSpecResource ) setCredentialSpecResourceLocation () error {
354
468
// TODO: Use registry to setup credentialspec resource location
355
469
// This should always be available on Windows instances
@@ -378,3 +492,148 @@ func (cs *CredentialSpecResource) MarshallPlatformSpecificFields(credentialSpecR
378
492
func (cs * CredentialSpecResource ) UnmarshallPlatformSpecificFields (credentialSpecResourceJSON CredentialSpecResourceJSON ) {
379
493
return
380
494
}
495
+
496
+ // setTaskExecutionCredentialsRegKeys stores the taskExecutionRole IAM credentials to the task registry key
497
+ // so that the domainless gMSA plugin may use these credentials to access the customer Active Directory authentication
498
+ // information.
499
+ func setTaskExecutionCredentialsRegKeys (taskCredentials credentials.IAMRoleCredentials , taskArn string ) error {
500
+ if taskCredentials == (credentials.IAMRoleCredentials {}) {
501
+ err := errors .New ("Unable to find execution role credentials while setting registry key for task " + taskArn )
502
+ return err
503
+ }
504
+
505
+ taskRegistryKey , _ , err := registry .CreateKey (registry .LOCAL_MACHINE , ecsCcgPluginRegistryKeyRoot + "\\ " + taskArn , registry .WRITE )
506
+ if err != nil {
507
+ errMsg := fmt .Sprintf ("Error creating registry key root %s for task %s: %s" , ecsCcgPluginRegistryKeyRoot , taskArn , err )
508
+ seelog .Errorf (errMsg )
509
+ return errors .Wrapf (err , errMsg )
510
+ }
511
+ defer taskRegistryKey .Close ()
512
+
513
+ err = taskRegistryKey .SetStringValue ("AKID" , taskCredentials .AccessKeyID )
514
+ if err != nil {
515
+ errMsg := fmt .Sprintf ("Error creating AKID child value for task %s:%s" , taskArn , err )
516
+ seelog .Errorf (errMsg )
517
+ return errors .Wrapf (err , errMsg )
518
+ }
519
+ err = taskRegistryKey .SetStringValue ("SKID" , taskCredentials .SecretAccessKey )
520
+ if err != nil {
521
+ errMsg := fmt .Sprintf ("Error creating AKID child value for task %s:%s" , taskArn , err )
522
+ seelog .Errorf (errMsg )
523
+ return errors .Wrapf (err , errMsg )
524
+ }
525
+ err = taskRegistryKey .SetStringValue ("SESSIONTOKEN" , taskCredentials .SessionToken )
526
+ if err != nil {
527
+ errMsg := fmt .Sprintf ("Error creating SESSIONTOKEN child value for task %s:%s" , taskArn , err )
528
+ seelog .Errorf (errMsg )
529
+ return errors .Wrapf (err , errMsg )
530
+ }
531
+
532
+ return nil
533
+ }
534
+
535
+ // handleNonFileDomainlessGMSACredSpec reads and then injects the taskExecutionRoleRegistryKey location for
536
+ // the s3/ssm gMSA credential spec cases.
537
+ func handleNonFileDomainlessGMSACredSpec (originalCredSpec , localCredSpecFilePath , taskARN string ) error {
538
+ // Exit early for non domainless gMSA cred specs
539
+ if ! strings .HasPrefix (originalCredSpec , "credentialspecdomainless:" ) {
540
+ return nil
541
+ }
542
+
543
+ err := readWriteDomainlessCredentialSpecImpl (localCredSpecFilePath , localCredSpecFilePath , taskARN )
544
+ if err != nil {
545
+ return err
546
+ }
547
+ return nil
548
+ }
549
+
550
+ // readWriteDomainlessCredentialSpec is used to open the credential spec file on local disk, inject the
551
+ // taskExecutionRoleInformation in memory, and then write the file to a specific path. The reason we do not
552
+ // modify the same fail is to avoid modifying the customer resource when the customer provides a local file
553
+ // credential spec
554
+ func readWriteDomainlessCredentialSpec (filePath , outFilePath , taskARN string ) error {
555
+ credSpec , err := readCredentialSpecImpl (filePath )
556
+ if err != nil {
557
+ return err
558
+ }
559
+ err = writeCredentialSpecImpl (credSpec , outFilePath , taskARN )
560
+ if err != nil {
561
+ return err
562
+ }
563
+ return nil
564
+ }
565
+
566
+ // readCredentialSpec is used to open the credential spec file on local disk and read it into a generic
567
+ // bytes map object map[string]interface{}
568
+ func readCredentialSpec (filePath string ) (map [string ]interface {}, error ) {
569
+ byteResult , err := osReadFileImpl (filePath )
570
+ if err != nil {
571
+ return nil , err
572
+ }
573
+ var credSpec map [string ]interface {}
574
+ err = json .Unmarshal (byteResult , & credSpec )
575
+ if err != nil {
576
+ return nil , err
577
+ }
578
+ return credSpec , nil
579
+ }
580
+
581
+ // writeCredentialSpec is used to selectively decode portions of the Microsoft gMSA generated credential spec file and then
582
+ // inject the taskExecutionRoleRegistryKey location so that the gMSA plugin is able to access these IAM credentials.
583
+ // The reason that the JSON unmarshalling is manual is to protect against future key/value pairs that appear in the JSON,
584
+ // while only modifying the portions that pertain to domainless gMSA. This is in case Microsoft adds additional keys to the
585
+ // JSON credential spec, so that our writer does not ignore this data.
586
+ func writeCredentialSpec (credSpec map [string ]interface {}, outFilePath string , taskARN string ) error {
587
+ activeDirectoryConfigUntyped , ok := credSpec ["ActiveDirectoryConfig" ]
588
+ if ! ok {
589
+ return errors .New (fmt .Sprintf (credentialSpecParseErrorMsgTemplate , "ActiveDirectoryConfig" ))
590
+ }
591
+ activeDirectoryConfig , ok := activeDirectoryConfigUntyped .(map [string ]interface {})
592
+ if ! ok {
593
+ return errors .New (fmt .Sprintf (untypedMarshallErrorMsgTemplate , "activeDirectoryConfigUntyped" , "map[string]interface{}" ))
594
+ }
595
+
596
+ hostAccountConfigUntyped , ok := activeDirectoryConfig ["HostAccountConfig" ]
597
+ if ! ok {
598
+ return errors .New (fmt .Sprintf (credentialSpecParseErrorMsgTemplate , "HostAccountConfig" ))
599
+ }
600
+ hostAccountConfig , ok := hostAccountConfigUntyped .(map [string ]interface {})
601
+ if ! ok {
602
+ return errors .New (fmt .Sprintf (untypedMarshallErrorMsgTemplate , "hostAccountConfigUntyped" , "map[string]interface{}" ))
603
+ }
604
+
605
+ pluginInputStringUntyped , ok := hostAccountConfig ["PluginInput" ]
606
+ if ! ok {
607
+ return errors .New (fmt .Sprintf (credentialSpecParseErrorMsgTemplate , "PluginInput" ))
608
+ }
609
+ var pluginInputParsed pluginInput
610
+ pluginInputString , ok := pluginInputStringUntyped .(string )
611
+ if ! ok {
612
+ return errors .New (fmt .Sprintf (untypedMarshallErrorMsgTemplate , "pluginInputStringUntyped" , "string" ))
613
+ }
614
+ err := json .Unmarshal ([]byte (pluginInputString ), & pluginInputParsed )
615
+ if err != nil {
616
+ return err
617
+ }
618
+
619
+ pluginInputParsed .RegKeyPath = fmt .Sprintf (regKeyPathFormat , taskARN )
620
+
621
+ pluginInputBytes , err := json .Marshal (pluginInputParsed )
622
+ if err != nil {
623
+ return err
624
+ }
625
+
626
+ hostAccountConfig ["PluginInput" ] = string (pluginInputBytes )
627
+
628
+ jsonBytes , err := json .Marshal (credSpec )
629
+ if err != nil {
630
+ return err
631
+ }
632
+
633
+ err = osWriteFileImpl (outFilePath , jsonBytes , filePerm )
634
+ if err != nil {
635
+ return err
636
+ }
637
+
638
+ return nil
639
+ }
0 commit comments