package awsauth import ( "context" "crypto/subtle" "crypto/x509" "encoding/base64" "encoding/pem" "encoding/xml" "errors" "fmt" "io/ioutil" "net/http" "net/url" "regexp" "strings" "time" "github.com/aws/aws-sdk-go/aws" awsClient "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" "github.com/fullsailor/pkcs7" "github.com/hashicorp/errwrap" cleanhttp "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-retryablehttp" uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/awsutil" "github.com/hashicorp/vault/sdk/helper/cidrutil" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/logical" ) const ( reauthenticationDisabledNonce = "reauthentication-disabled-nonce" iamAuthType = "iam" ec2AuthType = "ec2" ec2EntityType = "ec2_instance" // Retry configuration retryWaitMin = 500 * time.Millisecond retryWaitMax = 30 * time.Second ) var ( errRequestBodyNotValid = errors.New("iam request body is invalid") errInvalidGetCallerIdentityResponse = errors.New("body of GetCallerIdentity is invalid") ) func (b *backend) pathLogin() *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.TypeHeader, Description: `Key/value pairs of headers for use in the sts:GetCallerIdentity HTTP requests headers when auth_type is iam. Can be either a Base64-encoded, JSON-serialized string, or a JSON object of key/value pairs. 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.`, }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.UpdateOperation: &framework.PathOperation{ Callback: b.pathLoginUpdate, }, logical.AliasLookaheadOperation: &framework.PathOperation{ Callback: 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 "", awsutil.AppendAWSError(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(ctx context.Context, s logical.Storage, instanceID, region, accountID string) (*ec2.Instance, error) { // Create an EC2 client to pull the instance information ec2Client, err := b.clientEC2(ctx, s, region, accountID) if err != nil { return nil, err } status, err := ec2Client.DescribeInstances(&ec2.DescribeInstancesInput{ InstanceIds: []*string{ aws.String(instanceID), }, }) if err != nil { errW := errwrap.Wrapf(fmt.Sprintf("error fetching description for instance ID %q: {{err}}", instanceID), err) return nil, errwrap.Wrap(errW, awsutil.CheckAWSError(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 // predefined 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(ctx context.Context, 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/' endpoint, for verifying the RSA // digest. publicCerts, err := b.awsPublicCertificates(ctx, 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(ctx context.Context, 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, errwrap.Wrapf("failed to parse the BER encoded PKCS#7 signature: {{err}}", 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/' endpoint publicCerts, err := b.awsPublicCertificates(ctx, 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(ctx context.Context, 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(ctx, 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(ctx, 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(ctx context.Context, 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 instance ID matches one of the ones set by the role if len(roleEntry.BoundEc2InstanceIDs) > 0 && !strutil.StrListContains(roleEntry.BoundEc2InstanceIDs, *instance.InstanceId) { return fmt.Errorf("instance ID %q does not belong to the role %q", *instance.InstanceId, roleName), nil } // Verify that the AccountID of the instance trying to login matches the // AccountID specified as a constraint on role if len(roleEntry.BoundAccountIDs) > 0 && !strutil.StrListContains(roleEntry.BoundAccountIDs, identityDoc.AccountID) { 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 len(roleEntry.BoundAmiIDs) > 0 { if instance.ImageId == nil { return nil, fmt.Errorf("AMI ID in the instance description is nil") } if !strutil.StrListContains(roleEntry.BoundAmiIDs, *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 len(roleEntry.BoundSubnetIDs) > 0 { if instance.SubnetId == nil { return nil, fmt.Errorf("subnet ID in the instance description is nil") } if !strutil.StrListContains(roleEntry.BoundSubnetIDs, *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 len(roleEntry.BoundVpcIDs) > 0 { if instance.VpcId == nil { return nil, fmt.Errorf("VPC ID in the instance description is nil") } if !strutil.StrListContains(roleEntry.BoundVpcIDs, *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 len(roleEntry.BoundIamInstanceProfileARNs) > 0 { 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 matchesInstanceProfile := false // NOTE: Can't use strutil.StrListContainsGlob. A * is a perfectly valid character in the "path" component // of an ARN. See, e.g., https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateInstanceProfile.html : // The path allows strings "containing any ASCII character from the ! (\u0021) thru the DEL character // (\u007F), including most punctuation characters, digits, and upper and lowercased letters." // So, e.g., arn:aws:iam::123456789012:instance-profile/Some*Path/MyProfileName is a perfectly valid instance // profile ARN, and it wouldn't be correct to expand the * in the middle as a wildcard. // If a user wants to match an IAM instance profile arn beginning with arn:aws:iam::123456789012:instance-profile/foo* // then bound_iam_instance_profile_arn would need to be arn:aws:iam::123456789012:instance-profile/foo** // Wanting to exactly match an ARN that has a * at the end is not a valid use case. The * is only valid in the // path; it's not valid in the name. That means no valid ARN can ever end with a *. For example, // arn:aws:iam::123456789012:instance-profile/Foo* is NOT valid as an instance profile ARN, so no valid instance // profile ARN could ever equal that value. for _, boundInstanceProfileARN := range roleEntry.BoundIamInstanceProfileARNs { switch { case strings.HasSuffix(boundInstanceProfileARN, "*") && strings.HasPrefix(iamInstanceProfileARN, boundInstanceProfileARN[:len(boundInstanceProfileARN)-1]): matchesInstanceProfile = true break case iamInstanceProfileARN == boundInstanceProfileARN: matchesInstanceProfile = true break } } if !matchesInstanceProfile { 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 len(roleEntry.BoundIamRoleARNs) > 0 { 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 iamInstanceProfileEntity, err := parseIamArn(iamInstanceProfileARN) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("failed to parse IAM instance profile ARN %q: {{err}}", iamInstanceProfileARN), err) } // Use instance profile ARN to fetch the associated role ARN iamClient, err := b.clientIAM(ctx, s, identityDoc.Region, identityDoc.AccountID) if err != nil { return nil, errwrap.Wrapf("could not fetch IAM client: {{err}}", err) } else if iamClient == nil { return nil, fmt.Errorf("received a nil iamClient") } iamRoleARN, err := b.instanceIamRoleARN(iamClient, iamInstanceProfileEntity.FriendlyName) if err != nil { return nil, errwrap.Wrapf("IAM role ARN could not be fetched: {{err}}", err) } if iamRoleARN == "" { return nil, fmt.Errorf("IAM role ARN could not be fetched") } matchesInstanceRoleARN := false for _, boundIamRoleARN := range roleEntry.BoundIamRoleARNs { switch { // as with boundInstanceProfileARN, can't use strutil.StrListContainsGlob because * can validly exist in the middle of an ARN case strings.HasSuffix(boundIamRoleARN, "*") && strings.HasPrefix(iamRoleARN, boundIamRoleARN[:len(boundIamRoleARN)-1]): matchesInstanceRoleARN = true break case iamRoleARN == boundIamRoleARN: matchesInstanceRoleARN = true break } } if !matchesInstanceRoleARN { 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(ctx context.Context, 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(ctx, 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(ctx, 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.role(ctx, 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 } // Check for a CIDR match. if len(roleEntry.TokenBoundCIDRs) > 0 { if req.Connection == nil { b.Logger().Warn("token bound CIDRs found but no connection information available for validation") return nil, logical.ErrPermissionDenied } if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, roleEntry.TokenBoundCIDRs) { return nil, logical.ErrPermissionDenied } } if roleEntry.AuthType != ec2AuthType { return logical.ErrorResponse(fmt.Sprintf("auth method ec2 not allowed for role %s", roleName)), nil } identityConfigEntry, err := identityConfigEntry(ctx, req.Storage) if err != nil { return nil, err } identityAlias := "" switch identityConfigEntry.EC2Alias { case identityAliasRoleID: identityAlias = roleEntry.RoleID case identityAliasEC2InstanceID: identityAlias = identityDocParsed.InstanceID case identityAliasEC2ImageID: identityAlias = identityDocParsed.AmiID } // If we're just looking up for MFA, return the Alias info if req.Operation == logical.AliasLookaheadOperation { return &logical.Response{ Auth: &logical.Auth{ Alias: &logical.Alias{ Name: identityAlias, }, }, }, 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(ctx, 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 len(roleEntry.BoundRegions) > 0 && !strutil.StrListContains(roleEntry.BoundRegions, identityDocParsed.Region) { return logical.ErrorResponse(fmt.Sprintf("Region %q does not satisfy the constraint on role %q", identityDocParsed.Region, roleName)), nil } validationError, err := b.verifyInstanceMeetsRoleRequirements(ctx, 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(ctx, 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 the 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.TokenMaxTTL > time.Duration(0) && roleEntry.TokenMaxTTL < shortestMaxTTL { shortestMaxTTL = roleEntry.TokenMaxTTL } if roleEntry.TokenMaxTTL > longestMaxTTL { longestMaxTTL = roleEntry.TokenMaxTTL } policies := roleEntry.TokenPolicies rTagMaxTTL := time.Duration(0) var roleTagResp *roleTagLoginResponse if roleEntry.RoleTag != "" { roleTagResp, err = b.handleRoleTagLogin(ctx, 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(ctx, req.Storage, identityDocParsed.InstanceID, storedIdentity); err != nil { return nil, err } auth := &logical.Auth{ Metadata: map[string]string{ "role_tag_max_ttl": rTagMaxTTL.String(), "role": roleName, }, Alias: &logical.Alias{ Name: identityAlias, }, InternalData: map[string]interface{}{ "instance_id": identityDocParsed.InstanceID, "region": identityDocParsed.Region, "account_id": identityDocParsed.AccountID, }, } roleEntry.PopulateTokenAuth(auth) if err := identityConfigEntry.EC2AuthMetadataHandler.PopulateDesiredMetadata(auth, map[string]string{ "instance_id": identityDocParsed.InstanceID, "region": identityDocParsed.Region, "account_id": identityDocParsed.AccountID, "ami_id": identityDocParsed.AmiID, "auth_type": ec2AuthType, }); err != nil { b.Logger().Warn("unable to set alias metadata", "err", err) } resp := &logical.Response{ Auth: auth, } resp.Auth.Policies = policies resp.Auth.LeaseOptions.MaxTTL = shortestMaxTTL // Return the nonce only if reauthentication is allowed and if the nonce // was not supplied by the user. if !disallowReauthentication && !clientNonceSupplied { // 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 } 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 criteria satisfies. func (b *backend) handleRoleTagLogin(ctx context.Context, 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(ctx, 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(ctx, 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.TokenPolicies, 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(ctx context.Context, 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(ctx, req, data) } else if authType == iamAuthType { return b.pathLoginRenewIam(ctx, req, data) } else { return nil, fmt.Errorf("unrecognized auth_type: %q", authType) } } func (b *backend) pathLoginRenewIam(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { canonicalArn, err := getMetadataValue(req.Auth, "canonical_arn") if err != nil { return nil, err } roleName := "" roleNameIfc, ok := req.Auth.InternalData["role_name"] if ok { roleName = roleNameIfc.(string) } if roleName == "" { return nil, fmt.Errorf("error retrieving role_name during renewal") } roleEntry, err := b.role(ctx, req.Storage, roleName) if err != nil { return nil, err } if roleEntry == nil { return nil, fmt.Errorf("role entry not found") } // we don't really care what the inferred entity type was when the role was initially created. We // care about what the role currently requires. However, the metadata's inferred_entity_id is only // set when inferencing is turned on at initial login time. So, if inferencing is turned on, any // existing roles will NOT be able to renew tokens. // This might change later, but authenticating the actual inferred entity ID is NOT done if there // is no inferencing requested in the role. The reason is that authenticating the inferred entity // ID requires additional AWS IAM permissions that might not be present (e.g., // ec2:DescribeInstances) as well as additional inferencing configuration (the inferred region). // So, for now, if you want to turn on inferencing, all clients must re-authenticate and cannot // renew existing tokens. if roleEntry.InferredEntityType != "" { if roleEntry.InferredEntityType == ec2EntityType { instanceID, err := getMetadataValue(req.Auth, "inferred_entity_id") if err != nil { return nil, err } instanceRegion, err := getMetadataValue(req.Auth, "inferred_aws_region") if err != nil { return nil, err } accountID, err := getMetadataValue(req.Auth, "account_id") if err != nil { b.Logger().Debug("account_id not present during iam renewal attempt, continuing to attempt validation") } if _, err := b.validateInstance(ctx, req.Storage, instanceID, instanceRegion, accountID); err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("failed to verify instance ID %q: {{err}}", instanceID), err) } } else { return nil, fmt.Errorf("unrecognized entity_type in metadata: %q", roleEntry.InferredEntityType) } } // Note that the error messages below can leak a little bit of information about the role information // For example, if on renew, the client gets the "error parsing ARN..." error message, the client // will know that it's a wildcard bind (but not the actual bind), even if the client can't actually // read the role directly to know what the bind is. It's a relatively small amount of leakage, in // some fairly corner cases, and in the most likely error case (role has been changed to a new ARN), // the error message is identical. if len(roleEntry.BoundIamPrincipalARNs) > 0 { // We might not get here if all bindings were on the inferred entity, which we've already validated // above // As with logins, there are three ways to pass this check: // 1: clientUserId is in roleEntry.BoundIamPrincipalIDs (entries in roleEntry.BoundIamPrincipalIDs // implies that roleEntry.ResolveAWSUniqueIDs is true) // 2: roleEntry.ResolveAWSUniqueIDs is false and canonical_arn is in roleEntry.BoundIamPrincipalARNs // 3: Full ARN matches one of the wildcard globs in roleEntry.BoundIamPrincipalARNs clientUserId, err := getMetadataValue(req.Auth, "client_user_id") switch { case err == nil && strutil.StrListContains(roleEntry.BoundIamPrincipalIDs, clientUserId): // check 1 passed case !roleEntry.ResolveAWSUniqueIDs && strutil.StrListContains(roleEntry.BoundIamPrincipalARNs, canonicalArn): // check 2 passed default: // check 3 is a bit more complex, so we do it last fullArn := b.getCachedUserId(clientUserId) if fullArn == "" { entity, err := parseIamArn(canonicalArn) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("error parsing ARN %q: {{err}}", canonicalArn), err) } fullArn, err = b.fullArn(ctx, entity, req.Storage) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("error looking up full ARN of entity %v: {{err}}", entity), err) } if fullArn == "" { return nil, fmt.Errorf("got empty string back when looking up full ARN of entity %v", entity) } if clientUserId != "" { b.setCachedUserId(clientUserId, fullArn) } } matchedWildcardBind := false for _, principalARN := range roleEntry.BoundIamPrincipalARNs { if strings.HasSuffix(principalARN, "*") && strutil.GlobbedStringsMatch(principalARN, fullArn) { matchedWildcardBind = true break } } if !matchedWildcardBind { return nil, fmt.Errorf("role no longer bound to ARN %q", canonicalArn) } } } resp := &logical.Response{Auth: req.Auth} resp.Auth.TTL = roleEntry.TokenTTL resp.Auth.MaxTTL = roleEntry.TokenMaxTTL resp.Auth.Period = roleEntry.TokenPeriod return resp, nil } func (b *backend) pathLoginRenewEc2(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { instanceID, err := getMetadataValue(req.Auth, "instance_id") if err != nil { return nil, err } region, err := getMetadataValue(req.Auth, "region") if err != nil { return nil, err } accountID, err := getMetadataValue(req.Auth, "account_id") if err != nil { b.Logger().Debug("account_id not present during ec2 renewal attempt, continuing to attempt validation") } // Cross check that the instance is still in 'running' state if _, err := b.validateInstance(ctx, req.Storage, instanceID, region, accountID); err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("failed to verify instance ID %q: {{err}}", instanceID), err) } storedIdentity, err := whitelistIdentityEntry(ctx, 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.role(ctx, 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 // is cached in internal data during login and used here to cap the // max_ttl of renewal. rTagMaxTTL, err := time.ParseDuration(req.Auth.Metadata["role_tag_max_ttl"]) if err != nil { return nil, err } // Re-evaluate the maxTTL bounds shortestMaxTTL := b.System().MaxLeaseTTL() longestMaxTTL := b.System().MaxLeaseTTL() if roleEntry.TokenMaxTTL > time.Duration(0) && roleEntry.TokenMaxTTL < shortestMaxTTL { shortestMaxTTL = roleEntry.TokenMaxTTL } if roleEntry.TokenMaxTTL > longestMaxTTL { longestMaxTTL = roleEntry.TokenMaxTTL } if rTagMaxTTL > time.Duration(0) && rTagMaxTTL < shortestMaxTTL { shortestMaxTTL = rTagMaxTTL } if rTagMaxTTL > longestMaxTTL { longestMaxTTL = rTagMaxTTL } // Only LastUpdatedTime and ExpirationTime change and all other fields remain the same currentTime := time.Now() storedIdentity.LastUpdatedTime = currentTime storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL) // Updating the expiration time is required for the tidy operation on the // whitelist identity storage items if err = setWhitelistIdentityEntry(ctx, req.Storage, instanceID, storedIdentity); err != nil { return nil, err } resp := &logical.Response{Auth: req.Auth} resp.Auth.TTL = roleEntry.TokenTTL resp.Auth.MaxTTL = shortestMaxTTL resp.Auth.Period = roleEntry.TokenPeriod return resp, nil } func (b *backend) pathLoginUpdateIam(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { method := data.Get("iam_http_request_method").(string) if method == "" { return logical.ErrorResponse("missing iam_http_request_method"), nil } // In the future, might consider supporting GET if method != "POST" { return logical.ErrorResponse("invalid iam_http_request_method; currently only 'POST' is supported"), nil } rawUrlB64 := data.Get("iam_request_url").(string) if rawUrlB64 == "" { return logical.ErrorResponse("missing iam_request_url"), nil } rawUrl, err := base64.StdEncoding.DecodeString(rawUrlB64) if err != nil { return logical.ErrorResponse("failed to base64 decode iam_request_url"), nil } parsedUrl, err := url.Parse(string(rawUrl)) if err != nil { return logical.ErrorResponse("error parsing iam_request_url"), nil } if parsedUrl.RawQuery != "" { // Should be no query parameters return logical.ErrorResponse(logical.ErrInvalidRequest.Error()), nil } // TODO: There are two potentially valid cases we're not yet supporting that would // necessitate this check being changed. First, if we support GET requests. // Second if we support presigned POST requests bodyB64 := data.Get("iam_request_body").(string) if bodyB64 == "" { return logical.ErrorResponse("missing iam_request_body"), nil } bodyRaw, err := base64.StdEncoding.DecodeString(bodyB64) if err != nil { return logical.ErrorResponse("failed to base64 decode iam_request_body"), nil } body := string(bodyRaw) if err = validateLoginIamRequestBody(body); err != nil { return logical.ErrorResponse(err.Error()), nil } headers := data.Get("iam_request_headers").(http.Header) if len(headers) == 0 { return logical.ErrorResponse("missing iam_request_headers"), nil } config, err := b.lockedClientConfigEntry(ctx, req.Storage) if err != nil { return logical.ErrorResponse("error getting configuration"), nil } endpoint := "https://sts.amazonaws.com" maxRetries := awsClient.DefaultRetryerMaxNumRetries if config != nil { if config.IAMServerIdHeaderValue != "" { err = validateVaultHeaderValue(headers, parsedUrl, config.IAMServerIdHeaderValue) if err != nil { return logical.ErrorResponse(fmt.Sprintf("error validating %s header: %v", iamServerIdHeader, err)), nil } } if err = config.validateAllowedSTSHeaderValues(headers); err != nil { return logical.ErrorResponse(err.Error()), nil } if config.STSEndpoint != "" { endpoint = config.STSEndpoint } if config.MaxRetries >= 0 { maxRetries = config.MaxRetries } } callerID, err := submitCallerIdentityRequest(ctx, maxRetries, method, endpoint, parsedUrl, body, headers) if err != nil { return logical.ErrorResponse(fmt.Sprintf("error making upstream request: %v", err)), nil } entity, err := parseIamArn(callerID.Arn) if err != nil { return logical.ErrorResponse(fmt.Sprintf("error parsing arn %q: %v", callerID.Arn, err)), nil } roleName := data.Get("role").(string) if roleName == "" { roleName = entity.FriendlyName } roleEntry, err := b.role(ctx, req.Storage, roleName) if err != nil { return nil, err } if roleEntry == nil { return logical.ErrorResponse(fmt.Sprintf("entry for role %s not found", roleName)), nil } // Check for a CIDR match. if len(roleEntry.TokenBoundCIDRs) > 0 { if req.Connection == nil { b.Logger().Warn("token bound CIDRs found but no connection information available for validation") return nil, logical.ErrPermissionDenied } if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, roleEntry.TokenBoundCIDRs) { return nil, logical.ErrPermissionDenied } } if roleEntry.AuthType != iamAuthType { return logical.ErrorResponse(fmt.Sprintf("auth method iam not allowed for role %s", roleName)), nil } identityConfigEntry, err := identityConfigEntry(ctx, req.Storage) if err != nil { return nil, err } // This could either be a "userID:SessionID" (in the case of an assumed role) or just a "userID" // (in the case of an IAM user). callerUniqueId := strings.Split(callerID.UserId, ":")[0] identityAlias := "" switch identityConfigEntry.IAMAlias { case identityAliasRoleID: identityAlias = roleEntry.RoleID case identityAliasIAMUniqueID: identityAlias = callerUniqueId case identityAliasIAMFullArn: identityAlias = callerID.Arn } // If we're just looking up for MFA, return the Alias info if req.Operation == logical.AliasLookaheadOperation { return &logical.Response{ Auth: &logical.Auth{ Alias: &logical.Alias{ Name: identityAlias, }, }, }, nil } // The role creation should ensure that either we're inferring this is an EC2 instance // or that we're binding an ARN if len(roleEntry.BoundIamPrincipalARNs) > 0 { // As with renews, there are three ways to pass this check: // 1: callerUniqueId is in roleEntry.BoundIamPrincipalIDs (entries in roleEntry.BoundIamPrincipalIDs // implies that roleEntry.ResolveAWSUniqueIDs is true) // 2: roleEntry.ResolveAWSUniqueIDs is false and entity.canonicalArn() is in roleEntry.BoundIamPrincipalARNs // 3: Full ARN matches one of the wildcard globs in roleEntry.BoundIamPrincipalARNs // Need to be able to handle pathological configurations such as roleEntry.BoundIamPrincipalARNs looking something like: // arn:aw:iam::123456789012:{user/UserName,user/path/*,role/RoleName,role/path/*} switch { case strutil.StrListContains(roleEntry.BoundIamPrincipalIDs, callerUniqueId): // check 1 passed case !roleEntry.ResolveAWSUniqueIDs && strutil.StrListContains(roleEntry.BoundIamPrincipalARNs, entity.canonicalArn()): // check 2 passed default: // evaluate check 3 fullArn := b.getCachedUserId(callerUniqueId) if fullArn == "" { fullArn, err = b.fullArn(ctx, entity, req.Storage) if err != nil { return logical.ErrorResponse(fmt.Sprintf("error looking up full ARN of entity %v: %v", entity, err)), nil } if fullArn == "" { return logical.ErrorResponse(fmt.Sprintf("got empty string back when looking up full ARN of entity %v", entity)), nil } b.setCachedUserId(callerUniqueId, fullArn) } matchedWildcardBind := false for _, principalARN := range roleEntry.BoundIamPrincipalARNs { if strings.HasSuffix(principalARN, "*") && strutil.GlobbedStringsMatch(principalARN, fullArn) { matchedWildcardBind = true break } } if !matchedWildcardBind { return logical.ErrorResponse(fmt.Sprintf("IAM Principal %q does not belong to the role %q", callerID.Arn, roleName)), nil } } } inferredEntityType := "" inferredEntityID := "" if roleEntry.InferredEntityType == ec2EntityType { instance, err := b.validateInstance(ctx, req.Storage, entity.SessionInfo, roleEntry.InferredAWSRegion, callerID.Account) if err != nil { return logical.ErrorResponse(fmt.Sprintf("failed to verify %s as a valid EC2 instance in region %s", entity.SessionInfo, roleEntry.InferredAWSRegion)), nil } // build a fake identity doc to pass on metadata about the instance to verifyInstanceMeetsRoleRequirements identityDoc := &identityDocument{ Tags: nil, // Don't really need the tags, so not doing the work of converting them from Instance.Tags to identityDocument.Tags InstanceID: *instance.InstanceId, AmiID: *instance.ImageId, AccountID: callerID.Account, Region: roleEntry.InferredAWSRegion, PendingTime: instance.LaunchTime.Format(time.RFC3339), } validationError, err := b.verifyInstanceMeetsRoleRequirements(ctx, req.Storage, instance, roleEntry, roleName, identityDoc) if err != nil { return nil, err } if validationError != nil { return logical.ErrorResponse(fmt.Sprintf("error validating instance: %s", validationError)), nil } inferredEntityType = ec2EntityType inferredEntityID = entity.SessionInfo } auth := &logical.Auth{ Metadata: map[string]string{ "role_id": roleEntry.RoleID, }, InternalData: map[string]interface{}{ "role_name": roleName, "role_id": roleEntry.RoleID, "canonical_arn": entity.canonicalArn(), "client_user_id": callerUniqueId, "inferred_entity_id": inferredEntityID, "inferred_aws_region": roleEntry.InferredAWSRegion, "account_id": entity.AccountNumber, }, DisplayName: entity.FriendlyName, Alias: &logical.Alias{ Name: identityAlias, }, } roleEntry.PopulateTokenAuth(auth) if err := identityConfigEntry.IAMAuthMetadataHandler.PopulateDesiredMetadata(auth, map[string]string{ "client_arn": callerID.Arn, "canonical_arn": entity.canonicalArn(), "client_user_id": callerUniqueId, "auth_type": iamAuthType, "inferred_entity_type": inferredEntityType, "inferred_entity_id": inferredEntityID, "inferred_aws_region": roleEntry.InferredAWSRegion, "account_id": entity.AccountNumber, }); err != nil { b.Logger().Warn(fmt.Sprintf("unable to set alias metadata due to %s", err)) } return &logical.Response{ Auth: auth, }, nil } // Validate that the iam_request_body passed is valid for the STS request func validateLoginIamRequestBody(body string) error { qs, err := url.ParseQuery(body) if err != nil { return err } for k, v := range qs { switch k { case "Action": if len(v) != 1 || v[0] != "GetCallerIdentity" { return errRequestBodyNotValid } case "Version": // Will assume for now that future versions don't change // the semantics default: // Not expecting any other values return errRequestBodyNotValid } } return nil } // These two methods (hasValuesFor*) return two bools // The first is a hasAll, that is, does the request have all the values // necessary for this auth method // The second is a hasAny, that is, does the request have any of the fields // exclusive to this auth method func hasValuesForEc2Auth(data *framework.FieldData) (bool, bool) { _, hasPkcs7 := data.GetOk("pkcs7") _, hasIdentity := data.GetOk("identity") _, hasSignature := data.GetOk("signature") return (hasPkcs7 || (hasIdentity && hasSignature)), (hasPkcs7 || hasIdentity || hasSignature) } func hasValuesForIamAuth(data *framework.FieldData) (bool, bool) { _, hasRequestMethod := data.GetOk("iam_http_request_method") _, hasRequestURL := data.GetOk("iam_request_url") _, hasRequestBody := data.GetOk("iam_request_body") _, hasRequestHeaders := data.GetOk("iam_request_headers") return (hasRequestMethod && hasRequestURL && hasRequestBody && hasRequestHeaders), (hasRequestMethod || hasRequestURL || hasRequestBody || hasRequestHeaders) } func parseIamArn(iamArn string) (*iamEntity, error) { // iamArn should look like one of the following: // 1. arn:aws:iam:::/ // 2. arn:aws:sts:::assumed-role// // if we get something like 2, then we want to transform that back to what // most people would expect, which is arn:aws:iam:::role/ var entity iamEntity fullParts := strings.Split(iamArn, ":") if len(fullParts) != 6 { return nil, fmt.Errorf("unrecognized arn: contains %d colon-separated parts, expected 6", len(fullParts)) } if fullParts[0] != "arn" { return nil, fmt.Errorf("unrecognized arn: does not begin with \"arn:\"") } // normally aws, but could be aws-cn or aws-us-gov entity.Partition = fullParts[1] if fullParts[2] != "iam" && fullParts[2] != "sts" { return nil, fmt.Errorf("unrecognized service: %v, not one of iam or sts", fullParts[2]) } // fullParts[3] is the region, which doesn't matter for AWS IAM entities entity.AccountNumber = fullParts[4] // fullParts[5] would now be something like user/ or assumed-role// parts := strings.Split(fullParts[5], "/") if len(parts) < 2 { return nil, fmt.Errorf("unrecognized arn: %q contains fewer than 2 slash-separated parts", fullParts[5]) } entity.Type = parts[0] entity.Path = strings.Join(parts[1:len(parts)-1], "/") entity.FriendlyName = parts[len(parts)-1] // now, entity.FriendlyName should either be or switch entity.Type { case "assumed-role": // Check for three parts for assumed role ARNs if len(parts) < 3 { return nil, fmt.Errorf("unrecognized arn: %q contains fewer than 3 slash-separated parts", fullParts[5]) } // Assumed roles don't have paths and have a slightly different format // parts[2] is entity.Path = "" entity.FriendlyName = parts[1] entity.SessionInfo = parts[2] case "user": case "role": case "instance-profile": default: return &iamEntity{}, fmt.Errorf("unrecognized principal type: %q", entity.Type) } return &entity, nil } func validateVaultHeaderValue(headers http.Header, _ *url.URL, requiredHeaderValue string) error { providedValue := "" for k, v := range headers { if strings.EqualFold(iamServerIdHeader, k) { providedValue = strings.Join(v, ",") break } } if providedValue == "" { return fmt.Errorf("missing header %q", iamServerIdHeader) } // NOT doing a constant time compare here since the value is NOT intended to be secret if providedValue != requiredHeaderValue { return fmt.Errorf("expected %q but got %q", requiredHeaderValue, providedValue) } if authzHeaders, ok := headers["Authorization"]; ok { // authzHeader looks like AWS4-HMAC-SHA256 Credential=AKI..., SignedHeaders=host;x-amz-date;x-vault-awsiam-id, Signature=... // We need to extract out the SignedHeaders re := regexp.MustCompile(".*SignedHeaders=([^,]+)") authzHeader := strings.Join(authzHeaders, ",") matches := re.FindSubmatch([]byte(authzHeader)) if len(matches) < 1 { return fmt.Errorf("vault header wasn't signed") } if len(matches) > 2 { return fmt.Errorf("found multiple SignedHeaders components") } signedHeaders := string(matches[1]) return ensureHeaderIsSigned(signedHeaders, iamServerIdHeader) } // TODO: If we support GET requests, then we need to parse the X-Amz-SignedHeaders // argument out of the query string and search in there for the header value return fmt.Errorf("missing Authorization header") } func buildHttpRequest(method, endpoint string, parsedUrl *url.URL, body string, headers http.Header) *http.Request { // This is all a bit complicated because the AWS signature algorithm requires that // the Host header be included in the signed headers. See // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html // The use cases we want to support, in order of increasing complexity, are: // 1. All defaults (client assumes sts.amazonaws.com and server has no override) // 2. Alternate STS regions: client wants to go to a specific region, in which case // Vault must be configured with that endpoint as well. The client's signed request // will include a signature over what the client expects the Host header to be, // so we cannot change that and must match. // 3. Alternate STS regions with a proxy that is transparent to Vault's clients. // In this case, Vault is aware of the proxy, as the proxy is configured as the // endpoint, but the clients should NOT be aware of the proxy (because STS will // not be aware of the proxy) // It's also annoying because: // 1. The AWS Sigv4 algorithm requires the Host header to be defined // 2. Some of the official SDKs (at least botocore and aws-sdk-go) don't actually // include an explicit Host header in the HTTP requests they generate, relying on // the underlying HTTP library to do that for them. // 3. To get a validly signed request, the SDKs check if a Host header has been set // and, if not, add an inferred host header (based on the URI) to the internal // data structure used for calculating the signature, but never actually expose // that to clients. So then they just "hope" that the underlying library actually // adds the right Host header which was included in the signature calculation. // We could either explicitly require all Vault clients to explicitly add the Host header // in the encoded request, or we could also implicitly infer it from the URI. // We choose to support both -- allow you to explicitly set a Host header, but if not, // infer one from the URI. // HOWEVER, we have to preserve the request URI portion of the client's // URL because the GetCallerIdentity Action can be encoded in either the body // or the URL. So, we need to rebuild the URL sent to the http library to have the // custom, Vault-specified endpoint with the client-side request parameters. targetUrl := fmt.Sprintf("%s/%s", endpoint, parsedUrl.RequestURI()) request, err := http.NewRequest(method, targetUrl, strings.NewReader(body)) if err != nil { return nil } request.Host = parsedUrl.Host for k, vals := range headers { for _, val := range vals { request.Header.Add(k, val) } } return request } func ensureHeaderIsSigned(signedHeaders, headerToSign string) error { // Not doing a constant time compare here, the values aren't secret for _, header := range strings.Split(signedHeaders, ";") { if header == strings.ToLower(headerToSign) { return nil } } return fmt.Errorf("vault header wasn't signed") } func parseGetCallerIdentityResponse(response string) (GetCallerIdentityResponse, error) { result := GetCallerIdentityResponse{} response = strings.TrimSpace(response) if !strings.HasPrefix(response, "