Implemented bound_iam_role_arn constraint

This commit is contained in:
vishalnayak 2016-09-23 12:47:35 -04:00
parent 2d4bfeff49
commit bf0b7f218e
5 changed files with 159 additions and 35 deletions

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/vault/helper/salt" "github.com/hashicorp/vault/helper/salt"
"github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework" "github.com/hashicorp/vault/logical/framework"
@ -49,6 +50,12 @@ type backend struct {
// the credentials are modified or deleted, all the cached client objects // the credentials are modified or deleted, all the cached client objects
// will be flushed. // will be flushed.
EC2ClientsMap map[string]*ec2.EC2 EC2ClientsMap map[string]*ec2.EC2
// Map to hold the IAM client objects indexed by region. This avoids
// the overhead of creating a client object for every login request.
// When the credentials are modified or deleted, all the cached client
// objects will be flushed.
IAMClientsMap map[string]*iam.IAM
} }
func Backend(conf *logical.BackendConfig) (*backend, error) { func Backend(conf *logical.BackendConfig) (*backend, error) {
@ -65,6 +72,7 @@ func Backend(conf *logical.BackendConfig) (*backend, error) {
tidyCooldownPeriod: time.Hour, tidyCooldownPeriod: time.Hour,
Salt: salt, Salt: salt,
EC2ClientsMap: make(map[string]*ec2.EC2), EC2ClientsMap: make(map[string]*ec2.EC2),
IAMClientsMap: make(map[string]*iam.IAM),
} }
b.Backend = &framework.Backend{ b.Backend = &framework.Backend{

View File

@ -6,6 +6,7 @@ import (
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault/helper/awsutil" "github.com/hashicorp/vault/helper/awsutil"
"github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical"
@ -61,43 +62,82 @@ func (b *backend) getClientConfig(s logical.Storage, region string) (*aws.Config
// flushCachedEC2Clients deletes all the cached ec2 client objects from the backend. // flushCachedEC2Clients deletes all the cached ec2 client objects from the backend.
// If the client credentials configuration is deleted or updated in the backend, all // If the client credentials configuration is deleted or updated in the backend, all
// the cached EC2 client objects will be flushed. // the cached EC2 client objects will be flushed. Config mutex lock should be
// // acquired for write operation before calling this method.
// Write lock should be acquired using b.configMutex.Lock() before calling this method
// and lock should be released using b.configMutex.Unlock() after the method returns.
func (b *backend) flushCachedEC2Clients() { func (b *backend) flushCachedEC2Clients() {
// deleting items in map during iteration is safe. // deleting items in map during iteration is safe
for region, _ := range b.EC2ClientsMap { for region, _ := range b.EC2ClientsMap {
delete(b.EC2ClientsMap, region) delete(b.EC2ClientsMap, region)
} }
} }
// clientEC2 creates a client to interact with AWS EC2 API. // flushCachedIAMClients deletes all the cached iam client objects from the
// backend. If the client credentials configuration is deleted or updated in
// the backend, all the cached IAM client objects will be flushed. Config mutex
// lock should be acquired for write operation before calling this method.
func (b *backend) flushCachedIAMClients() {
// deleting items in map during iteration is safe
for region, _ := range b.IAMClientsMap {
delete(b.IAMClientsMap, region)
}
}
// clientEC2 creates a client to interact with AWS EC2 API
func (b *backend) clientEC2(s logical.Storage, region string) (*ec2.EC2, error) { func (b *backend) clientEC2(s logical.Storage, region string) (*ec2.EC2, error) {
b.configMutex.RLock() b.configMutex.RLock()
if b.EC2ClientsMap[region] != nil { if b.EC2ClientsMap[region] != nil {
defer b.configMutex.RUnlock() defer b.configMutex.RUnlock()
// If the client object was already created, return it. // If the client object was already created, return it
return b.EC2ClientsMap[region], nil return b.EC2ClientsMap[region], nil
} }
// Release the read lock and acquire the write lock. // Release the read lock and acquire the write lock
b.configMutex.RUnlock() b.configMutex.RUnlock()
b.configMutex.Lock() b.configMutex.Lock()
defer b.configMutex.Unlock() defer b.configMutex.Unlock()
// If the client gets created while switching the locks, return it. // If the client gets created while switching the locks, return it
if b.EC2ClientsMap[region] != nil { if b.EC2ClientsMap[region] != nil {
return b.EC2ClientsMap[region], nil return b.EC2ClientsMap[region], nil
} }
// Create a AWS config object using a chain of providers. // Create an AWS config object using a chain of providers
awsConfig, err := b.getClientConfig(s, region) awsConfig, err := b.getClientConfig(s, region)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Create a new EC2 client object, cache it and return the same. // Create a new EC2 client object, cache it and return the same
b.EC2ClientsMap[region] = ec2.New(session.New(awsConfig)) b.EC2ClientsMap[region] = ec2.New(session.New(awsConfig))
return b.EC2ClientsMap[region], nil return b.EC2ClientsMap[region], nil
} }
// clientIAM creates a client to interact with AWS IAM API
func (b *backend) clientIAM(s logical.Storage, region string) (*iam.IAM, error) {
b.configMutex.RLock()
if b.IAMClientsMap[region] != nil {
defer b.configMutex.RUnlock()
// If the client object was already created, return it
return b.IAMClientsMap[region], nil
}
// Release the read lock and acquire the write lock
b.configMutex.RUnlock()
b.configMutex.Lock()
defer b.configMutex.Unlock()
// If the client gets created while switching the locks, return it
if b.IAMClientsMap[region] != nil {
return b.IAMClientsMap[region], nil
}
// Create an AWS config object using a chain of providers
awsConfig, err := b.getClientConfig(s, region)
if err != nil {
return nil, err
}
// Create a new IAM client object, cache it and return the same
b.IAMClientsMap[region] = iam.New(session.New(awsConfig))
return b.IAMClientsMap[region], nil
}

View File

@ -108,6 +108,9 @@ func (b *backend) pathConfigClientDelete(
// Remove all the cached EC2 client objects in the backend. // Remove all the cached EC2 client objects in the backend.
b.flushCachedEC2Clients() b.flushCachedEC2Clients()
// Remove all the cached EC2 client objects in the backend.
b.flushCachedIAMClients()
return nil, nil return nil, nil
} }
@ -175,6 +178,7 @@ func (b *backend) pathConfigClientCreateUpdate(
if changedCreds { if changedCreds {
b.flushCachedEC2Clients() b.flushCachedEC2Clients()
b.flushCachedIAMClients()
} }
return nil, nil return nil, nil

View File

@ -4,10 +4,12 @@ import (
"crypto/subtle" "crypto/subtle"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/fullsailor/pkcs7" "github.com/fullsailor/pkcs7"
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/jsonutil" "github.com/hashicorp/vault/helper/jsonutil"
@ -61,6 +63,39 @@ on either the role or the role tag, the 'nonce' holds no significance.`,
} }
} }
// instanceIamRoleARN fetches the IAM role ARN associated with the given
// instance profile name
func (b *backend) instanceIamRoleARN(s logical.Storage, instanceProfileName, region string) (string, error) {
iamClient, err := b.clientIAM(s, region)
if err != nil {
return "", err
}
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 // validateInstance queries the status of the EC2 instance using AWS EC2 API and
// checks if the instance is running and is healthy. // checks if the instance is running and is healthy.
func (b *backend) validateInstance(s logical.Storage, instanceID, region string) (*ec2.DescribeInstancesOutput, error) { func (b *backend) validateInstance(s logical.Storage, instanceID, region string) (*ec2.DescribeInstancesOutput, error) {
@ -83,6 +118,9 @@ func (b *backend) validateInstance(s logical.Storage, instanceID, region string)
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching description for instance ID %s: %s\n", instanceID, err) return nil, fmt.Errorf("error fetching description for instance ID %s: %s\n", instanceID, err)
} }
if status == nil {
return nil, fmt.Errorf("nil output from describe instances")
}
if len(status.Reservations) == 0 { if len(status.Reservations) == 0 {
return nil, fmt.Errorf("no reservations found in instance description") return nil, fmt.Errorf("no reservations found in instance description")
@ -284,9 +322,41 @@ func (b *backend) pathLoginUpdate(
if instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn == nil { if instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn == nil {
return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil") return nil, fmt.Errorf("IAM instance profile ARN in the instance description is nil")
} }
iamInstanceProfileArn := *instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn iamInstanceProfileARN := *instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn
if iamInstanceProfileArn != roleEntry.BoundIamInstanceProfileARN { if iamInstanceProfileARN != roleEntry.BoundIamInstanceProfileARN {
return logical.ErrorResponse(fmt.Sprintf("IAM instance profile ARN %q does not satisfy the constraint role %q", iamInstanceProfileArn, roleName)), nil return logical.ErrorResponse(fmt.Sprintf("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 instanceDesc.Reservations[0].Instances[0].IamInstanceProfile == nil {
return nil, fmt.Errorf("IAM instance profile in the instance description is nil")
}
if instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Id == nil {
return nil, fmt.Errorf("IAM instance profile identifier in the instance description is nil")
}
// Fetch the instance profile ARN from the instance description
iamInstanceProfileARN := *instanceDesc.Reservations[0].Instances[0].IamInstanceProfile.Arn
// Extract out the instance profile name from the instance
// profile ARN
iamInstanceProfileARNSlice := strings.SplitAfter(iamInstanceProfileARN, ":instance-profile/")
iamInstanceProfileName := iamInstanceProfileARNSlice[len(iamInstanceProfileARNSlice)-1]
// Use instance profile ARN to fetch the associated role ARN
iamRoleARN, err := b.instanceIamRoleARN(req.Storage, iamInstanceProfileName, identityDoc.Region)
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 iamRoleARN != roleEntry.BoundIamRoleARN {
return logical.ErrorResponse(fmt.Sprintf("IAM role ARN %q does not satisfy the constraint role %q", iamRoleARN, roleName)), nil
} }
} }

View File

@ -29,6 +29,11 @@ using the AMI ID specified by this parameter.`,
Type: framework.TypeString, Type: framework.TypeString,
Description: `If set, defines a constraint on the EC2 instances that the account ID Description: `If set, defines a constraint on the EC2 instances that the account ID
in its identity document to match the one specified by this parameter.`, in its identity document to match the one specified by this parameter.`,
},
"bound_iam_role_arn": {
Type: framework.TypeString,
Description: `If set, defines a constraint on the EC2 instances that they should be using the
IAM role ARN specified by this parameter.`,
}, },
"bound_iam_instance_profile_arn": { "bound_iam_instance_profile_arn": {
Type: framework.TypeString, Type: framework.TypeString,
@ -194,22 +199,22 @@ func (b *backend) nonLockedAWSRole(s logical.Storage, roleName string) (*awsRole
return nil, err return nil, err
} }
// Upgrade code to use proper field for bound_iam_instance_profile_arn // Check if the value held by role ARN field is actually an instance profile ARN
if result.DeprecatedBoundIamARN != "" { if result.BoundIamRoleARN != "" && strings.Contains(result.BoundIamRoleARN, ":instance-profile/") {
// For sanity // For sanity
if result.BoundIamInstanceProfileARN != "" { if result.BoundIamInstanceProfileARN != "" {
return nil, fmt.Errorf("both bound_iam_role_arn and bound_iam_instance_profile_arn are set") return nil, fmt.Errorf("bound_iam_role_arn contains instance profile ARN and bound_iam_instance_profile_arn is non empty")
} }
// Fill in the new field // If yes, move it to the correct field
result.BoundIamInstanceProfileARN = result.DeprecatedBoundIamARN result.BoundIamInstanceProfileARN = result.BoundIamRoleARN
// Reset the old field // Reset the old field
result.DeprecatedBoundIamARN = "" result.BoundIamRoleARN = ""
// Save the update // Save the update
if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil { if err = b.nonLockedSetAWSRole(s, roleName, &result); err != nil {
return nil, fmt.Errorf("failed to upgrade bound_iam_role_arn to bound_iam_instance_profile_arn") return nil, fmt.Errorf("failed to move instance profile ARN to bound_iam_instance_profile_arn field")
} }
} }
@ -246,8 +251,6 @@ func (b *backend) pathRoleList(
// pathRoleRead is used to view the information registered for a given AMI ID. // pathRoleRead is used to view the information registered for a given AMI ID.
func (b *backend) pathRoleRead( func (b *backend) pathRoleRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) { req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
resp := &logical.Response{}
roleEntry, err := b.lockedAWSRole(req.Storage, strings.ToLower(data.Get("role").(string))) roleEntry, err := b.lockedAWSRole(req.Storage, strings.ToLower(data.Get("role").(string)))
if err != nil { if err != nil {
return nil, err return nil, err
@ -267,15 +270,9 @@ func (b *backend) pathRoleRead(
// Display the max_ttl in seconds. // Display the max_ttl in seconds.
respData["max_ttl"] = roleEntry.MaxTTL / time.Second respData["max_ttl"] = roleEntry.MaxTTL / time.Second
// To be removed in the coming releases return &logical.Response{
if respData["bound_iam_instance_profile_arn"] != "" { Data: respData,
respData["bound_iam_role_arn"] = respData["bound_iam_instance_profile_arn"] }, nil
resp.AddWarning("The field bound_iam_role_arn is deprecated and will be removed in future releases; refer bound_iam_instance_profile_arn instead.")
}
resp.Data = respData
return resp, nil
} }
// pathRoleCreateUpdate is used to associate Vault policies to a given AMI ID. // pathRoleCreateUpdate is used to associate Vault policies to a given AMI ID.
@ -298,16 +295,20 @@ func (b *backend) pathRoleCreateUpdate(
roleEntry = &awsRoleEntry{} roleEntry = &awsRoleEntry{}
} }
// Set BoundAmiID only if it is supplied. There can't be a default value. // Fetch and set the bound parameters. There can't be default values
// for these.
if boundAmiIDRaw, ok := data.GetOk("bound_ami_id"); ok { if boundAmiIDRaw, ok := data.GetOk("bound_ami_id"); ok {
roleEntry.BoundAmiID = boundAmiIDRaw.(string) roleEntry.BoundAmiID = boundAmiIDRaw.(string)
} }
// Set BoundAccountID only if it is supplied. There can't be a default value.
if boundAccountIDRaw, ok := data.GetOk("bound_account_id"); ok { if boundAccountIDRaw, ok := data.GetOk("bound_account_id"); ok {
roleEntry.BoundAccountID = boundAccountIDRaw.(string) roleEntry.BoundAccountID = boundAccountIDRaw.(string)
} }
if boundIamRoleARNRaw, ok := data.GetOk("bound_iam_role_arn"); ok {
roleEntry.BoundIamRoleARN = boundIamRoleARNRaw.(string)
}
if boundIamInstanceProfileARNRaw, ok := data.GetOk("bound_iam_instance_profile_arn"); ok { if boundIamInstanceProfileARNRaw, ok := data.GetOk("bound_iam_instance_profile_arn"); ok {
roleEntry.BoundIamInstanceProfileARN = boundIamInstanceProfileARNRaw.(string) roleEntry.BoundIamInstanceProfileARN = boundIamInstanceProfileARNRaw.(string)
} }
@ -317,6 +318,7 @@ func (b *backend) pathRoleCreateUpdate(
case roleEntry.BoundAccountID != "": case roleEntry.BoundAccountID != "":
case roleEntry.BoundAmiID != "": case roleEntry.BoundAmiID != "":
case roleEntry.BoundIamInstanceProfileARN != "": case roleEntry.BoundIamInstanceProfileARN != "":
case roleEntry.BoundIamRoleARN != "":
default: default:
return logical.ErrorResponse("at least be one bound parameter should be specified on the role"), nil return logical.ErrorResponse("at least be one bound parameter should be specified on the role"), nil
@ -412,7 +414,7 @@ func (b *backend) pathRoleCreateUpdate(
type awsRoleEntry struct { type awsRoleEntry struct {
BoundAmiID string `json:"bound_ami_id" structs:"bound_ami_id" mapstructure:"bound_ami_id"` BoundAmiID string `json:"bound_ami_id" structs:"bound_ami_id" mapstructure:"bound_ami_id"`
BoundAccountID string `json:"bound_account_id" structs:"bound_account_id" mapstructure:"bound_account_id"` BoundAccountID string `json:"bound_account_id" structs:"bound_account_id" mapstructure:"bound_account_id"`
DeprecatedBoundIamARN string `json:"bound_iam_role_arn" structs:"bound_iam_role_arn" mapstructure:"bound_iam_role_arn"` BoundIamRoleARN string `json:"bound_iam_role_arn" structs:"bound_iam_role_arn" mapstructure:"bound_iam_role_arn"`
BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn" structs:"bound_iam_instance_profile_arn" mapstructure:"bound_iam_instance_profile_arn"` BoundIamInstanceProfileARN string `json:"bound_iam_instance_profile_arn" structs:"bound_iam_instance_profile_arn" mapstructure:"bound_iam_instance_profile_arn"`
RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` RoleTag string `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"`
AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"` AllowInstanceMigration bool `json:"allow_instance_migration" structs:"allow_instance_migration" mapstructure:"allow_instance_migration"`