AWS EC2 instances authentication backend

This commit is contained in:
vishalnayak 2016-04-05 20:42:26 -04:00
parent 51a97717db
commit d3adc85886
14 changed files with 2245 additions and 0 deletions

View 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.
`

View 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)
}
}

View 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
}

View 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.
`

View 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.
`

View 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).
`

View 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.
`

View 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.
`

View 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.
`

View 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.
`

View 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.
`

View 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.
`

View 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
}

View file

@ -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,