-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
path_login.go
1477 lines (1303 loc) · 55.9 KB
/
path_login.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package awsauth
import (
"crypto/subtle"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/fullsailor/pkcs7"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/jsonutil"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
const (
reauthenticationDisabledNonce = "reauthentication-disabled-nonce"
iamAuthType = "iam"
ec2AuthType = "ec2"
ec2EntityType = "ec2_instance"
)
func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: "login$",
Fields: map[string]*framework.FieldSchema{
"role": {
Type: framework.TypeString,
Description: `Name of the role against which the login is being attempted.
If 'role' is not specified, then the login endpoint looks for a role
bearing the name of the AMI ID of the EC2 instance that is trying to login.
If a matching role is not found, login fails.`,
},
"pkcs7": {
Type: framework.TypeString,
Description: `PKCS7 signature of the identity document when using an auth_type
of ec2.`,
},
"nonce": {
Type: framework.TypeString,
Description: `The nonce to be used for subsequent login requests when
auth_type is ec2. If this parameter is not specified at
all and if reauthentication is allowed, then the backend will generate a random
nonce, attaches it to the instance's identity-whitelist entry and returns the
nonce back as part of auth metadata. This value should be used with further
login requests, to establish client authenticity. Clients can choose to set a
custom nonce if preferred, in which case, it is recommended that clients provide
a strong nonce. If a nonce is provided but with an empty value, it indicates
intent to disable reauthentication. Note that, when 'disallow_reauthentication'
option is enabled on either the role or the role tag, the 'nonce' holds no
significance.`,
},
"iam_http_request_method": {
Type: framework.TypeString,
Description: `HTTP method to use for the AWS request when auth_type is
iam. This must match what has been signed in the
presigned request. Currently, POST is the only supported value`,
},
"iam_request_url": {
Type: framework.TypeString,
Description: `Base64-encoded full URL against which to make the AWS request
when using iam auth_type.`,
},
"iam_request_body": {
Type: framework.TypeString,
Description: `Base64-encoded request body when auth_type is iam.
This must match the request body included in the signature.`,
},
"iam_request_headers": {
Type: framework.TypeString,
Description: `Base64-encoded JSON representation of the request headers when auth_type is
iam. This must at a minimum include the headers over
which AWS has included a signature.`,
},
"identity": {
Type: framework.TypeString,
Description: `Base64 encoded EC2 instance identity document. This needs to be supplied along
with the 'signature' parameter. If using 'curl' for fetching the identity
document, consider using the option '-w 0' while piping the output to 'base64'
binary.`,
},
"signature": {
Type: framework.TypeString,
Description: `Base64 encoded SHA256 RSA signature of the instance identity document. This
needs to be supplied along with 'identity' parameter.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathLoginUpdate,
},
HelpSynopsis: pathLoginSyn,
HelpDescription: pathLoginDesc,
}
}
// instanceIamRoleARN fetches the IAM role ARN associated with the given
// instance profile name
func (b *backend) instanceIamRoleARN(iamClient *iam.IAM, instanceProfileName string) (string, error) {
if iamClient == nil {
return "", fmt.Errorf("nil iamClient")
}
if instanceProfileName == "" {
return "", fmt.Errorf("missing instance profile name")
}
profile, err := iamClient.GetInstanceProfile(&iam.GetInstanceProfileInput{
InstanceProfileName: aws.String(instanceProfileName),
})
if err != nil {
return "", err
}
if profile == nil {
return "", fmt.Errorf("nil output while getting instance profile details")
}
if profile.InstanceProfile == nil {
return "", fmt.Errorf("nil instance profile in the output of instance profile details")
}
if profile.InstanceProfile.Roles == nil || len(profile.InstanceProfile.Roles) != 1 {
return "", fmt.Errorf("invalid roles in the output of instance profile details")
}
if profile.InstanceProfile.Roles[0].Arn == nil {
return "", fmt.Errorf("nil role ARN in the output of instance profile details")
}
return *profile.InstanceProfile.Roles[0].Arn, nil
}
// validateInstance queries the status of the EC2 instance using AWS EC2 API
// and checks if the instance is running and is healthy
func (b *backend) validateInstance(s logical.Storage, instanceID, region, accountID string) (*ec2.Instance, error) {
// Check if an STS configuration exists for the AWS account
sts, err := b.lockedAwsStsEntry(s, accountID)
if err != nil {
return nil, fmt.Errorf("error fetching STS config for account ID %q: %q\n", accountID, err)
}
// An empty STS role signifies the master account
stsRole := ""
if sts != nil {
stsRole = sts.StsRole
}
// Create an EC2 client to pull the instance information
ec2Client, err := b.clientEC2(s, region, stsRole)
if err != nil {
return nil, err
}
status, err := ec2Client.DescribeInstances(&ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
&ec2.Filter{
Name: aws.String("instance-id"),
Values: []*string{
aws.String(instanceID),
},
},
},
})
if err != nil {
return nil, fmt.Errorf("error fetching description for instance ID %q: %q\n", instanceID, err)
}
if status == nil {
return nil, fmt.Errorf("nil output from describe instances")
}
if len(status.Reservations) == 0 {
return nil, fmt.Errorf("no reservations found in instance description")
}
if len(status.Reservations[0].Instances) == 0 {
return nil, fmt.Errorf("no instance details found in reservations")
}
if *status.Reservations[0].Instances[0].InstanceId != instanceID {
return nil, fmt.Errorf("expected instance ID not matching the instance ID in the instance description")
}
if status.Reservations[0].Instances[0].State == nil {
return nil, fmt.Errorf("instance state in instance description is nil")
}
if *status.Reservations[0].Instances[0].State.Name != "running" {
return nil, fmt.Errorf("instance is not in 'running' state")
}
return status.Reservations[0].Instances[0], nil
}
// validateMetadata matches the given client nonce and pending time with the
// one cached in the identity whitelist during the previous login. But, if
// reauthentication is disabled, login attempt is failed immediately.
func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelistIdentity, roleEntry *awsRoleEntry) error {
// For sanity
if !storedIdentity.DisallowReauthentication && storedIdentity.ClientNonce == "" {
return fmt.Errorf("client nonce missing in stored identity")
}
// If reauthentication is disabled or if the nonce supplied matches a
// predefied nonce which indicates reauthentication to be disabled,
// authentication will not succeed.
if storedIdentity.DisallowReauthentication ||
subtle.ConstantTimeCompare([]byte(reauthenticationDisabledNonce), []byte(clientNonce)) == 1 {
return fmt.Errorf("reauthentication is disabled")
}
givenPendingTime, err := time.Parse(time.RFC3339, pendingTime)
if err != nil {
return err
}
storedPendingTime, err := time.Parse(time.RFC3339, storedIdentity.PendingTime)
if err != nil {
return err
}
// When the presented client nonce does not match the cached entry, it
// is either that a rogue client is trying to login or that a valid
// client suffered a migration. The migration is detected via
// pendingTime in the instance metadata, which sadly is only updated
// when an instance is stopped and started but *not* when the instance
// is rebooted. If reboot survivability is needed, either
// instrumentation to delete the instance ID from the whitelist is
// necessary, or the client must durably store the nonce.
//
// If the `allow_instance_migration` property of the registered role is
// enabled, then the client nonce mismatch is ignored, as long as the
// pending time in the presented instance identity document is newer
// than the cached pending time. The new pendingTime is stored and used
// for future checks.
//
// This is a weak criterion and hence the `allow_instance_migration`
// option should be used with caution.
if subtle.ConstantTimeCompare([]byte(clientNonce), []byte(storedIdentity.ClientNonce)) != 1 {
if !roleEntry.AllowInstanceMigration {
return fmt.Errorf("client nonce mismatch")
}
if roleEntry.AllowInstanceMigration && !givenPendingTime.After(storedPendingTime) {
return fmt.Errorf("client nonce mismatch and instance meta-data incorrect")
}
}
// Ensure that the 'pendingTime' on the given identity document is not
// before the 'pendingTime' that was used for previous login. This
// disallows old metadata documents from being used to perform login.
if givenPendingTime.Before(storedPendingTime) {
return fmt.Errorf("instance meta-data is older than the one used for previous login")
}
return nil
}
// Verifies the integrity of the instance identity document using its SHA256
// RSA signature. After verification, returns the unmarshaled instance identity
// document.
func (b *backend) verifyInstanceIdentitySignature(s logical.Storage, identityBytes, signatureBytes []byte) (*identityDocument, error) {
if len(identityBytes) == 0 {
return nil, fmt.Errorf("missing instance identity document")
}
if len(signatureBytes) == 0 {
return nil, fmt.Errorf("missing SHA256 RSA signature of the instance identity document")
}
// Get the public certificates that are used to verify the signature.
// This returns a slice of certificates containing the default
// certificate and all the registered certificates via
// 'config/certificate/<cert_name>' endpoint, for verifying the RSA
// digest.
publicCerts, err := b.awsPublicCertificates(s, false)
if err != nil {
return nil, err
}
if publicCerts == nil || len(publicCerts) == 0 {
return nil, fmt.Errorf("certificates to verify the signature are not found")
}
// Check if any of the certs registered at the backend can verify the
// signature
for _, cert := range publicCerts {
err := cert.CheckSignature(x509.SHA256WithRSA, identityBytes, signatureBytes)
if err == nil {
var identityDoc identityDocument
if decErr := jsonutil.DecodeJSON(identityBytes, &identityDoc); decErr != nil {
return nil, decErr
}
return &identityDoc, nil
}
}
return nil, fmt.Errorf("instance identity verification using SHA256 RSA signature is unsuccessful")
}
// Verifies the correctness of the authenticated attributes present in the PKCS#7
// signature. After verification, extracts the instance identity document from the
// signature, parses it and returns it.
func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocument, error) {
// Insert the header and footer for the signature to be able to pem decode it
pkcs7B64 = fmt.Sprintf("-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----", pkcs7B64)
// Decode the PEM encoded signature
pkcs7BER, pkcs7Rest := pem.Decode([]byte(pkcs7B64))
if len(pkcs7Rest) != 0 {
return nil, fmt.Errorf("failed to decode the PEM encoded PKCS#7 signature")
}
// Parse the signature from asn1 format into a struct
pkcs7Data, err := pkcs7.Parse(pkcs7BER.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse the BER encoded PKCS#7 signature: %v\n", err)
}
// Get the public certificates that are used to verify the signature.
// This returns a slice of certificates containing the default certificate
// and all the registered certificates via 'config/certificate/<cert_name>' endpoint
publicCerts, err := b.awsPublicCertificates(s, true)
if err != nil {
return nil, err
}
if publicCerts == nil || len(publicCerts) == 0 {
return nil, fmt.Errorf("certificates to verify the signature are not found")
}
// Before calling Verify() on the PKCS#7 struct, set the certificates to be used
// to verify the contents in the signer information.
pkcs7Data.Certificates = publicCerts
// Verify extracts the authenticated attributes in the PKCS#7 signature, and verifies
// the authenticity of the content using 'dsa.PublicKey' embedded in the public certificate.
if pkcs7Data.Verify() != nil {
return nil, fmt.Errorf("failed to verify the signature")
}
// Check if the signature has content inside of it
if len(pkcs7Data.Content) == 0 {
return nil, fmt.Errorf("instance identity document could not be found in the signature")
}
var identityDoc identityDocument
if err := jsonutil.DecodeJSON(pkcs7Data.Content, &identityDoc); err != nil {
return nil, err
}
return &identityDoc, nil
}
func (b *backend) pathLoginUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
anyEc2, allEc2 := hasValuesForEc2Auth(data)
anyIam, allIam := hasValuesForIamAuth(data)
switch {
case anyEc2 && anyIam:
return logical.ErrorResponse("supplied auth values for both ec2 and iam auth types"), nil
case anyEc2 && !allEc2:
return logical.ErrorResponse("supplied some of the auth values for the ec2 auth type but not all"), nil
case anyEc2:
return b.pathLoginUpdateEc2(req, data)
case anyIam && !allIam:
return logical.ErrorResponse("supplied some of the auth values for the iam auth type but not all"), nil
case anyIam:
return b.pathLoginUpdateIam(req, data)
default:
return logical.ErrorResponse("didn't supply required authentication values"), nil
}
}
// Returns whether the EC2 instance meets the requirements of the particular
// AWS role entry.
// The first error return value is whether there's some sort of validation
// error that means the instance doesn't meet the role requirements
// The second error return value indicates whether there's an error in even
// trying to validate those requirements
func (b *backend) verifyInstanceMeetsRoleRequirements(
s logical.Storage, instance *ec2.Instance, roleEntry *awsRoleEntry, roleName string, identityDoc *identityDocument) (error, error) {
switch {
case instance == nil:
return nil, fmt.Errorf("nil instance")
case roleEntry == nil:
return nil, fmt.Errorf("nil roleEntry")
case identityDoc == nil:
return nil, fmt.Errorf("nil identityDoc")
}
// Verify that the AccountID of the instance trying to login matches the
// AccountID specified as a constraint on role
if roleEntry.BoundAccountID != "" && identityDoc.AccountID != roleEntry.BoundAccountID {
return fmt.Errorf("account ID %q does not belong to role %q", identityDoc.AccountID, roleName), nil
}
// Verify that the AMI ID of the instance trying to login matches the
// AMI ID specified as a constraint on the role.
//
// Here, we're making a tradeoff and pulling the AMI ID out of the EC2
// API rather than the signed instance identity doc. They *should* match.
// This means we require an EC2 API call to retrieve the AMI ID, but we're
// already calling the API to validate the Instance ID anyway, so it shouldn't
// matter. The benefit is that we have the exact same code whether auth_type
// is ec2 or iam.
if roleEntry.BoundAmiID != "" {
if instance.ImageId == nil {
return nil, fmt.Errorf("AMI ID in the instance description is nil")
}
if roleEntry.BoundAmiID != *instance.ImageId {
return fmt.Errorf("AMI ID %q does not belong to role %q", instance.ImageId, roleName), nil
}
}
// Validate the SubnetID if corresponding bound was set on the role
if roleEntry.BoundSubnetID != "" {
if instance.SubnetId == nil {
return nil, fmt.Errorf("subnet ID in the instance description is nil")
}
if roleEntry.BoundSubnetID != *instance.SubnetId {
return fmt.Errorf("subnet ID %q does not satisfy the constraint on role %q", *instance.SubnetId, roleName), nil
}
}
// Validate the VpcID if corresponding bound was set on the role
if roleEntry.BoundVpcID != "" {
if instance.VpcId == nil {
return nil, fmt.Errorf("VPC ID in the instance description is nil")
}
if roleEntry.BoundVpcID != *instance.VpcId {
return fmt.Errorf("VPC ID %q does not satisfy the constraint on role %q", *instance.VpcId, roleName), nil
}
}
// Check if the IAM instance profile ARN of the instance trying to
// login, matches the IAM instance profile ARN specified as a constraint
// on the role
if roleEntry.BoundIamInstanceProfileARN != "" {
if instance.IamInstanceProfile == nil {
return nil, fmt.Errorf("IAM instance profile in the instance description is nil")
}
if instance.IamInstanceProfile.Arn == nil {
return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil")
}
iamInstanceProfileARN := *instance.IamInstanceProfile.Arn
if !strings.HasPrefix(iamInstanceProfileARN, roleEntry.BoundIamInstanceProfileARN) {
return fmt.Errorf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileARN, roleName), nil
}
}
// Check if the IAM role ARN of the instance trying to login, matches
// the IAM role ARN specified as a constraint on the role.
if roleEntry.BoundIamRoleARN != "" {
if instance.IamInstanceProfile == nil {
return nil, fmt.Errorf("IAM instance profile in the instance description is nil")
}
if instance.IamInstanceProfile.Arn == nil {
return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil")
}
// Fetch the instance profile ARN from the instance description
iamInstanceProfileARN := *instance.IamInstanceProfile.Arn
if iamInstanceProfileARN == "" {
return nil, fmt.Errorf("IAM instance profile ARN in the instance description is empty")
}
// Extract out the instance profile name from the instance
// profile ARN
iamInstanceProfileARNSlice := strings.SplitAfter(iamInstanceProfileARN, ":instance-profile/")
iamInstanceProfileName := iamInstanceProfileARNSlice[len(iamInstanceProfileARNSlice)-1]
if iamInstanceProfileName == "" {
return nil, fmt.Errorf("failed to extract out IAM instance profile name from IAM instance profile ARN")
}
// Check if an STS configuration exists for the AWS account
sts, err := b.lockedAwsStsEntry(s, identityDoc.AccountID)
if err != nil {
return fmt.Errorf("error fetching STS config for account ID %q: %q\n", identityDoc.AccountID, err), nil
}
// An empty STS role signifies the master account
stsRole := ""
if sts != nil {
stsRole = sts.StsRole
}
// Use instance profile ARN to fetch the associated role ARN
iamClient, err := b.clientIAM(s, identityDoc.Region, stsRole)
if err != nil {
return nil, fmt.Errorf("could not fetch IAM client: %v", err)
} else if iamClient == nil {
return nil, fmt.Errorf("received a nil iamClient")
}
iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileName)
if err != nil {
return nil, fmt.Errorf("IAM role ARN could not be fetched: %v", err)
}
if iamRoleARN == "" {
return nil, fmt.Errorf("IAM role ARN could not be fetched")
}
if !strings.HasPrefix(iamRoleARN, roleEntry.BoundIamRoleARN) {
return fmt.Errorf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName), nil
}
}
return nil, nil
}
// pathLoginUpdateEc2 is used to create a Vault token by the EC2 instances
// by providing the pkcs7 signature of the instance identity document
// and a client created nonce. Client nonce is optional if 'disallow_reauthentication'
// option is enabled on the registered role.
func (b *backend) pathLoginUpdateEc2(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
identityDocB64 := data.Get("identity").(string)
var identityDocBytes []byte
var err error
if identityDocB64 != "" {
identityDocBytes, err = base64.StdEncoding.DecodeString(identityDocB64)
if err != nil || len(identityDocBytes) == 0 {
return logical.ErrorResponse("failed to base64 decode the instance identity document"), nil
}
}
signatureB64 := data.Get("signature").(string)
var signatureBytes []byte
if signatureB64 != "" {
signatureBytes, err = base64.StdEncoding.DecodeString(signatureB64)
if err != nil {
return logical.ErrorResponse("failed to base64 decode the SHA256 RSA signature of the instance identity document"), nil
}
}
pkcs7B64 := data.Get("pkcs7").(string)
// Either the pkcs7 signature of the instance identity document, or
// the identity document itself along with its SHA256 RSA signature
// needs to be provided.
if pkcs7B64 == "" && (len(identityDocBytes) == 0 && len(signatureBytes) == 0) {
return logical.ErrorResponse("either pkcs7 or a tuple containing the instance identity document and its SHA256 RSA signature needs to be provided"), nil
} else if pkcs7B64 != "" && (len(identityDocBytes) != 0 && len(signatureBytes) != 0) {
return logical.ErrorResponse("both pkcs7 and a tuple containing the instance identity document and its SHA256 RSA signature is supplied; provide only one"), nil
}
// Verify the signature of the identity document and unmarshal it
var identityDocParsed *identityDocument
if pkcs7B64 != "" {
identityDocParsed, err = b.parseIdentityDocument(req.Storage, pkcs7B64)
if err != nil {
return nil, err
}
if identityDocParsed == nil {
return logical.ErrorResponse("failed to verify the instance identity document using pkcs7"), nil
}
} else {
identityDocParsed, err = b.verifyInstanceIdentitySignature(req.Storage, identityDocBytes, signatureBytes)
if err != nil {
return nil, err
}
if identityDocParsed == nil {
return logical.ErrorResponse("failed to verify the instance identity document using the SHA256 RSA digest"), nil
}
}
roleName := data.Get("role").(string)
// If roleName is not supplied, a role in the name of the instance's AMI ID will be looked for
if roleName == "" {
roleName = identityDocParsed.AmiID
}
// Get the entry for the role used by the instance
roleEntry, err := b.lockedAWSRole(req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("entry for role %q not found", roleName)), nil
}
if roleEntry.AuthType != ec2AuthType {
return logical.ErrorResponse(fmt.Sprintf("auth method ec2 not allowed for role %s", roleName)), nil
}
// Validate the instance ID by making a call to AWS EC2 DescribeInstances API
// and fetching the instance description. Validation succeeds only if the
// instance is in 'running' state.
instance, err := b.validateInstance(req.Storage, identityDocParsed.InstanceID, identityDocParsed.Region, identityDocParsed.AccountID)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %v", err)), nil
}
// Verify that the `Region` of the instance trying to login matches the
// `Region` specified as a constraint on role
if roleEntry.BoundRegion != "" && identityDocParsed.Region != roleEntry.BoundRegion {
return logical.ErrorResponse(fmt.Sprintf("Region %q does not satisfy the constraint on role %q", identityDocParsed.Region, roleName)), nil
}
validationError, err := b.verifyInstanceMeetsRoleRequirements(req.Storage, instance, roleEntry, roleName, identityDocParsed)
if err != nil {
return nil, err
}
if validationError != nil {
return logical.ErrorResponse(fmt.Sprintf("Error validating instance: %v", validationError)), nil
}
// Get the entry from the identity whitelist, if there is one
storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDocParsed.InstanceID)
if err != nil {
return nil, err
}
// disallowReauthentication value that gets cached at the stored
// identity-whitelist entry is determined not just by the role entry.
// If client explicitly sets nonce to be empty, it implies intent to
// disable reauthentication. Also, role tag can override the 'false'
// value with 'true' (the other way around is not allowed).
// Read the value from the role entry
disallowReauthentication := roleEntry.DisallowReauthentication
clientNonce := ""
// Check if the nonce is supplied by the client
clientNonceRaw, clientNonceSupplied := data.GetOk("nonce")
if clientNonceSupplied {
clientNonce = clientNonceRaw.(string)
// Nonce explicitly set to empty implies intent to disable
// reauthentication by the client. Set a predefined nonce which
// indicates reauthentication being disabled.
if clientNonce == "" {
clientNonce = reauthenticationDisabledNonce
// Ensure that the intent lands in the whitelist
disallowReauthentication = true
}
}
// This is NOT a first login attempt from the client
if storedIdentity != nil {
// Check if the client nonce match the cached nonce and if the pending time
// of the identity document is not before the pending time of the document
// with which previous login was made. If 'allow_instance_migration' is
// enabled on the registered role, client nonce requirement is relaxed.
if err = validateMetadata(clientNonce, identityDocParsed.PendingTime, storedIdentity, roleEntry); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Don't let subsequent login attempts to bypass in initial
// intent of disabling reauthentication, despite the properties
// of role getting updated. For example: Role has the value set
// to 'false', a role-tag login sets the value to 'true', then
// role gets updated to not use a role-tag, and a login attempt
// is made with role's value set to 'false'. Removing the entry
// from the identity-whitelist should be the only way to be
// able to login from the instance again.
disallowReauthentication = disallowReauthentication || storedIdentity.DisallowReauthentication
}
// If we reach this point without erroring and if the client nonce was
// not supplied, a first time login is implied and that the client
// intends that the nonce be generated by the backend. Create a random
// nonce to be associated for the instance ID.
if !clientNonceSupplied {
if clientNonce, err = uuid.GenerateUUID(); err != nil {
return nil, fmt.Errorf("failed to generate random nonce")
}
}
// Load the current values for max TTL and policies from the role entry,
// before checking for overriding max TTL in the role tag. The shortest
// max TTL is used to cap the token TTL; the longest max TTL is used to
// make the whitelist entry as long as possible as it controls for replay
// attacks.
shortestMaxTTL := b.System().MaxLeaseTTL()
longestMaxTTL := b.System().MaxLeaseTTL()
if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < shortestMaxTTL {
shortestMaxTTL = roleEntry.MaxTTL
}
if roleEntry.MaxTTL > longestMaxTTL {
longestMaxTTL = roleEntry.MaxTTL
}
policies := roleEntry.Policies
rTagMaxTTL := time.Duration(0)
var roleTagResp *roleTagLoginResponse
if roleEntry.RoleTag != "" {
roleTagResp, err := b.handleRoleTagLogin(req.Storage, roleName, roleEntry, instance)
if err != nil {
return nil, err
}
if roleTagResp == nil {
return logical.ErrorResponse("failed to fetch and verify the role tag"), nil
}
}
if roleTagResp != nil {
// Role tag is enabled on the role.
//
// Overwrite the policies with the ones returned from processing the role tag
// If there are no policies on the role tag, policies on the role are inherited.
// If policies on role tag are set, by this point, it is verified that it is a subset of the
// policies on the role. So, apply only those.
if len(roleTagResp.Policies) != 0 {
policies = roleTagResp.Policies
}
// If roleEntry had disallowReauthentication set to 'true', do not reset it
// to 'false' based on role tag having it not set. But, if role tag had it set,
// be sure to override the value.
if !disallowReauthentication {
disallowReauthentication = roleTagResp.DisallowReauthentication
}
// Cache the value of role tag's max_ttl value
rTagMaxTTL = roleTagResp.MaxTTL
// Scope the shortestMaxTTL to the value set on the role tag
if roleTagResp.MaxTTL > time.Duration(0) && roleTagResp.MaxTTL < shortestMaxTTL {
shortestMaxTTL = roleTagResp.MaxTTL
}
if roleTagResp.MaxTTL > longestMaxTTL {
longestMaxTTL = roleTagResp.MaxTTL
}
}
// Save the login attempt in the identity whitelist
currentTime := time.Now()
if storedIdentity == nil {
// Role, ClientNonce and CreationTime of the identity entry,
// once set, should never change.
storedIdentity = &whitelistIdentity{
Role: roleName,
ClientNonce: clientNonce,
CreationTime: currentTime,
}
}
// DisallowReauthentication, PendingTime, LastUpdatedTime and
// ExpirationTime may change.
storedIdentity.LastUpdatedTime = currentTime
storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL)
storedIdentity.PendingTime = identityDocParsed.PendingTime
storedIdentity.DisallowReauthentication = disallowReauthentication
// Don't cache the nonce if DisallowReauthentication is set
if storedIdentity.DisallowReauthentication {
storedIdentity.ClientNonce = ""
}
// Sanitize the nonce to a reasonable length
if len(clientNonce) > 128 && !storedIdentity.DisallowReauthentication {
return logical.ErrorResponse("client nonce exceeding the limit of 128 characters"), nil
}
if err = setWhitelistIdentityEntry(req.Storage, identityDocParsed.InstanceID, storedIdentity); err != nil {
return nil, err
}
resp := &logical.Response{
Auth: &logical.Auth{
Period: roleEntry.Period,
Policies: policies,
Metadata: map[string]string{
"instance_id": identityDocParsed.InstanceID,
"region": identityDocParsed.Region,
"account_id": identityDocParsed.AccountID,
"role_tag_max_ttl": rTagMaxTTL.String(),
"role": roleName,
"ami_id": identityDocParsed.AmiID,
},
LeaseOptions: logical.LeaseOptions{
Renewable: true,
TTL: roleEntry.TTL,
},
},
}
// Return the nonce only if reauthentication is allowed
if !disallowReauthentication {
// Echo the client nonce back. If nonce param was not supplied
// to the endpoint at all (setting it to empty string does not
// qualify here), callers should extract out the nonce from
// this field for reauthentication requests.
resp.Auth.Metadata["nonce"] = clientNonce
}
if roleEntry.Period > time.Duration(0) {
resp.Auth.TTL = roleEntry.Period
} else {
// Cap the TTL value.
shortestTTL := b.System().DefaultLeaseTTL()
if roleEntry.TTL > time.Duration(0) && roleEntry.TTL < shortestTTL {
shortestTTL = roleEntry.TTL
}
if shortestMaxTTL < shortestTTL {
resp.AddWarning(fmt.Sprintf("Effective ttl of %q exceeded the effective max_ttl of %q; ttl value is capped appropriately", (shortestTTL / time.Second).String(), (shortestMaxTTL / time.Second).String()))
shortestTTL = shortestMaxTTL
}
resp.Auth.TTL = shortestTTL
}
return resp, nil
}
// handleRoleTagLogin is used to fetch the role tag of the instance and
// verifies it to be correct. Then the policies for the login request will be
// set off of the role tag, if certain creteria satisfies.
func (b *backend) handleRoleTagLogin(s logical.Storage, roleName string, roleEntry *awsRoleEntry, instance *ec2.Instance) (*roleTagLoginResponse, error) {
if roleEntry == nil {
return nil, fmt.Errorf("nil role entry")
}
if instance == nil {
return nil, fmt.Errorf("nil instance")
}
// Input validation on instance is not performed here considering
// that it would have been done in validateInstance method.
tags := instance.Tags
if tags == nil || len(tags) == 0 {
return nil, fmt.Errorf("missing tag with key %q on the instance", roleEntry.RoleTag)
}
// Iterate through the tags attached on the instance and look for
// a tag with its 'key' matching the expected role tag value.
rTagValue := ""
for _, tagItem := range tags {
if tagItem.Key != nil && *tagItem.Key == roleEntry.RoleTag {
rTagValue = *tagItem.Value
break
}
}
// If 'role_tag' is enabled on the role, and if a corresponding tag is not found
// to be attached to the instance, fail.
if rTagValue == "" {
return nil, fmt.Errorf("missing tag with key %q on the instance", roleEntry.RoleTag)
}
// Parse the role tag into a struct, extract the plaintext part of it and verify its HMAC
rTag, err := b.parseAndVerifyRoleTagValue(s, rTagValue)
if err != nil {
return nil, err
}
// Check if the role name with which this login is being made is same
// as the role name embedded in the tag.
if rTag.Role != roleName {
return nil, fmt.Errorf("role on the tag is not matching the role supplied")
}
// If instance_id was set on the role tag, check if the same instance is attempting to login
if rTag.InstanceID != "" && rTag.InstanceID != *instance.InstanceId {
return nil, fmt.Errorf("role tag is being used by an unauthorized instance.")
}
// Check if the role tag is blacklisted
blacklistEntry, err := b.lockedBlacklistRoleTagEntry(s, rTagValue)
if err != nil {
return nil, err
}
if blacklistEntry != nil {
return nil, fmt.Errorf("role tag is blacklisted")
}
// Ensure that the policies on the RoleTag is a subset of policies on the role
if !strutil.StrListSubset(roleEntry.Policies, rTag.Policies) {
return nil, fmt.Errorf("policies on the role tag must be subset of policies on the role")
}
return &roleTagLoginResponse{
Policies: rTag.Policies,
MaxTTL: rTag.MaxTTL,
DisallowReauthentication: rTag.DisallowReauthentication,
}, nil
}
// pathLoginRenew is used to renew an authenticated token
func (b *backend) pathLoginRenew(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
authType, ok := req.Auth.Metadata["auth_type"]
if !ok {
// backwards compatibility for clients that have leases from before we added auth_type
authType = ec2AuthType
}
if authType == ec2AuthType {
return b.pathLoginRenewEc2(req, data)
} else if authType == iamAuthType {
return b.pathLoginRenewIam(req, data)
} else {
return nil, fmt.Errorf("unrecognized auth_type: %q", authType)
}
}
func (b *backend) pathLoginRenewIam(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
canonicalArn := req.Auth.Metadata["canonical_arn"]
if canonicalArn == "" {
return nil, fmt.Errorf("unable to retrieve canonical ARN from metadata during renewal")
}
roleName := req.Auth.InternalData["role_name"].(string)
if roleName == "" {
return nil, fmt.Errorf("error retrieving role_name during renewal")
}
roleEntry, err := b.lockedAWSRole(req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return nil, fmt.Errorf("role entry not found")
}
if entityType, ok := req.Auth.Metadata["inferred_entity_type"]; !ok {
if entityType == ec2EntityType {
instanceID, ok := req.Auth.Metadata["inferred_entity_id"]
if !ok {
return nil, fmt.Errorf("no inferred entity ID in auth metadata")
}
instanceRegion, ok := req.Auth.Metadata["inferred_aws_region"]
if !ok {
return nil, fmt.Errorf("no inferred AWS region in auth metadata")
}
_, err := b.validateInstance(req.Storage, instanceID, instanceRegion, req.Auth.Metadata["accountID"])
if err != nil {
return nil, fmt.Errorf("failed to verify instance ID %q: %v", instanceID, err)
}
} else {
return nil, fmt.Errorf("unrecognized entity_type in metadata: %q", entityType)
}
}
if roleEntry.BoundIamPrincipalARN != canonicalArn {
return nil, fmt.Errorf("role no longer bound to arn %q", canonicalArn)
}
return framework.LeaseExtend(roleEntry.TTL, roleEntry.MaxTTL, b.System())(req, data)
}
func (b *backend) pathLoginRenewEc2(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
instanceID := req.Auth.Metadata["instance_id"]
if instanceID == "" {
return nil, fmt.Errorf("unable to fetch instance ID from metadata during renewal")
}
region := req.Auth.Metadata["region"]
if region == "" {
return nil, fmt.Errorf("unable to fetch region from metadata during renewal")
}
// Ensure backwards compatibility for older clients without account_id saved in metadata
accountID, ok := req.Auth.Metadata["account_id"]
if ok {
if accountID == "" {
return nil, fmt.Errorf("unable to fetch account_id from metadata during renewal")
}
}
// Cross check that the instance is still in 'running' state
_, err := b.validateInstance(req.Storage, instanceID, region, accountID)
if err != nil {
return nil, fmt.Errorf("failed to verify instance ID %q: %q", instanceID, err)
}
storedIdentity, err := whitelistIdentityEntry(req.Storage, instanceID)
if err != nil {
return nil, err
}
if storedIdentity == nil {
return nil, fmt.Errorf("failed to verify the whitelist identity entry for instance ID: %q", instanceID)
}
// Ensure that role entry is not deleted
roleEntry, err := b.lockedAWSRole(req.Storage, storedIdentity.Role)
if err != nil {
return nil, err
}
if roleEntry == nil {
return nil, fmt.Errorf("role entry not found")
}
// If the login was made using the role tag, then max_ttl from tag