AWS EC2 instances authentication backend
This commit is contained in:
parent
51a97717db
commit
d3adc85886
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,
|
||||
|
|
Loading…
Reference in a new issue