mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	AWS EC2 instances authentication backend
This commit is contained in:
		
							
								
								
									
										70
									
								
								builtin/credential/aws/backend.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								builtin/credential/aws/backend.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"github.com/hashicorp/vault/helper/salt" | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| func Factory(conf *logical.BackendConfig) (logical.Backend, error) { | ||||
| 	b, err := Backend(conf) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return b.Setup(conf) | ||||
| } | ||||
|  | ||||
| func Backend(conf *logical.BackendConfig) (*framework.Backend, error) { | ||||
| 	salt, err := salt.NewSalt(conf.StorageView, &salt.Config{ | ||||
| 		HashFunc: salt.SHA256Hash, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	var b backend | ||||
| 	b.Salt = salt | ||||
| 	b.Backend = &framework.Backend{ | ||||
| 		Help: backendHelp, | ||||
|  | ||||
| 		PathsSpecial: &logical.Paths{ | ||||
| 			Unauthenticated: []string{ | ||||
| 				"login", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Paths: append([]*framework.Path{ | ||||
| 			pathLogin(&b), | ||||
| 			pathImage(&b), | ||||
| 			pathListImages(&b), | ||||
| 			pathImageTag(&b), | ||||
| 			pathConfigClient(&b), | ||||
| 			pathConfigCertificate(&b), | ||||
| 			pathBlacklistRoleTag(&b), | ||||
| 			pathListBlacklistRoleTags(&b), | ||||
| 			pathBlacklistRoleTagTidy(&b), | ||||
| 			pathWhitelistIdentity(&b), | ||||
| 			pathWhitelistIdentityTidy(&b), | ||||
| 			pathListWhitelistIdentities(&b), | ||||
| 		}), | ||||
|  | ||||
| 		AuthRenew: b.pathLoginRenew, | ||||
| 	} | ||||
|  | ||||
| 	return b.Backend, nil | ||||
| } | ||||
|  | ||||
| type backend struct { | ||||
| 	*framework.Backend | ||||
| 	Salt *salt.Salt | ||||
| } | ||||
|  | ||||
| const backendHelp = ` | ||||
| AWS auth backend takes in a AWS EC2 instance identity document, its PKCS#7 signature | ||||
| and a client created nonce to authenticates the instance with Vault. | ||||
|  | ||||
| Authentication is backed by a preconfigured association of AMIs to Vault's policies | ||||
| through 'image/<name>' endpoint. For instances that share an AMI, an instance tag can | ||||
| be created through 'image/<name>/tag'. This tag should be attached to the EC2 instance | ||||
| before the instance attempts to login to Vault. | ||||
| ` | ||||
							
								
								
									
										68
									
								
								builtin/credential/aws/backend_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								builtin/credential/aws/backend_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	logicaltest "github.com/hashicorp/vault/logical/testing" | ||||
| ) | ||||
|  | ||||
| func TestBackend_ConfigClient(t *testing.T) { | ||||
| 	config := logical.TestBackendConfig() | ||||
| 	storageView := &logical.InmemStorage{} | ||||
| 	config.StorageView = storageView | ||||
|  | ||||
| 	b, err := Factory(config) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	logicaltest.Test(t, logicaltest.TestCase{ | ||||
| 		AcceptanceTest: false, | ||||
| 		Backend:        b, | ||||
| 		Steps:          []logicaltest.TestStep{}, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestBackend_parseRoleTagValue(t *testing.T) { | ||||
| 	tag := "v1:XwuKhyyBNJc=:a=ami-fce3c696:p=root:t=3h0m0s:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" | ||||
| 	expected := roleTag{ | ||||
| 		Version:  "v1", | ||||
| 		Nonce:    "XwuKhyyBNJc=", | ||||
| 		Policies: []string{"root"}, | ||||
| 		MaxTTL:   10800000000000, | ||||
| 		ImageID:  "ami-fce3c696", | ||||
| 		HMAC:     "lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=", | ||||
| 	} | ||||
| 	actual, err := parseRoleTagValue(tag) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
| 	if !actual.Equal(&expected) { | ||||
| 		t.Fatalf("err: expected:%#v \ngot: %#v\n", expected, actual) | ||||
| 	} | ||||
|  | ||||
| 	tag = "v2:XwuKhyyBNJc=:a=ami-fce3c696:p=root:t=3h0m0s:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" | ||||
| 	actual, err = parseRoleTagValue(tag) | ||||
| 	if err == nil { | ||||
| 		t.Fatalf("err: expected error due to invalid role tag version", err) | ||||
| 	} | ||||
|  | ||||
| 	tag = "v1:XwuKhyyBNJc=:a=ami-fce3c696:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" | ||||
| 	expected = roleTag{ | ||||
| 		Version: "v1", | ||||
| 		Nonce:   "XwuKhyyBNJc=", | ||||
| 		ImageID: "ami-fce3c696", | ||||
| 		HMAC:    "lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=", | ||||
| 	} | ||||
| 	actual, err = parseRoleTagValue(tag) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("err: %s", err) | ||||
| 	} | ||||
|  | ||||
| 	tag = "v1:XwuKhyyBNJc=:p=ami-fce3c696:lhvKJAZn8kxNwmPFnyXzmphQTtbXqQe6WG6sLiIf3dQ=" | ||||
| 	actual, err = parseRoleTagValue(tag) | ||||
| 	if err == nil { | ||||
| 		t.Fatalf("err: expected error due to missing image ID", err) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										48
									
								
								builtin/credential/aws/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								builtin/credential/aws/client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/aws/aws-sdk-go/aws" | ||||
| 	"github.com/aws/aws-sdk-go/aws/credentials" | ||||
| 	"github.com/aws/aws-sdk-go/aws/session" | ||||
| 	"github.com/aws/aws-sdk-go/service/ec2" | ||||
| 	"github.com/hashicorp/go-cleanhttp" | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| ) | ||||
|  | ||||
| // getClientConfig creates a aws-sdk-go config, which is used to create | ||||
| // client that can interact with AWS API. This reads out the secret key | ||||
| // and access key that was configured via 'config/client' endpoint and | ||||
| // uses them to create credentials required to make the AWS API calls. | ||||
| func getClientConfig(s logical.Storage) (*aws.Config, error) { | ||||
| 	// Read the configured secret key and access key | ||||
| 	config, err := clientConfigEntry(s) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if config == nil { | ||||
| 		return nil, fmt.Errorf( | ||||
| 			"client credentials haven't been configured. Please configure\n" + | ||||
| 				"them at the 'config/client' endpoint") | ||||
| 	} | ||||
|  | ||||
| 	// Create the credentials required to access the API. | ||||
| 	creds := credentials.NewStaticCredentials(config.AccessKey, config.SecretKey, "") | ||||
|  | ||||
| 	// Create a config that can be used to make the API calls. | ||||
| 	return &aws.Config{ | ||||
| 		Credentials: creds, | ||||
| 		Region:      aws.String(config.Region), | ||||
| 		HTTPClient:  cleanhttp.DefaultClient(), | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // clientEC2 creates a client to interact with AWS EC2 API. | ||||
| func clientEC2(s logical.Storage) (*ec2.EC2, error) { | ||||
| 	awsConfig, err := getClientConfig(s) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return ec2.New(session.New(awsConfig)), nil | ||||
| } | ||||
							
								
								
									
										245
									
								
								builtin/credential/aws/path_blacklist_roletag.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								builtin/credential/aws/path_blacklist_roletag.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| func pathBlacklistRoleTag(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "blacklist/roletag$", | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"role_tag": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "Role tag that needs be blacklisted", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		ExistenceCheck: b.pathBlacklistRoleTagExistenceCheck, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.UpdateOperation: b.pathBlacklistRoleTagUpdate, | ||||
| 			logical.ReadOperation:   b.pathBlacklistRoleTagRead, | ||||
| 			logical.DeleteOperation: b.pathBlacklistRoleTagDelete, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathBlacklistRoleTagSyn, | ||||
| 		HelpDescription: pathBlacklistRoleTagDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Path to list all the blacklisted tags. | ||||
| func pathListBlacklistRoleTags(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "blacklist/roletags/?", | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.ListOperation: b.pathBlacklistRoleTagsList, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathListBlacklistRoleTagsHelpSyn, | ||||
| 		HelpDescription: pathListBlacklistRoleTagsHelpDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Lists all the blacklisted role tags. | ||||
| func (b *backend) pathBlacklistRoleTagsList( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	tags, err := req.Storage.List("blacklist/roletag/") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return logical.ListResponse(tags), nil | ||||
| } | ||||
|  | ||||
| // Establishes dichotomy of request operation between CreateOperation and UpdateOperation. | ||||
| // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. | ||||
| // | ||||
| // A role should be allowed to be blacklisted even if it was prevously blacklisted. | ||||
| func (b *backend) pathBlacklistRoleTagExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { | ||||
| 	return true, nil | ||||
| } | ||||
|  | ||||
| // Fetch an un-expired entry from the role tag blacklist for a given tag. | ||||
| func blacklistRoleTagValidEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { | ||||
| 	entry, err := blacklistRoleTagEntry(s, tag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Exclude the item if it is expired. | ||||
| 	if entry == nil || time.Now().After(entry.ExpirationTime) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return entry, nil | ||||
| } | ||||
|  | ||||
| // Fetch an entry from the role tag blacklist for a given tag. | ||||
| func blacklistRoleTagEntry(s logical.Storage, tag string) (*roleTagBlacklistEntry, error) { | ||||
| 	entry, err := s.Get("blacklist/roletag/" + tag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	var result roleTagBlacklistEntry | ||||
| 	if err := entry.DecodeJSON(&result); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // Deletes an entry from the role tag blacklist for a given tag. | ||||
| func (b *backend) pathBlacklistRoleTagDelete( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	tag := data.Get("role_tag").(string) | ||||
| 	if tag == "" { | ||||
| 		return logical.ErrorResponse("missing role_tag"), nil | ||||
| 	} | ||||
|  | ||||
| 	err := req.Storage.Delete("blacklist/roletag/" + tag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| // If the given role tag is blacklisted, returns the details of the blacklist entry. | ||||
| // Returns 'nil' otherwise. | ||||
| func (b *backend) pathBlacklistRoleTagRead( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	tag := data.Get("role_tag").(string) | ||||
| 	if tag == "" { | ||||
| 		return logical.ErrorResponse("missing role_tag"), nil | ||||
| 	} | ||||
|  | ||||
| 	entry, err := blacklistRoleTagEntry(req.Storage, tag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"creation_time":   entry.CreationTime, | ||||
| 			"expiration_time": entry.ExpirationTime, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // pathBlacklistRoleTagUpdate is used to blacklist a given role tag. | ||||
| // Before a role tag is blacklisted, the correctness of the plaintext part | ||||
| // in the role tag is verified using the associated HMAC. | ||||
| func (b *backend) pathBlacklistRoleTagUpdate( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	tag := data.Get("role_tag").(string) | ||||
| 	if tag == "" { | ||||
| 		return logical.ErrorResponse("missing role_tag"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Parse the role tag from string form to a struct form. | ||||
| 	rTag, err := parseRoleTagValue(tag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Build the plaintext form of the role tag and verify the prepared | ||||
| 	// value using the HMAC. | ||||
| 	verified, err := verifyRoleTagValue(req.Storage, rTag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if !verified { | ||||
| 		return logical.ErrorResponse("role tag invalid"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Get the entry for the AMI used by the instance. | ||||
| 	imageEntry, err := awsImage(req.Storage, rTag.ImageID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if imageEntry == nil { | ||||
| 		return logical.ErrorResponse("image entry not found"), nil | ||||
| 	} | ||||
|  | ||||
| 	blEntry, err := blacklistRoleTagEntry(req.Storage, tag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if blEntry == nil { | ||||
| 		blEntry = &roleTagBlacklistEntry{} | ||||
| 	} | ||||
|  | ||||
| 	currentTime := time.Now() | ||||
|  | ||||
| 	var epoch time.Time | ||||
| 	if blEntry.CreationTime.Equal(epoch) { | ||||
| 		// Set the creation time for the blacklist entry. | ||||
| 		// This should not be updated after setting it once. | ||||
| 		// If blacklist operation is invoked more than once, only update the expiration time. | ||||
| 		blEntry.CreationTime = currentTime | ||||
| 	} | ||||
|  | ||||
| 	// If max_ttl is not set for the role tag, fall back on the mount's max_ttl. | ||||
| 	if rTag.MaxTTL == time.Duration(0) { | ||||
| 		rTag.MaxTTL = b.System().MaxLeaseTTL() | ||||
| 	} | ||||
|  | ||||
| 	if imageEntry.MaxTTL > time.Duration(0) && rTag.MaxTTL > imageEntry.MaxTTL { | ||||
| 		rTag.MaxTTL = imageEntry.MaxTTL | ||||
| 	} | ||||
|  | ||||
| 	// Expiration time is decided by the max_ttl value. | ||||
| 	blEntry.ExpirationTime = currentTime.Add(rTag.MaxTTL) | ||||
|  | ||||
| 	entry, err := logical.StorageEntryJSON("blacklist/roletag/"+tag, blEntry) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Store it. | ||||
| 	if err := req.Storage.Put(entry); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| type roleTagBlacklistEntry struct { | ||||
| 	CreationTime   time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"` | ||||
| 	ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"` | ||||
| } | ||||
|  | ||||
| const pathBlacklistRoleTagSyn = ` | ||||
| Blacklist a previously created role tag. | ||||
| ` | ||||
|  | ||||
| const pathBlacklistRoleTagDesc = ` | ||||
| Blacklist a role tag so that it cannot be used by an EC2 instance to perform logins | ||||
| in the future. This can be used if the role tag is suspected or believed to be possessed | ||||
| by an unauthorized entity. | ||||
|  | ||||
| The entries in the blacklist are not automatically deleted. Although, they will have an | ||||
| expiration time set on the entry. There is a separate endpoint 'blacklist/roletag/tidy', | ||||
| that needs to be invoked to clean-up all the expired entries in the blacklist. | ||||
| ` | ||||
|  | ||||
| const pathListBlacklistRoleTagsHelpSyn = ` | ||||
| List the blacklisted role tags. | ||||
| ` | ||||
|  | ||||
| const pathListBlacklistRoleTagsHelpDesc = ` | ||||
| List all the entries present in the blacklist. This will show both the valid entries and | ||||
| the expired entries in the blacklist. Use 'blacklist/roletag/tidy' endpoint to clean-up | ||||
| the blacklist of role tags. | ||||
| ` | ||||
							
								
								
									
										88
									
								
								builtin/credential/aws/path_blacklist_roletag_tidy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								builtin/credential/aws/path_blacklist_roletag_tidy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| func pathBlacklistRoleTagTidy(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "blacklist/roletag/tidy$", | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"safety_buffer": &framework.FieldSchema{ | ||||
| 				Type:    framework.TypeDurationSecond, | ||||
| 				Default: 259200, // 72h | ||||
| 				Description: `The amount of extra time that must have passed beyond the roletag's | ||||
| expiration, before it is removed from the backend storage.`, | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.UpdateOperation: b.pathBlacklistRoleTagTidyUpdate, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathBlacklistRoleTagTidySyn, | ||||
| 		HelpDescription: pathBlacklistRoleTagTidyDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // pathBlacklistRoleTagTidyUpdate is used to clean-up the entries in the role tag blacklist. | ||||
| func (b *backend) pathBlacklistRoleTagTidyUpdate( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	// safety_buffer is an optional parameter. | ||||
| 	safety_buffer := data.Get("safety_buffer").(int) | ||||
| 	bufferDuration := time.Duration(safety_buffer) * time.Second | ||||
|  | ||||
| 	tags, err := req.Storage.List("blacklist/roletag/") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for _, tag := range tags { | ||||
| 		tagEntry, err := req.Storage.Get("blacklist/roletag/" + tag) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error fetching tag %s: %s", tag, err) | ||||
| 		} | ||||
|  | ||||
| 		if tagEntry == nil { | ||||
| 			return nil, fmt.Errorf("tag entry for tag %s is nil", tag) | ||||
| 		} | ||||
|  | ||||
| 		if tagEntry.Value == nil || len(tagEntry.Value) == 0 { | ||||
| 			return nil, fmt.Errorf("found entry for tag %s but actual tag is empty", tag) | ||||
| 		} | ||||
|  | ||||
| 		var result roleTagBlacklistEntry | ||||
| 		if err := tagEntry.DecodeJSON(&result); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		if time.Now().After(result.ExpirationTime.Add(bufferDuration)) { | ||||
| 			if err := req.Storage.Delete("blacklist/roletag" + tag); err != nil { | ||||
| 				return nil, fmt.Errorf("error deleting tag %s from storage: %s", tag, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| const pathBlacklistRoleTagTidySyn = ` | ||||
| Clean-up the blacklisted role tag entries. | ||||
| ` | ||||
|  | ||||
| const pathBlacklistRoleTagTidyDesc = ` | ||||
| When a role tag is blacklisted, the expiration time of the blacklist entry is | ||||
| determined by the 'max_ttl' present in the role tag. If 'max_ttl' is not provided | ||||
| in the role tag, the backend mount's 'max_ttl' value will be used to determine | ||||
| the expiration time of the blacklist entry. | ||||
|  | ||||
| When this endpoint is invoked all the entries that are expired will be deleted. | ||||
|  | ||||
| A 'safety_buffer' (duration in seconds) can be provided, to ensure deletion of | ||||
| only those entries that are expired before 'safety_buffer' seconds.  | ||||
| ` | ||||
							
								
								
									
										245
									
								
								builtin/credential/aws/path_config_certificate.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										245
									
								
								builtin/credential/aws/path_config_certificate.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,245 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"crypto" | ||||
| 	"crypto/dsa" | ||||
| 	"crypto/x509" | ||||
| 	"encoding/asn1" | ||||
| 	"encoding/base64" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| // dsaSignature represents the contents of the signature of a signed | ||||
| // content using digital signature algorithm. | ||||
| type dsaSignature struct { | ||||
| 	R, S *big.Int | ||||
| } | ||||
|  | ||||
| // As per AWS documentation, this public key is valid for US East (N. Virginia), | ||||
| // US West (Oregon), US West (N. California), EU (Ireland), EU (Frankfurt), | ||||
| // Asia Pacific (Tokyo), Asia Pacific (Seoul), Asia Pacific (Singapore), | ||||
| // Asia Pacific (Sydney), and South America (Sao Paulo) | ||||
| const defaultAWSPublicCert = ` | ||||
| -----BEGIN CERTIFICATE----- | ||||
| MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw | ||||
| FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD | ||||
| VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z | ||||
| ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u | ||||
| IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl | ||||
| cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e | ||||
| ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3 | ||||
| VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P | ||||
| hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j | ||||
| k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U | ||||
| hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF | ||||
| lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf | ||||
| MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW | ||||
| MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw | ||||
| vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw | ||||
| 7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K | ||||
| -----END CERTIFICATE----- | ||||
| ` | ||||
|  | ||||
| func pathConfigCertificate(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "config/certificate$", | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"aws_public_cert": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Default:     defaultAWSPublicCert, | ||||
| 				Description: "AWS Public key required to verify PKCS7 signature.", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		ExistenceCheck: b.pathConfigCertificateExistenceCheck, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.CreateOperation: b.pathConfigCertificateCreateUpdate, | ||||
| 			logical.UpdateOperation: b.pathConfigCertificateCreateUpdate, | ||||
| 			logical.ReadOperation:   b.pathConfigCertificateRead, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathConfigCertificateSyn, | ||||
| 		HelpDescription: pathConfigCertificateDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Establishes dichotomy of request operation between CreateOperation and UpdateOperation. | ||||
| // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. | ||||
| func (b *backend) pathConfigCertificateExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { | ||||
| 	entry, err := awsPublicCertificateEntry(req.Storage) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	return entry != nil, nil | ||||
| } | ||||
|  | ||||
| // Decodes the PEM encoded certiticate and parses it into a x509 cert. | ||||
| func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error) { | ||||
| 	// Decode the PEM block and error out if a block is not detected in the first attempt. | ||||
| 	decodedPublicCert, rest := pem.Decode([]byte(certificate)) | ||||
| 	if len(rest) != 0 { | ||||
| 		return nil, fmt.Errorf("invalid certificate; failed to decode certificate") | ||||
| 	} | ||||
|  | ||||
| 	// Check if the certificate can be parsed. | ||||
| 	publicCert, err := x509.ParseCertificate(decodedPublicCert.Bytes) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if publicCert == nil { | ||||
| 		return nil, fmt.Errorf("invalid certificate; failed to parse certificate") | ||||
| 	} | ||||
| 	return publicCert, nil | ||||
| } | ||||
|  | ||||
| // awsPublicCertificateParsed will fetch the storage entry for the certificate, | ||||
| // decodes it and returns the parsed certificate. | ||||
| func awsPublicCertificateParsed(s logical.Storage) (*x509.Certificate, error) { | ||||
| 	certEntry, err := awsPublicCertificateEntry(s) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if certEntry == nil { | ||||
| 		return decodePEMAndParseCertificate(defaultAWSPublicCert) | ||||
| 	} | ||||
| 	return decodePEMAndParseCertificate(certEntry.AWSPublicCert) | ||||
| } | ||||
|  | ||||
| // awsPublicCertificate is used to get the configured AWS Public Key that is used | ||||
| // to verify the PKCS#7 signature of the instance identity document. | ||||
| func awsPublicCertificateEntry(s logical.Storage) (*awsPublicCert, error) { | ||||
| 	entry, err := s.Get("config/certificate") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		// Existence check depends on this being nil when the storage entry is not present. | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	var result awsPublicCert | ||||
| 	if err := entry.DecodeJSON(&result); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // pathConfigCertificateRead is used to view the configured AWS Public Key that is | ||||
| // used to verify the PKCS#7 signature of the instance identity document. | ||||
| func (b *backend) pathConfigCertificateRead( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	certificateEntry, err := awsPublicCertificateEntry(req.Storage) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if certificateEntry == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"aws_public_cert": certificateEntry.AWSPublicCert, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // pathConfigCertificateCreateUpdate is used to register an AWS Public Key that is | ||||
| // used to verify the PKCS#7 signature of the instance identity document. | ||||
| func (b *backend) pathConfigCertificateCreateUpdate( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	// Check if there is already a certificate entry registered. | ||||
| 	certEntry, err := awsPublicCertificateEntry(req.Storage) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if certEntry == nil { | ||||
| 		certEntry = &awsPublicCert{} | ||||
| 	} | ||||
|  | ||||
| 	// Check if the value is provided by the client. | ||||
| 	certStrB64, ok := data.GetOk("aws_public_cert") | ||||
| 	if ok { | ||||
| 		certBytes, err := base64.StdEncoding.DecodeString(certStrB64.(string)) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		certEntry.AWSPublicCert = string(certBytes) | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		certEntry.AWSPublicCert = data.Get("aws_public_cert").(string) | ||||
| 	} | ||||
|  | ||||
| 	// If explicitly set to empty string, error out. | ||||
| 	if certEntry.AWSPublicCert == "" { | ||||
| 		return logical.ErrorResponse("missing aws_public_cert"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Verify the certificate by decoding it and parsing it. | ||||
| 	publicCert, err := decodePEMAndParseCertificate(certEntry.AWSPublicCert) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if publicCert == nil { | ||||
| 		return logical.ErrorResponse("invalid certificate; failed to decode and parse certificate"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Before trusting the signature provided, validate its signature. | ||||
|  | ||||
| 	// Extract the signature of the certificate. | ||||
| 	dsaSig := &dsaSignature{} | ||||
| 	dsaSigRest, err := asn1.Unmarshal(publicCert.Signature, dsaSig) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(dsaSigRest) != 0 { | ||||
| 		return nil, fmt.Errorf("failed to unmarshal certificate's signature") | ||||
| 	} | ||||
|  | ||||
| 	certHashFunc := crypto.SHA1.New() | ||||
|  | ||||
| 	// RawTBSCertificate will contain the information in the certificate that is signed. | ||||
| 	certHashFunc.Write(publicCert.RawTBSCertificate) | ||||
|  | ||||
| 	// Verify the signature using the public key present in the certificate. | ||||
| 	if !dsa.Verify(publicCert.PublicKey.(*dsa.PublicKey), certHashFunc.Sum(nil), dsaSig.R, dsaSig.S) { | ||||
| 		return logical.ErrorResponse("invalid certificate; failed to verify certificate's signature"), nil | ||||
| 	} | ||||
|  | ||||
| 	// If none of the checks fail, save the provided certificate. | ||||
| 	entry, err := logical.StorageEntryJSON("config/certificate", certEntry) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := req.Storage.Put(entry); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| // Struct awsPublicCert holds the AWS Public Key that is used to verify the PKCS#7 signature | ||||
| // of the instnace identity document. | ||||
| type awsPublicCert struct { | ||||
| 	AWSPublicCert string `json:"aws_public_cert" structs:"aws_public_cert" mapstructure:"aws_public_cert"` | ||||
| } | ||||
|  | ||||
| const pathConfigCertificateSyn = ` | ||||
| Configure the AWS Public Key that is used to verify the PKCS#7 signature of the identidy document. | ||||
| ` | ||||
|  | ||||
| const pathConfigCertificateDesc = ` | ||||
| AWS Public Key used to verify the PKCS#7 signature of the identity document | ||||
| varies by region. It can be found in AWS's documentation. The default key that | ||||
| is used to verify the signature is the one that is applicable for following regions: | ||||
| US East (N. Virginia), US West (Oregon), US West (N. California), EU (Ireland), | ||||
| EU (Frankfurt), Asia Pacific (Tokyo), Asia Pacific (Seoul), Asia Pacific (Singapore), | ||||
| Asia Pacific (Sydney), and South America (Sao Paulo). | ||||
| ` | ||||
							
								
								
									
										144
									
								
								builtin/credential/aws/path_config_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								builtin/credential/aws/path_config_client.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| func pathConfigClient(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "config/client", | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"access_key": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "Access key with permission to query instance metadata.", | ||||
| 			}, | ||||
|  | ||||
| 			"secret_key": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "Secret key with permission to query instance metadata.", | ||||
| 			}, | ||||
|  | ||||
| 			"region": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Default:     "us-east-1", | ||||
| 				Description: "Region for API calls.", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		ExistenceCheck: b.pathConfigClientExistenceCheck, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.CreateOperation: b.pathConfigClientCreateUpdate, | ||||
| 			logical.UpdateOperation: b.pathConfigClientCreateUpdate, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathConfigClientHelpSyn, | ||||
| 		HelpDescription: pathConfigClientHelpDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Establishes dichotomy of request operation between CreateOperation and UpdateOperation. | ||||
| // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. | ||||
| func (b *backend) pathConfigClientExistenceCheck( | ||||
| 	req *logical.Request, data *framework.FieldData) (bool, error) { | ||||
| 	entry, err := clientConfigEntry(req.Storage) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	return entry != nil, nil | ||||
| } | ||||
|  | ||||
| // Fetch the client configuration required to access the AWS API. | ||||
| func clientConfigEntry(s logical.Storage) (*clientConfig, error) { | ||||
| 	entry, err := s.Get("config/client") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	var result clientConfig | ||||
| 	if err := entry.DecodeJSON(&result); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // pathConfigClientCreateUpdate is used to register the 'aws_secret_key' and 'aws_access_key' | ||||
| // that can be used to interact with AWS EC2 API. | ||||
| func (b *backend) pathConfigClientCreateUpdate( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	configEntry, err := clientConfigEntry(req.Storage) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if configEntry == nil { | ||||
| 		configEntry = &clientConfig{} | ||||
| 	} | ||||
|  | ||||
| 	regionStr, ok := data.GetOk("region") | ||||
| 	if ok { | ||||
| 		configEntry.Region = regionStr.(string) | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		configEntry.Region = data.Get("region").(string) | ||||
| 	} | ||||
|  | ||||
| 	// Either a valid region needs to be provided or it should be left empty | ||||
| 	// so a default value could take over. | ||||
| 	if configEntry.Region == "" { | ||||
| 		return nil, fmt.Errorf("invalid region") | ||||
|  | ||||
| 	} | ||||
|  | ||||
| 	accessKeyStr, ok := data.GetOk("access_key") | ||||
| 	if ok { | ||||
| 		configEntry.AccessKey = accessKeyStr.(string) | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		if configEntry.AccessKey = data.Get("access_key").(string); configEntry.AccessKey == "" { | ||||
| 			return nil, fmt.Errorf("missing access_key") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	secretKeyStr, ok := data.GetOk("secret_key") | ||||
| 	if ok { | ||||
| 		configEntry.SecretKey = secretKeyStr.(string) | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		if configEntry.SecretKey = data.Get("secret_key").(string); configEntry.SecretKey == "" { | ||||
| 			return nil, fmt.Errorf("missing secret_key") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	entry, err := logical.StorageEntryJSON("config/client", configEntry) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if err := req.Storage.Put(entry); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| // Struct to hold 'aws_access_key' and 'aws_secret_key' that are required to | ||||
| // interact with the AWS EC2 API. | ||||
| type clientConfig struct { | ||||
| 	AccessKey string `json:"access_key" structs:"access_key" mapstructure:"access_key"` | ||||
| 	SecretKey string `json:"secret_key" structs:"secret_key" mapstructure:"secret_key"` | ||||
| 	Region    string `json:"region" structs:"region" mapstructure:"region"` | ||||
| } | ||||
|  | ||||
| const pathConfigClientHelpSyn = ` | ||||
| Configure the client credentials that are used to query instance details from AWS EC2 API. | ||||
| ` | ||||
|  | ||||
| const pathConfigClientHelpDesc = ` | ||||
| AWS auth backend makes API calls to retrieve EC2 instance metadata. | ||||
| The aws_secret_key and aws_access_key registered with Vault should have the | ||||
| permissions to make these API calls. | ||||
| ` | ||||
							
								
								
									
										246
									
								
								builtin/credential/aws/path_image.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								builtin/credential/aws/path_image.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,246 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/helper/policyutil" | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| func pathImage(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "image/" + framework.GenericNameRegex("name"), | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"name": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "AMI name to be mapped.", | ||||
| 			}, | ||||
|  | ||||
| 			"role_tag": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Default:     "", | ||||
| 				Description: "If set, enables the RoleTag for this AMI. The value set for this field should be the 'key' of the tag on the EC2 instance using the RoleTag. Defaults to empty string.", | ||||
| 			}, | ||||
|  | ||||
| 			"max_ttl": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeDurationSecond, | ||||
| 				Default:     0, | ||||
| 				Description: "The maximum allowed lease duration", | ||||
| 			}, | ||||
|  | ||||
| 			"policies": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Default:     "default", | ||||
| 				Description: "Policies to be associated with the AMI.", | ||||
| 			}, | ||||
|  | ||||
| 			"allow_instance_reboot": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeBool, | ||||
| 				Default:     false, | ||||
| 				Description: "If set, allows rebooting of the OS where the client resides. Essentially, this disables the client nonce check. Use with caution.", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		ExistenceCheck: b.pathImageExistenceCheck, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.CreateOperation: b.pathImageCreateUpdate, | ||||
| 			logical.UpdateOperation: b.pathImageCreateUpdate, | ||||
| 			logical.ReadOperation:   b.pathImageRead, | ||||
| 			logical.DeleteOperation: b.pathImageDelete, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathImageSyn, | ||||
| 		HelpDescription: pathImageDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // pathListImages createa a path that enables listing of all the AMIs that are | ||||
| // registered with Vault. | ||||
| func pathListImages(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "images/?", | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.ListOperation: b.pathImageList, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathListImagesHelpSyn, | ||||
| 		HelpDescription: pathListImagesHelpDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // Establishes dichotomy of request operation between CreateOperation and UpdateOperation. | ||||
| // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. | ||||
| func (b *backend) pathImageExistenceCheck(req *logical.Request, data *framework.FieldData) (bool, error) { | ||||
| 	entry, err := awsImage(req.Storage, strings.ToLower(data.Get("name").(string))) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	return entry != nil, nil | ||||
| } | ||||
|  | ||||
| // awsImage is used to get the information registered for the given AMI ID. | ||||
| func awsImage(s logical.Storage, name string) (*awsImageEntry, error) { | ||||
| 	entry, err := s.Get("image/" + strings.ToLower(name)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	var result awsImageEntry | ||||
| 	if err := entry.DecodeJSON(&result); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // pathImageDelete is used to delete the information registered for a given AMI ID. | ||||
| func (b *backend) pathImageDelete( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	err := req.Storage.Delete("image/" + strings.ToLower(data.Get("name").(string))) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| // pathImageList is used to list all the AMI IDs registered with Vault. | ||||
| func (b *backend) pathImageList( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	images, err := req.Storage.List("image/") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return logical.ListResponse(images), nil | ||||
| } | ||||
|  | ||||
| // pathImageRead is used to view the information registered for a given AMI ID. | ||||
| func (b *backend) pathImageRead( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	imageEntry, err := awsImage(req.Storage, strings.ToLower(data.Get("name").(string))) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if imageEntry == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"role_tag": imageEntry.RoleTag, | ||||
| 			"policies": strings.Join(imageEntry.Policies, ","), | ||||
| 			"max_ttl":  imageEntry.MaxTTL / time.Second, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // pathImageCreateUpdate is used to associate Vault policies to a given AMI ID. | ||||
| func (b *backend) pathImageCreateUpdate( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	imageID := strings.ToLower(data.Get("name").(string)) | ||||
| 	if imageID == "" { | ||||
| 		return logical.ErrorResponse("missing AMI name"), nil | ||||
| 	} | ||||
|  | ||||
| 	imageEntry, err := awsImage(req.Storage, imageID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if imageEntry == nil { | ||||
| 		imageEntry = &awsImageEntry{} | ||||
| 	} | ||||
|  | ||||
| 	policiesStr, ok := data.GetOk("policies") | ||||
| 	if ok { | ||||
| 		imageEntry.Policies = policyutil.ParsePolicies(policiesStr.(string)) | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		imageEntry.Policies = []string{"default"} | ||||
| 	} | ||||
|  | ||||
| 	allowInstanceRebootBool, ok := data.GetOk("allow_instance_reboot") | ||||
| 	if ok { | ||||
| 		imageEntry.AllowInstanceReboot = allowInstanceRebootBool.(bool) | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		imageEntry.AllowInstanceReboot = data.Get("allow_instance_reboot").(bool) | ||||
| 	} | ||||
|  | ||||
| 	maxTTLInt, ok := data.GetOk("max_ttl") | ||||
| 	if ok { | ||||
| 		maxTTL := time.Duration(maxTTLInt.(int)) * time.Second | ||||
| 		systemMaxTTL := b.System().MaxLeaseTTL() | ||||
| 		if maxTTL > systemMaxTTL { | ||||
| 			return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds", maxTTL/time.Second, systemMaxTTL/time.Second)), nil | ||||
| 		} | ||||
|  | ||||
| 		if maxTTL < time.Duration(0) { | ||||
| 			return logical.ErrorResponse("max_ttl cannot be negative"), nil | ||||
| 		} | ||||
|  | ||||
| 		imageEntry.MaxTTL = maxTTL | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		imageEntry.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second | ||||
| 	} | ||||
|  | ||||
| 	roleTagStr, ok := data.GetOk("role_tag") | ||||
| 	if ok { | ||||
| 		imageEntry.RoleTag = roleTagStr.(string) | ||||
| 		if len(imageEntry.RoleTag) > 127 { | ||||
| 			return logical.ErrorResponse("role tag 'key' is exceeding the limit of 127 characters"), nil | ||||
| 		} | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		imageEntry.RoleTag = data.Get("role_tag").(string) | ||||
| 	} | ||||
|  | ||||
| 	entry, err := logical.StorageEntryJSON("image/"+imageID, imageEntry) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if err := req.Storage.Put(entry); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| // Struct to hold the information associated with an AMI ID in Vault. | ||||
| type awsImageEntry struct { | ||||
| 	RoleTag             string        `json:"role_tag" structs:"role_tag" mapstructure:"role_tag"` | ||||
| 	AllowInstanceReboot bool          `json:"allow_instance_reboot" structs:"allow_instance_reboot" mapstructure:"allow_instance_reboot"` | ||||
| 	MaxTTL              time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` | ||||
| 	Policies            []string      `json:"policies" structs:"policies" mapstructure:"policies"` | ||||
| } | ||||
|  | ||||
| const pathImageSyn = ` | ||||
| Associate an AMI to Vault's policies. | ||||
| ` | ||||
|  | ||||
| const pathImageDesc = ` | ||||
| A precondition for login is that the AMI used by the EC2 instance, needs to | ||||
| be registered with Vault. After the authentication of the instance, the | ||||
| authorization for the instance to access Vault's resources is determined | ||||
| by the policies that are associated to the AMI through this endpoint. | ||||
|  | ||||
| In case the AMI is shared by many instances, then a role tag can be created | ||||
| through the endpoint 'image/<name>/tag'. This tag needs to be applied on the | ||||
| instance before it attempts to login to Vault. The policies on the tag should | ||||
| be a subset of policies that are associated to the AMI in this endpoint. In | ||||
| order to enable login using tags, RoleTag needs to be enabled in this endpoint. | ||||
|  | ||||
| Also, a 'max_ttl' can be configured in this endpoint that determines the maximum | ||||
| duration for which a login can be renewed. Note that the 'max_ttl' has a upper | ||||
| limit of the 'max_ttl' value that is applicable to the backend. | ||||
| ` | ||||
|  | ||||
| const pathListImagesHelpSyn = ` | ||||
| Lists all the AMIs that are registered with Vault. | ||||
| ` | ||||
|  | ||||
| const pathListImagesHelpDesc = ` | ||||
| AMIs will be listed by their respective AMI ID. | ||||
| ` | ||||
							
								
								
									
										299
									
								
								builtin/credential/aws/path_image_tag.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								builtin/credential/aws/path_image_tag.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,299 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"crypto/hmac" | ||||
| 	"crypto/sha256" | ||||
| 	"encoding/base64" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/go-uuid" | ||||
| 	"github.com/hashicorp/vault/helper/policyutil" | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| const roleTagVersion = "v1" | ||||
|  | ||||
| func pathImageTag(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "image/" + framework.GenericNameRegex("name") + "/tag$", | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"name": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "AMI name to create a tag for.", | ||||
| 			}, | ||||
|  | ||||
| 			"policies": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "Policies to be associated with the tag.", | ||||
| 			}, | ||||
|  | ||||
| 			"max_ttl": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeDurationSecond, | ||||
| 				Default:     0, | ||||
| 				Description: "The maximum allowed lease duration", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.UpdateOperation: b.pathImageTagUpdate, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathImageTagSyn, | ||||
| 		HelpDescription: pathImageTagDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // pathImageTagUpdate is used to create an EC2 instance tag which will | ||||
| // identify the Vault resources that the instance will be authorized for. | ||||
| func (b *backend) pathImageTagUpdate( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	imageID := strings.ToLower(data.Get("name").(string)) | ||||
| 	if imageID == "" { | ||||
| 		return logical.ErrorResponse("missing image name"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Parse the given policies into a slice and add 'default' if not provided. | ||||
| 	// Remove all other policies if 'root' is present. | ||||
| 	policies := policyutil.ParsePolicies(data.Get("policies").(string)) | ||||
|  | ||||
| 	// Fetch the image entry corresponding to the AMI name | ||||
| 	imageEntry, err := awsImage(req.Storage, imageID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if imageEntry == nil { | ||||
| 		return logical.ErrorResponse("image entry not found"), nil | ||||
| 	} | ||||
|  | ||||
| 	// If RoleTag is empty, disallow creation of tag. | ||||
| 	if imageEntry.RoleTag == "" { | ||||
| 		return logical.ErrorResponse("tag creation is not enabled for this image"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Create a random nonce | ||||
| 	nonce, err := createRoleTagNonce() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// max_ttl for the role tag should be less than the max_ttl set on the image. | ||||
| 	maxTTL := time.Duration(data.Get("max_ttl").(int)) * time.Second | ||||
|  | ||||
| 	// max_ttl on the tag should not be greater than the system view's max_ttl value. | ||||
| 	if maxTTL > b.System().MaxLeaseTTL() { | ||||
| 		return logical.ErrorResponse(fmt.Sprintf("Registered AMI does not have a max_ttl set. So, the given TTL of %d seconds should be less than the max_ttl set for the corresponding backend mount of %d seconds.", maxTTL/time.Second, b.System().MaxLeaseTTL()/time.Second)), nil | ||||
| 	} | ||||
|  | ||||
| 	// If max_ttl is set for the image, check the bounds for tag's max_ttl value using that. | ||||
| 	if imageEntry.MaxTTL != time.Duration(0) && maxTTL > imageEntry.MaxTTL { | ||||
| 		return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than the max_ttl set for the corresponding image of %d seconds", maxTTL/time.Second, imageEntry.MaxTTL/time.Second)), nil | ||||
| 	} | ||||
|  | ||||
| 	if maxTTL < time.Duration(0) { | ||||
| 		return logical.ErrorResponse("max_ttl cannot be negative"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Attach version, nonce, policies and maxTTL to the role tag value. | ||||
| 	rTagValue, err := prepareRoleTagPlainValue(&roleTag{Version: roleTagVersion, | ||||
| 		ImageID:  imageID, | ||||
| 		Nonce:    nonce, | ||||
| 		Policies: policies, | ||||
| 		MaxTTL:   maxTTL, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Get the key used for creating the HMAC | ||||
| 	key, err := hmacKey(req.Storage) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Create the HMAC of the value | ||||
| 	hmacB64, err := createRoleTagHMACBase64(key, rTagValue) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// attach the HMAC to the value | ||||
| 	rTagValue = fmt.Sprintf("%s:%s", rTagValue, hmacB64) | ||||
| 	if len(rTagValue) > 255 { | ||||
| 		return nil, fmt.Errorf("role tag 'value' exceeding the limit of 255 characters") | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"tag_key":   imageEntry.RoleTag, | ||||
| 			"tag_value": rTagValue, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // verifyRoleTagValue rebuilds the role tag value without the HMAC, | ||||
| // computes the HMAC from it using the backend specific key and | ||||
| // compares it with the received HMAC. | ||||
| func verifyRoleTagValue(s logical.Storage, rTag *roleTag) (bool, error) { | ||||
| 	// Fetch the plaintext part of role tag | ||||
| 	rTagPlainText, err := prepareRoleTagPlainValue(rTag) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	// Get the key used for creating the HMAC | ||||
| 	key, err := hmacKey(s) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	// TODO: for testing purposes. Remove this. | ||||
| 	key = "ab1728ba-5fd5-7298-d344-e9df1b09f5ea" | ||||
|  | ||||
| 	// Compute the HMAC of the plaintext | ||||
| 	hmacB64, err := createRoleTagHMACBase64(key, rTagPlainText) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
| 	return rTag.HMAC == hmacB64, nil | ||||
| } | ||||
|  | ||||
| // prepareRoleTagPlainValue builds the role tag value without the HMAC in it. | ||||
| func prepareRoleTagPlainValue(rTag *roleTag) (string, error) { | ||||
| 	if rTag.Version == "" { | ||||
| 		return "", fmt.Errorf("missing version") | ||||
| 	} | ||||
| 	// attach version to the value | ||||
| 	value := rTag.Version | ||||
|  | ||||
| 	if rTag.Nonce == "" { | ||||
| 		return "", fmt.Errorf("missing nonce") | ||||
| 	} | ||||
| 	// attach nonce to the value | ||||
| 	value = fmt.Sprintf("%s:%s", value, rTag.Nonce) | ||||
|  | ||||
| 	if rTag.ImageID == "" { | ||||
| 		return "", fmt.Errorf("missing ami_name") | ||||
| 	} | ||||
| 	// attach ami_name to the value | ||||
| 	value = fmt.Sprintf("%s:a=%s", value, rTag.ImageID) | ||||
|  | ||||
| 	// attach policies to value | ||||
| 	value = fmt.Sprintf("%s:p=%s", value, strings.Join(rTag.Policies, ",")) | ||||
|  | ||||
| 	// attach max_ttl if it is provided | ||||
| 	if rTag.MaxTTL > time.Duration(0) { | ||||
| 		value = fmt.Sprintf("%s:t=%s", value, rTag.MaxTTL) | ||||
| 	} | ||||
|  | ||||
| 	return value, nil | ||||
| } | ||||
|  | ||||
| // Parses the tag from string form into a struct form. | ||||
| func parseRoleTagValue(tag string) (*roleTag, error) { | ||||
| 	tagItems := strings.Split(tag, ":") | ||||
| 	// Tag must contain version, nonce, policies and HMAC | ||||
| 	if len(tagItems) < 4 { | ||||
| 		return nil, fmt.Errorf("invalid tag") | ||||
| 	} | ||||
|  | ||||
| 	rTag := &roleTag{} | ||||
|  | ||||
| 	// Cache the HMAC value. The last item in the collection. | ||||
| 	rTag.HMAC = tagItems[len(tagItems)-1] | ||||
|  | ||||
| 	// Delete the HMAC from the list. | ||||
| 	tagItems = tagItems[:len(tagItems)-1] | ||||
|  | ||||
| 	// Version is the first element. | ||||
| 	rTag.Version = tagItems[0] | ||||
| 	if rTag.Version != roleTagVersion { | ||||
| 		return nil, fmt.Errorf("invalid role tag version") | ||||
| 	} | ||||
|  | ||||
| 	// Nonce is the second element. | ||||
| 	rTag.Nonce = tagItems[1] | ||||
|  | ||||
| 	if len(tagItems) > 2 { | ||||
| 		// Delete the version and nonce from the list. | ||||
| 		tagItems = tagItems[2:] | ||||
| 		for _, tagItem := range tagItems { | ||||
| 			switch { | ||||
| 			case strings.Contains(tagItem, "a="): | ||||
| 				rTag.ImageID = strings.TrimPrefix(tagItem, "a=") | ||||
| 			case strings.Contains(tagItem, "p="): | ||||
| 				rTag.Policies = strings.Split(strings.TrimPrefix(tagItem, "p="), ",") | ||||
| 			case strings.Contains(tagItem, "t="): | ||||
| 				var err error | ||||
| 				rTag.MaxTTL, err = time.ParseDuration(strings.TrimPrefix(tagItem, "t=")) | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 			default: | ||||
| 				return nil, fmt.Errorf("unrecognized item in tag") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if rTag.ImageID == "" { | ||||
| 		return nil, fmt.Errorf("missing image ID") | ||||
| 	} | ||||
|  | ||||
| 	return rTag, nil | ||||
| } | ||||
|  | ||||
| // Creates base64 encoded HMAC using a backend specific key. | ||||
| func createRoleTagHMACBase64(key, value string) (string, error) { | ||||
| 	hm := hmac.New(sha256.New, []byte(key)) | ||||
| 	hm.Write([]byte(value)) | ||||
|  | ||||
| 	// base64 encode the hmac bytes. | ||||
| 	return base64.StdEncoding.EncodeToString(hm.Sum(nil)), nil | ||||
| } | ||||
|  | ||||
| // Creates a base64 encoded random nonce. | ||||
| func createRoleTagNonce() (string, error) { | ||||
| 	uuidBytes, err := uuid.GenerateRandomBytes(8) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
| 	return base64.StdEncoding.EncodeToString(uuidBytes), nil | ||||
| } | ||||
|  | ||||
| // Struct roleTag represents a role tag in a struc form. | ||||
| type roleTag struct { | ||||
| 	Version  string        `json:"version" structs:"version" mapstructure:"version"` | ||||
| 	Nonce    string        `json:"nonce" structs:"nonce" mapstructure:"nonce"` | ||||
| 	Policies []string      `json:"policies" structs:"policies" mapstructure:"policies"` | ||||
| 	MaxTTL   time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` | ||||
| 	ImageID  string        `json:"image_id" structs:"image_id" mapstructure:"image_id"` | ||||
| 	HMAC     string        `json:"hmac" structs:"hmac" mapstructure:"hmac"` | ||||
| } | ||||
|  | ||||
| func (rTag1 *roleTag) Equal(rTag2 *roleTag) bool { | ||||
| 	return rTag1.Version == rTag2.Version && | ||||
| 		rTag1.Nonce == rTag2.Nonce && | ||||
| 		policyutil.EquivalentPolicies(rTag1.Policies, rTag2.Policies) && | ||||
| 		rTag1.MaxTTL == rTag2.MaxTTL && | ||||
| 		rTag1.ImageID == rTag2.ImageID && | ||||
| 		rTag1.HMAC == rTag2.HMAC | ||||
| } | ||||
|  | ||||
| const pathImageTagSyn = ` | ||||
| Create a tag for an EC2 instance. | ||||
| ` | ||||
|  | ||||
| const pathImageTagDesc = ` | ||||
| When an AMI is used by more than one EC2 instance, policies to be associated | ||||
| during login are determined by a particular tag on the instance. This tag | ||||
| can be created using this endpoint. | ||||
|  | ||||
| A RoleTag setting needs to be enabled in 'image/<name>' endpoint, to be able | ||||
| to create a tag. Also, the policies to be associated with the tag should be | ||||
| a subset of the policies associated with the regisred AMI. | ||||
|  | ||||
| This endpoint will return both the 'key' and the 'value' to be set for the | ||||
| instance tag. | ||||
| ` | ||||
							
								
								
									
										483
									
								
								builtin/credential/aws/path_login.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										483
									
								
								builtin/credential/aws/path_login.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,483 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"crypto/x509" | ||||
| 	"encoding/json" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/aws/aws-sdk-go/aws" | ||||
| 	"github.com/aws/aws-sdk-go/service/ec2" | ||||
| 	"github.com/hashicorp/vault/helper/strutil" | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| 	"github.com/vishalnayak/pkcs7" | ||||
| ) | ||||
|  | ||||
| func pathLogin(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "login$", | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"pkcs7": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "PKCS7 signature of the identity document.", | ||||
| 			}, | ||||
|  | ||||
| 			"nonce": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "The nonce created by a client of this backend.", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.UpdateOperation: b.pathLoginUpdate, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathLoginSyn, | ||||
| 		HelpDescription: pathLoginDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // validateInstanceID queries the status of the EC2 instance using AWS EC2 API and | ||||
| // checks if the instance is running and is healthy. | ||||
| func validateInstanceID(s logical.Storage, instanceID string) error { | ||||
| 	// Create an EC2 client to pull the instance information | ||||
| 	ec2Client, err := clientEC2(s) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Get the status of the instance | ||||
| 	instanceStatus, err := ec2Client.DescribeInstanceStatus(&ec2.DescribeInstanceStatusInput{ | ||||
| 		InstanceIds: []*string{aws.String(instanceID)}, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Validate the instance through InstanceState, InstanceStatus and SystemStatus | ||||
| 	return validateInstanceStatus(instanceStatus) | ||||
| } | ||||
|  | ||||
| // validateMetadata matches the given client nonce and pending time with the one cached | ||||
| // in the identity whitelist during the previous login. | ||||
| func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelistIdentity, imageEntry *awsImageEntry) error { | ||||
|  | ||||
| 	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 an OS reboot and | ||||
| 	// lost its client nonce. | ||||
| 	// | ||||
| 	// If `allow_instance_reboot` property of the registered AMI, 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. | ||||
| 	// | ||||
| 	// This is a weak creterion and hence the `allow_instance_reboot` option should be used with caution. | ||||
| 	if clientNonce != storedIdentity.ClientNonce { | ||||
| 		if !imageEntry.AllowInstanceReboot { | ||||
| 			return fmt.Errorf("client nonce mismatch") | ||||
| 		} | ||||
| 		if imageEntry.AllowInstanceReboot && !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 than the | ||||
| 	// 'pendingTime' that was used for previous login. | ||||
| 	if givenPendingTime.Before(storedPendingTime) { | ||||
| 		return fmt.Errorf("instance meta-data is older than the one used for previous login") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // 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 parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocument, error) { | ||||
| 	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: %s\n", err) | ||||
| 	} | ||||
|  | ||||
| 	// Get the public certificate that is used to verify the signature. | ||||
| 	publicCert, err := awsPublicCertificateParsed(s) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if publicCert == nil { | ||||
| 		return nil, fmt.Errorf("certificate to verify the signature is not found") | ||||
| 	} | ||||
|  | ||||
| 	// Before calling Verify() on the PKCS#7 struct, set the certificate to be used | ||||
| 	// to verify the contents in the signer information. | ||||
| 	pkcs7Data.Certificates = []*x509.Certificate{publicCert} | ||||
|  | ||||
| 	// 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 | ||||
| 	err = json.Unmarshal(pkcs7Data.Content, &identityDoc) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &identityDoc, nil | ||||
| } | ||||
|  | ||||
| // pathLoginUpdate is used to create a Vault token by the EC2 instances | ||||
| // by providing its instance identity document, pkcs7 signature of the document, | ||||
| // and a client created nonce. | ||||
| func (b *backend) pathLoginUpdate( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	pkcs7B64 := data.Get("pkcs7").(string) | ||||
|  | ||||
| 	if pkcs7B64 == "" { | ||||
| 		return logical.ErrorResponse("missing pkcs7"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Verify the signature of the identity document. | ||||
| 	identityDoc, err := parseIdentityDocument(req.Storage, pkcs7B64) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if identityDoc == nil { | ||||
| 		return logical.ErrorResponse("failed to extract instance identity document from PKCS#7 signature"), nil | ||||
| 	} | ||||
|  | ||||
| 	clientNonce := data.Get("nonce").(string) | ||||
| 	if clientNonce == "" { | ||||
| 		return logical.ErrorResponse("missing nonce"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Allowing the lengh of UUID for a client nonce. | ||||
| 	if len(clientNonce) > 36 { | ||||
| 		return logical.ErrorResponse("client nonce exceeding the limit of 36 characters"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Validate the instance ID. | ||||
| 	if err := validateInstanceID(req.Storage, identityDoc.InstanceID); err != nil { | ||||
| 		return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil | ||||
| 	} | ||||
|  | ||||
| 	// Get the entry for the AMI used by the instance. | ||||
| 	imageEntry, err := awsImage(req.Storage, identityDoc.ImageID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if imageEntry == nil { | ||||
| 		return logical.ErrorResponse("image entry not found"), nil | ||||
| 	} | ||||
|  | ||||
| 	// Ensure that the TTL is less than the backend mount's max_ttl. | ||||
| 	// If RoleTag is enabled, max_ttl on the RoleTag will be checked to be smaller than this, before being set. | ||||
| 	maxTTL := imageEntry.MaxTTL | ||||
| 	if maxTTL > b.System().MaxLeaseTTL() { | ||||
| 		maxTTL = b.System().MaxLeaseTTL() | ||||
| 	} | ||||
|  | ||||
| 	// Get the entry from the identity whitelist, if there is one. | ||||
| 	storedIdentity, err := whitelistIdentityValidEntry(req.Storage, identityDoc.InstanceID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// 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. | ||||
| 		err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, imageEntry) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Initially, set the policies that are applicable to the image. | ||||
| 	// This may get updated if the image has RoleTag enabled. | ||||
| 	policies := imageEntry.Policies | ||||
|  | ||||
| 	rTagMaxTTL := time.Duration(0) | ||||
|  | ||||
| 	// Role tag is enabled for the AMI. | ||||
| 	if imageEntry.RoleTag != "" { | ||||
| 		// Overwrite the policies with the ones returned from processing the role tag. | ||||
| 		resp, err := handleRoleTagLogin(req.Storage, identityDoc, imageEntry) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		policies = resp.Policies | ||||
| 		rTagMaxTTL = resp.MaxTTL | ||||
|  | ||||
| 		// maxTTL should be set to least among these: image max_ttl, role-tag max_ttl, backend mount's max_ttl. | ||||
| 		if maxTTL > rTagMaxTTL { | ||||
| 			maxTTL = rTagMaxTTL | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Save the login attempt in the identity whitelist. | ||||
| 	currentTime := time.Now() | ||||
| 	if storedIdentity == nil { | ||||
| 		// ImageID, ClientNonce and CreationTime of the identity entry, | ||||
| 		// once set, should never change. | ||||
| 		storedIdentity = &whitelistIdentity{ | ||||
| 			ImageID:      identityDoc.ImageID, | ||||
| 			ClientNonce:  clientNonce, | ||||
| 			CreationTime: currentTime, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// PendingTime, LastUpdatedTime and ExpirationTime may change. | ||||
| 	storedIdentity.LastUpdatedTime = currentTime | ||||
| 	storedIdentity.ExpirationTime = currentTime.Add(maxTTL) | ||||
| 	storedIdentity.PendingTime = identityDoc.PendingTime | ||||
|  | ||||
| 	if err = setWhitelistIdentityEntry(req.Storage, identityDoc.InstanceID, storedIdentity); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
| 		Auth: &logical.Auth{ | ||||
| 			Policies: policies, | ||||
| 			Metadata: map[string]string{ | ||||
| 				"instance_id":      identityDoc.InstanceID, | ||||
| 				"role_tag_max_ttl": rTagMaxTTL.String(), | ||||
| 			}, | ||||
| 			LeaseOptions: logical.LeaseOptions{ | ||||
| 				Renewable: true, | ||||
| 				// There is no TTL on the image/role-tag. Set it to mount's default TTL. | ||||
| 				TTL: b.System().DefaultLeaseTTL(), | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil | ||||
|  | ||||
| } | ||||
|  | ||||
| // fetchRoleTagValue creates an AWS EC2 client and queries the tags | ||||
| // attached to the instance identified by the given instanceID. | ||||
| func fetchRoleTagValue(s logical.Storage, tagKey string) (string, error) { | ||||
| 	ec2Client, err := clientEC2(s) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	// Retrieve the instance tag with a "key" filter matching tagKey. | ||||
| 	tagsOutput, err := ec2Client.DescribeTags(&ec2.DescribeTagsInput{ | ||||
| 		Filters: []*ec2.Filter{ | ||||
| 			&ec2.Filter{ | ||||
| 				Name: aws.String("key"), | ||||
| 				Values: []*string{ | ||||
| 					aws.String(tagKey), | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if tagsOutput.Tags == nil || | ||||
| 		len(tagsOutput.Tags) != 1 || | ||||
| 		*tagsOutput.Tags[0].Key != tagKey || | ||||
| 		*tagsOutput.Tags[0].ResourceType != "instance" { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	return *tagsOutput.Tags[0].Value, nil | ||||
| } | ||||
|  | ||||
| // handleRoleTagLogin is used to fetch the role tag if 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 handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, imageEntry *awsImageEntry) (*roleTagLoginResponse, error) { | ||||
|  | ||||
| 	// Make a secondary call to the AWS instance to see if the desired tag is set. | ||||
| 	// NOTE: If AWS adds the instance tags as meta-data in the instance identity | ||||
| 	// document, then it is better to look this information there instead of making | ||||
| 	// another API call. Currently, we don't have an option but make this call. | ||||
| 	rTagValue, err := fetchRoleTagValue(s, imageEntry.RoleTag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	if rTagValue == "" { | ||||
| 		return nil, fmt.Errorf("missing tag with key %s on the instance", imageEntry.RoleTag) | ||||
| 	} | ||||
|  | ||||
| 	// Check if the role tag is blacklisted. | ||||
| 	blacklistEntry, err := blacklistRoleTagValidEntry(s, rTagValue) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if blacklistEntry != nil { | ||||
| 		return nil, fmt.Errorf("role tag is blacklisted") | ||||
| 	} | ||||
|  | ||||
| 	rTag, err := parseRoleTagValue(rTagValue) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Ensure that the policies on the RoleTag is a subset of policies on the image | ||||
| 	if !strutil.StrListSubset(imageEntry.Policies, rTag.Policies) { | ||||
| 		return nil, fmt.Errorf("policies on the role tag must be subset of policies on the image") | ||||
| 	} | ||||
|  | ||||
| 	// Create a HMAC of the plaintext value of role tag and compare it with the given value. | ||||
| 	verified, err := verifyRoleTagValue(s, rTag) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if !verified { | ||||
| 		return nil, fmt.Errorf("role tag signature mismatch") | ||||
| 	} | ||||
| 	return &roleTagLoginResponse{ | ||||
| 		Policies: rTag.Policies, | ||||
| 		MaxTTL:   rTag.MaxTTL, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // pathLoginRenew is used to renew an authenticated token. | ||||
| func (b *backend) pathLoginRenew( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	storedIdentity, err := whitelistIdentityValidEntry(req.Storage, req.Auth.Metadata["instance_id"]) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// For now, rTagMaxTTL is cached in internal data during login and used in renewal for | ||||
| 	// setting the MaxTTL for the stored login identity entry. | ||||
| 	// If `instance_id` can be used to fetch the role tag again (through an API), it would be good. | ||||
| 	// For accessing the max_ttl, storing the entire identity document is too heavy. | ||||
| 	rTagMaxTTL, err := time.ParseDuration(req.Auth.Metadata["role_tag_max_ttl"]) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	imageEntry, err := awsImage(req.Storage, storedIdentity.ImageID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if imageEntry == nil { | ||||
| 		return logical.ErrorResponse("image entry not found"), nil | ||||
| 	} | ||||
|  | ||||
| 	maxTTL := imageEntry.MaxTTL | ||||
| 	if maxTTL > b.System().MaxLeaseTTL() { | ||||
| 		maxTTL = b.System().MaxLeaseTTL() | ||||
| 	} | ||||
| 	if rTagMaxTTL > time.Duration(0) && maxTTL > rTagMaxTTL { | ||||
| 		maxTTL = rTagMaxTTL | ||||
| 	} | ||||
|  | ||||
| 	// Only LastUpdatedTime and ExpirationTime change, none else. | ||||
| 	currentTime := time.Now() | ||||
| 	storedIdentity.LastUpdatedTime = currentTime | ||||
| 	storedIdentity.ExpirationTime = currentTime.Add(maxTTL) | ||||
|  | ||||
| 	if err = setWhitelistIdentityEntry(req.Storage, req.Auth.Metadata["instance_id"], storedIdentity); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return framework.LeaseExtend(req.Auth.TTL, maxTTL, b.System())(req, data) | ||||
| } | ||||
|  | ||||
| // Validates the instance by checking the InstanceState, InstanceStatus and SystemStatus | ||||
| func validateInstanceStatus(instanceStatus *ec2.DescribeInstanceStatusOutput) error { | ||||
|  | ||||
| 	if instanceStatus.InstanceStatuses == nil { | ||||
| 		return fmt.Errorf("instance statuses not found") | ||||
| 	} | ||||
|  | ||||
| 	if len(instanceStatus.InstanceStatuses) != 1 { | ||||
| 		return fmt.Errorf("length of instance statuses is more than 1") | ||||
| 	} | ||||
|  | ||||
| 	if instanceStatus.InstanceStatuses[0].InstanceState == nil { | ||||
| 		return fmt.Errorf("instance state not found") | ||||
| 	} | ||||
|  | ||||
| 	// Instance should be in 'running'(code 16) state. | ||||
| 	if *instanceStatus.InstanceStatuses[0].InstanceState.Code != 16 { | ||||
| 		return fmt.Errorf("instance state is not 'running'") | ||||
| 	} | ||||
|  | ||||
| 	if instanceStatus.InstanceStatuses[0].InstanceStatus == nil { | ||||
| 		return fmt.Errorf("instance status not found") | ||||
| 	} | ||||
|  | ||||
| 	// InstanceStatus should be 'ok' | ||||
| 	if *instanceStatus.InstanceStatuses[0].InstanceStatus.Status != "ok" { | ||||
| 		return fmt.Errorf("instance status is not 'ok'") | ||||
| 	} | ||||
|  | ||||
| 	if instanceStatus.InstanceStatuses[0].SystemStatus == nil { | ||||
| 		return fmt.Errorf("system status not found") | ||||
| 	} | ||||
|  | ||||
| 	// SystemStatus should be 'ok' | ||||
| 	if *instanceStatus.InstanceStatuses[0].SystemStatus.Status != "ok" { | ||||
| 		return fmt.Errorf("system status is not 'ok'") | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Struct to represent items of interest from the EC2 instance identity document. | ||||
| type identityDocument struct { | ||||
| 	Tags        map[string]interface{} `json:"tags,omitempty" structs:"tags" mapstructure:"tags"` | ||||
| 	InstanceID  string                 `json:"instanceId,omitempty" structs:"instanceId" mapstructure:"instanceId"` | ||||
| 	ImageID     string                 `json:"imageId,omitempty" structs:"imageId" mapstructure:"imageId"` | ||||
| 	Region      string                 `json:"region,omitempty" structs:"region" mapstructure:"region"` | ||||
| 	PendingTime string                 `json:"pendingTime,omitempty" structs:"pendingTime" mapstructure:"pendingTime"` | ||||
| } | ||||
|  | ||||
| type roleTagLoginResponse struct { | ||||
| 	Policies []string      `json:"policies" structs:"policies" mapstructure:"policies"` | ||||
| 	MaxTTL   time.Duration `json:"max_ttl", structs:"max_ttl" mapstructure:"max_ttl"` | ||||
| } | ||||
|  | ||||
| const pathLoginSyn = ` | ||||
| Authenticates an EC2 instance with Vault. | ||||
| ` | ||||
|  | ||||
| const pathLoginDesc = ` | ||||
| An EC2 instance is authenticated using the instance identity document, the identity document's | ||||
| PKCS#7 signature and a client created nonce. This nonce should be unique and should be used by | ||||
| the instance for all future logins. | ||||
|  | ||||
| First login attempt, creates a whitelist entry in Vault associating the instance to the nonce | ||||
| provided. All future logins will succeed only if the client nonce matches the nonce in the | ||||
| whitelisted entry. | ||||
|  | ||||
| The entries in the whitelist are not automatically deleted. Although, they will have an | ||||
| expiration time set on the entry. There is a separate endpoint 'whitelist/identity/tidy', | ||||
| that needs to be invoked to clean-up all the expired entries in the whitelist. | ||||
| ` | ||||
							
								
								
									
										176
									
								
								builtin/credential/aws/path_whitelist_identity.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								builtin/credential/aws/path_whitelist_identity.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| func pathWhitelistIdentity(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "whitelist/identity$", | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"instance_id": &framework.FieldSchema{ | ||||
| 				Type:        framework.TypeString, | ||||
| 				Description: "EC2 instance ID.", | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.ReadOperation:   b.pathWhitelistIdentityRead, | ||||
| 			logical.DeleteOperation: b.pathWhitelistIdentityDelete, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathWhitelistIdentitySyn, | ||||
| 		HelpDescription: pathWhitelistIdentityDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func pathListWhitelistIdentities(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "whitelist/identity/?", | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.ListOperation: b.pathWhitelistIdentitiesList, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathListWhitelistIdentitiesHelpSyn, | ||||
| 		HelpDescription: pathListWhitelistIdentitiesHelpDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // pathWhitelistIdentitiesList is used to list all the instance IDs that are present | ||||
| // in the identity whitelist. This will list both valid and expired entries. | ||||
| func (b *backend) pathWhitelistIdentitiesList( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	identities, err := req.Storage.List("whitelist/identity/") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return logical.ListResponse(identities), nil | ||||
| } | ||||
|  | ||||
| // Fetch an un-expired item from the whitelist given an instance ID. | ||||
| func whitelistIdentityValidEntry(s logical.Storage, instanceID string) (*whitelistIdentity, error) { | ||||
| 	identity, err := whitelistIdentityEntry(s, instanceID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	// Don't return an expired item. | ||||
| 	if identity == nil || time.Now().After(identity.ExpirationTime) { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return identity, nil | ||||
| } | ||||
|  | ||||
| // Fetch an item from the whitelist given an instance ID. | ||||
| func whitelistIdentityEntry(s logical.Storage, instanceID string) (*whitelistIdentity, error) { | ||||
| 	entry, err := s.Get("whitelist/identity/" + instanceID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	var result whitelistIdentity | ||||
| 	if err := entry.DecodeJSON(&result); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return &result, nil | ||||
| } | ||||
|  | ||||
| // Stores an instance ID and the information required to validate further login/renewal attempts from | ||||
| // the same instance ID. | ||||
| func setWhitelistIdentityEntry(s logical.Storage, instanceID string, identity *whitelistIdentity) error { | ||||
| 	entry, err := logical.StorageEntryJSON("whitelist/identity/"+instanceID, identity) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	if err := s.Put(entry); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // pathWhitelistIdentityDelete is used to delete an entry from the identity whitelist given an instance ID. | ||||
| func (b *backend) pathWhitelistIdentityDelete( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	instanceID := data.Get("instance_id").(string) | ||||
| 	if instanceID == "" { | ||||
| 		return logical.ErrorResponse("missing instance_id"), nil | ||||
| 	} | ||||
|  | ||||
| 	err := req.Storage.Delete("whitelist/identity/" + instanceID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| // pathWhitelistIdentityRead is used to view an entry in the identity whitelist given an instance ID. | ||||
| func (b *backend) pathWhitelistIdentityRead( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	instanceID := data.Get("instance_id").(string) | ||||
| 	if instanceID == "" { | ||||
| 		return logical.ErrorResponse("missing instance_id"), nil | ||||
| 	} | ||||
|  | ||||
| 	entry, err := whitelistIdentityEntry(req.Storage, instanceID) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if entry == nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"imate_id":        entry.ImageID, | ||||
| 			"creation_time":   entry.CreationTime.String(), | ||||
| 			"expiration_time": entry.ExpirationTime.String(), | ||||
| 			"client_nonce":    entry.ClientNonce, | ||||
| 			"pending_time":    entry.PendingTime, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| // Struct to represent each item in the identity whitelist. | ||||
| type whitelistIdentity struct { | ||||
| 	ImageID         string    `json:"image_id" structs:"image_id" mapstructure:"image_id"` | ||||
| 	PendingTime     string    `json:"pending_time" structs:"pending_time" mapstructure:"pending_time"` | ||||
| 	ClientNonce     string    `json:"client_nonce" structs:"client_nonce" mapstructure:"client_nonce"` | ||||
| 	CreationTime    time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"` | ||||
| 	LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"` | ||||
| 	ExpirationTime  time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"` | ||||
| } | ||||
|  | ||||
| const pathWhitelistIdentitySyn = ` | ||||
| Read or delete entries in the identity whitelist. | ||||
| ` | ||||
|  | ||||
| const pathWhitelistIdentityDesc = ` | ||||
| Each login from an EC2 instance creates/updates an entry in the identity whitelist. | ||||
|  | ||||
| Entries in this list can be viewed or deleted using this endpoint. | ||||
|  | ||||
| The entries in the whitelist are not automatically deleted. Although, they will have an | ||||
| expiration time set on the entry. There is a separate endpoint 'whitelist/identity/tidy', | ||||
| that needs to be invoked to clean-up all the expired entries in the whitelist. | ||||
| ` | ||||
|  | ||||
| const pathListWhitelistIdentitiesHelpSyn = ` | ||||
| List the items present in the identity whitelist. | ||||
| ` | ||||
|  | ||||
| const pathListWhitelistIdentitiesHelpDesc = ` | ||||
| The entries in the identity whitelist is keyed off of the EC2 instance IDs. | ||||
| This endpoint lists all the entries present in the identity whitelist, both | ||||
| expired and un-expired entries. | ||||
| ` | ||||
							
								
								
									
										87
									
								
								builtin/credential/aws/path_whitelist_identity_tidy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								builtin/credential/aws/path_whitelist_identity_tidy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
|  | ||||
| func pathWhitelistIdentityTidy(b *backend) *framework.Path { | ||||
| 	return &framework.Path{ | ||||
| 		Pattern: "whitelist/identity/tidy$", | ||||
| 		Fields: map[string]*framework.FieldSchema{ | ||||
| 			"safety_buffer": &framework.FieldSchema{ | ||||
| 				Type:    framework.TypeDurationSecond, | ||||
| 				Default: 259200, | ||||
| 				Description: `The amount of extra time that must have passed beyond the identity's | ||||
| expiration, before it is removed from the backend storage.`, | ||||
| 			}, | ||||
| 		}, | ||||
|  | ||||
| 		Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 			logical.UpdateOperation: b.pathWhitelistIdentityTidyUpdate, | ||||
| 		}, | ||||
|  | ||||
| 		HelpSynopsis:    pathWhitelistIdentityTidySyn, | ||||
| 		HelpDescription: pathWhitelistIdentityTidyDesc, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // pathWhitelistIdentityTidyUpdate is used to delete entries in the whitelist that are expired. | ||||
| func (b *backend) pathWhitelistIdentityTidyUpdate( | ||||
| 	req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
|  | ||||
| 	safety_buffer := data.Get("safety_buffer").(int) | ||||
|  | ||||
| 	bufferDuration := time.Duration(safety_buffer) * time.Second | ||||
|  | ||||
| 	identities, err := req.Storage.List("whitelist/identity/") | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	for _, instanceID := range identities { | ||||
| 		identityEntry, err := req.Storage.Get("whitelist/identity/" + instanceID) | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("error fetching identity of instanceID %s: %s", instanceID, err) | ||||
| 		} | ||||
|  | ||||
| 		if identityEntry == nil { | ||||
| 			return nil, fmt.Errorf("identity entry for instanceID %s is nil", instanceID) | ||||
| 		} | ||||
|  | ||||
| 		if identityEntry.Value == nil || len(identityEntry.Value) == 0 { | ||||
| 			return nil, fmt.Errorf("found identity entry for instanceID %s but actual identity is empty", instanceID) | ||||
| 		} | ||||
|  | ||||
| 		var result whitelistIdentity | ||||
| 		if err := identityEntry.DecodeJSON(&result); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		if time.Now().After(result.ExpirationTime.Add(bufferDuration)) { | ||||
| 			if err := req.Storage.Delete("whitelist/identity" + instanceID); err != nil { | ||||
| 				return nil, fmt.Errorf("error deleting identity of instanceID %s from storage: %s", instanceID, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| const pathWhitelistIdentityTidySyn = ` | ||||
| Clean-up the whitelisted instance identity entries. | ||||
| ` | ||||
|  | ||||
| const pathWhitelistIdentityTidyDesc = ` | ||||
| When an instance identity is whitelisted, the expiration time of the whitelist | ||||
| entry is set to the least amont 'max_ttl' of the registered AMI, 'max_ttl' of the | ||||
| role tag and 'max_ttl' of the backend mount. | ||||
|  | ||||
| When this endpoint is invoked all the entries that are expired will be deleted. | ||||
|  | ||||
| A 'safety_buffer' (duration in seconds) can be provided, to ensure deletion of | ||||
| only those entries that are expired before 'safety_buffer' seconds.  | ||||
| ` | ||||
							
								
								
									
										44
									
								
								builtin/credential/aws/role_tag_hmac_key.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								builtin/credential/aws/role_tag_hmac_key.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| package aws | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/hashicorp/go-uuid" | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	RoleTagHMACKeyLocation = "role_tag_hmac_key" | ||||
| ) | ||||
|  | ||||
| // hmacKey returns the key to HMAC the RoleTag value. The key is valid per backend mount. | ||||
| // If a key is not created for the mount, a new key will be created. | ||||
| func hmacKey(s logical.Storage) (string, error) { | ||||
| 	raw, err := s.Get(RoleTagHMACKeyLocation) | ||||
| 	if err != nil { | ||||
| 		return "", fmt.Errorf("failed to read key: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	key := "" | ||||
| 	if raw != nil { | ||||
| 		key = string(raw.Value) | ||||
| 	} | ||||
|  | ||||
| 	if key == "" { | ||||
| 		key, err = uuid.GenerateUUID() | ||||
| 		if err != nil { | ||||
| 			return "", fmt.Errorf("failed to generate uuid: %v", err) | ||||
| 		} | ||||
| 		if s != nil { | ||||
| 			entry := &logical.StorageEntry{ | ||||
| 				Key:   RoleTagHMACKeyLocation, | ||||
| 				Value: []byte(key), | ||||
| 			} | ||||
| 			if err := s.Put(entry); err != nil { | ||||
| 				return "", fmt.Errorf("failed to persist key: %v", err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return key, nil | ||||
| } | ||||
| @@ -9,6 +9,7 @@ import ( | ||||
| 	"github.com/hashicorp/vault/version" | ||||
|  | ||||
| 	credAppId "github.com/hashicorp/vault/builtin/credential/app-id" | ||||
| 	credAws "github.com/hashicorp/vault/builtin/credential/aws" | ||||
| 	credCert "github.com/hashicorp/vault/builtin/credential/cert" | ||||
| 	credGitHub "github.com/hashicorp/vault/builtin/credential/github" | ||||
| 	credLdap "github.com/hashicorp/vault/builtin/credential/ldap" | ||||
| @@ -63,6 +64,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory { | ||||
| 				}, | ||||
| 				CredentialBackends: map[string]logical.Factory{ | ||||
| 					"cert":     credCert.Factory, | ||||
| 					"aws":      credAws.Factory, | ||||
| 					"app-id":   credAppId.Factory, | ||||
| 					"github":   credGitHub.Factory, | ||||
| 					"userpass": credUserpass.Factory, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 vishalnayak
					vishalnayak