@@ -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"
@@ -29,11 +30,14 @@ import (
29
30
s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory"
30
31
"github.com/aws/amazon-ecs-agent/agent/ssm"
31
32
ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory"
33
+ "github.com/aws/amazon-ecs-agent/agent/utils"
32
34
"github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper"
33
35
"github.com/aws/amazon-ecs-agent/agent/utils/oswrapper"
34
36
"github.com/aws/aws-sdk-go/aws/arn"
37
+
35
38
"github.com/cihub/seelog"
36
39
"github.com/pkg/errors"
40
+ "golang.org/x/sys/windows/registry"
37
41
)
38
42
39
43
const (
@@ -46,14 +50,38 @@ const (
46
50
// Environment variables to setup resource location
47
51
envProgramData = "ProgramData"
48
52
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"
49
58
)
50
59
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
+
51
78
// CredentialSpecResource is the abstraction for credentialspec resources
52
79
type CredentialSpecResource struct {
53
80
* CredentialSpecResourceCommon
54
81
ioutil ioutilwrapper.IOUtil
55
82
// credentialSpecResourceLocation is the location for all the tasks' credentialspec artifacts
56
83
credentialSpecResourceLocation string
84
+ domainlessGMSATask bool
57
85
}
58
86
59
87
// NewCredentialSpecResource creates a new CredentialSpecResource object
@@ -75,7 +103,8 @@ func NewCredentialSpecResource(taskARN, region string,
75
103
CredSpecMap : make (map [string ]string ),
76
104
credentialSpecContainerMap : credentialSpecContainerMap ,
77
105
},
78
- ioutil : ioutilwrapper .NewIOUtil (),
106
+ ioutil : ioutilwrapper .NewIOUtil (),
107
+ domainlessGMSATask : false ,
79
108
}
80
109
81
110
err := s .setCredentialSpecResourceLocation ()
@@ -98,11 +127,15 @@ func (cs *CredentialSpecResource) Create() error {
98
127
}
99
128
100
129
for credSpecStr := range cs .credentialSpecContainerMap {
101
- credSpecSplit := strings .SplitAfterN (credSpecStr , "credentialspec :" , 2 )
130
+ credSpecSplit := strings .SplitAfterN (credSpecStr , ":" , 2 )
102
131
if len (credSpecSplit ) != 2 {
103
132
seelog .Errorf ("Invalid credentialspec: %s" , credSpecStr )
104
133
continue
105
134
}
135
+ credSpecPrefix := credSpecSplit [0 ]
136
+ if credSpecPrefix == "credentialspecdomainless:" {
137
+ cs .domainlessGMSATask = true
138
+ }
106
139
credSpecValue := credSpecSplit [1 ]
107
140
108
141
if strings .HasPrefix (credSpecValue , "file://" ) {
@@ -143,22 +176,55 @@ func (cs *CredentialSpecResource) Create() error {
143
176
}
144
177
}
145
178
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
+
146
188
return nil
147
189
}
148
190
149
191
func (cs * CredentialSpecResource ) handleCredentialspecFile (credentialspec string ) error {
150
- credSpecSplit := strings .SplitAfterN (credentialspec , "credentialspec :" , 2 )
192
+ credSpecSplit := strings .SplitAfterN (credentialspec , ":" , 2 )
151
193
if len (credSpecSplit ) != 2 {
152
194
seelog .Errorf ("Invalid credentialspec: %s" , credentialspec )
153
195
return errors .New ("invalid credentialspec file specification" )
154
196
}
197
+ credSpecPrefix := credSpecSplit [0 ]
155
198
credSpecFile := credSpecSplit [1 ]
156
199
157
200
if ! strings .HasPrefix (credSpecFile , "file://" ) {
158
201
return errors .New ("invalid credentialspec file specification" )
159
202
}
160
203
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
162
228
cs .updateCredSpecMapping (credentialspec , dockerHostconfigSecOptCredSpec )
163
229
164
230
return nil
@@ -205,6 +271,12 @@ func (cs *CredentialSpecResource) handleS3CredentialspecFile(originalCredentials
205
271
return err
206
272
}
207
273
274
+ err = handleNonFileDomainlessGMSACredSpecImpl (originalCredentialspec , localCredSpecFilePath , cs .taskARN )
275
+ if err != nil {
276
+ cs .setTerminalReason (err .Error ())
277
+ return err
278
+ }
279
+
208
280
dockerHostconfigSecOptCredSpec := fmt .Sprintf ("credentialspec=file://%s" , filepath .Base (localCredSpecFilePath ))
209
281
cs .updateCredSpecMapping (originalCredentialspec , dockerHostconfigSecOptCredSpec )
210
282
@@ -267,6 +339,13 @@ func (cs *CredentialSpecResource) handleSSMCredentialspecFile(originalCredential
267
339
cs .setTerminalReason (err .Error ())
268
340
return err
269
341
}
342
+
343
+ err = handleNonFileDomainlessGMSACredSpecImpl (originalCredentialspec , localCredSpecFilePath , cs .taskARN )
344
+ if err != nil {
345
+ cs .setTerminalReason (err .Error ())
346
+ return err
347
+ }
348
+
270
349
dockerHostconfigSecOptCredSpec := fmt .Sprintf ("credentialspec=file://%s" , customCredSpecFileName )
271
350
cs .updateCredSpecMapping (originalCredentialspec , dockerHostconfigSecOptCredSpec )
272
351
@@ -315,11 +394,15 @@ func (cs *CredentialSpecResource) updateCredSpecMapping(credSpecInput, targetCre
315
394
// Cleanup removes the credentialspec created for the task
316
395
func (cs * CredentialSpecResource ) Cleanup () error {
317
396
cs .clearCredentialSpec ()
397
+ if cs .domainlessGMSATask {
398
+ err := cs .deleteTaskExecutionCredentialsRegKeys ()
399
+ if err != nil {
400
+ return err
401
+ }
402
+ }
318
403
return nil
319
404
}
320
405
321
- var remove = os .Remove
322
-
323
406
// clearCredentialSpec cycles through the collection of credentialspec data and
324
407
// removes them from the task
325
408
func (cs * CredentialSpecResource ) clearCredentialSpec () {
@@ -332,14 +415,14 @@ func (cs *CredentialSpecResource) clearCredentialSpec() {
332
415
continue
333
416
}
334
417
// Split credentialspec to obtain local file-name
335
- credSpecSplit := strings .SplitAfterN (value , "credentialspec= file://" , 2 )
418
+ credSpecSplit := strings .SplitAfterN (value , "file://" , 2 )
336
419
if len (credSpecSplit ) != 2 {
337
420
seelog .Warnf ("Unable to parse target credentialspec: %s" , value )
338
421
continue
339
422
}
340
423
localCredentialSpecFile := credSpecSplit [1 ]
341
424
localCredentialSpecFilePath := filepath .Join (cs .credentialSpecResourceLocation , localCredentialSpecFile )
342
- err := remove (localCredentialSpecFilePath )
425
+ err := osRemoveImpl (localCredentialSpecFilePath )
343
426
if err != nil {
344
427
seelog .Warnf ("Unable to clear local credential spec file %s for task %s" , localCredentialSpecFile , cs .taskARN )
345
428
}
@@ -348,6 +431,30 @@ func (cs *CredentialSpecResource) clearCredentialSpec() {
348
431
}
349
432
}
350
433
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
+
351
458
func (cs * CredentialSpecResource ) setCredentialSpecResourceLocation () error {
352
459
// TODO: Use registry to setup credentialspec resource location
353
460
// This should always be available on Windows instances
@@ -376,3 +483,128 @@ func (cs *CredentialSpecResource) MarshallPlatformSpecificFields(credentialSpecR
376
483
func (cs * CredentialSpecResource ) UnmarshallPlatformSpecificFields (credentialSpecResourceJSON CredentialSpecResourceJSON ) {
377
484
return
378
485
}
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