Authenticate aws-ec2 instances using identity document and its RSA signature

This commit is contained in:
vishalnayak 2016-10-03 18:06:28 -04:00
parent 5fb6758538
commit b105f8ccf3
2 changed files with 223 additions and 97 deletions

View File

@ -18,14 +18,15 @@ 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).
// This certificate is used to verify the PKCS#7 signature of the instance
// identity document. 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).
//
// It's also the same certificate, but for some reason listed separately, for
// GovCloud (US)
const genericAWSPublicCertificate = `-----BEGIN CERTIFICATE-----
const genericAWSPublicCertificatePkcs7 = `-----BEGIN CERTIFICATE-----
MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw
FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD
VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z
@ -45,6 +46,28 @@ vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw
-----END CERTIFICATE-----
`
// This certificate is used to verify the instance identity document using the
// RSA digest of the same
const genericAWSPublicCertificateIdentity = `-----BEGIN CERTIFICATE-----
MIIDIjCCAougAwIBAgIJAKnL4UEDMN/FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw
FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu
Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC
VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV
BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN//SRK2knbjySG0ho3yqQM3
e2TDhWO8D2e8+XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD
jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL
XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs
77VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh
dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h
em9uYXdzLmNvbYIJAKnL4UEDMN/FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF+j9uO6jz7VOmJqO+pRlAbRlvY8T
C1haGgSI/A1uZUKs/Zfnph0oEI0/hu1IIJ/SKBDtN5lvmZ/IzbOPIJWirlsllQIQ
7zvWbGd9c9+Rm3p04oTvhup99la7kZqevJK0QRdD/6NpCKsqP/0=
-----END CERTIFICATE-----`
// pathListCertificates creates a path that enables listing of all
// the AWS public certificates registered with Vault.
func pathListCertificates(b *backend) *framework.Path {
@ -136,9 +159,11 @@ func decodePEMAndParseCertificate(certificate string) (*x509.Certificate, error)
}
// awsPublicCertificates returns a slice of all the parsed AWS public
// certificates, that were registered using `config/certificate/<cert_name>` endpoint.
// This method will also append default certificate in the backend, to the slice.
func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate, error) {
// certificates, which are used to verify either the SHA256 RSA signature, or
// the PKCS7 signatures of the instance identity documents. This method will
// append the certificates registered using `config/certificate/<cert_name>`
// endpoint, along with the default certificate in the backend.
func (b *backend) awsPublicCertificates(s logical.Storage, isPkcs bool) ([]*x509.Certificate, error) {
// Lock at beginning and use internal method so that we are consistent as
// we iterate through
b.configMutex.RLock()
@ -146,8 +171,13 @@ func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate,
var certs []*x509.Certificate
defaultCert := genericAWSPublicCertificateIdentity
if isPkcs {
defaultCert = genericAWSPublicCertificatePkcs7
}
// Append the generic certificate provided in the AWS EC2 instance metadata documentation.
decodedCert, err := decodePEMAndParseCertificate(genericAWSPublicCertificate)
decodedCert, err := decodePEMAndParseCertificate(defaultCert)
if err != nil {
return nil, err
}
@ -178,7 +208,7 @@ func (b *backend) awsPublicCertificates(s logical.Storage) ([]*x509.Certificate,
return certs, nil
}
// awsPublicCertificate is used to get the configured AWS Public Key that is used
// lockedAWSPublicCertificateEntry is used to get the configured AWS Public Key that is used
// to verify the PKCS#7 signature of the instance identity document.
func (b *backend) lockedAWSPublicCertificateEntry(s logical.Storage, certName string) (*awsPublicCert, error) {
b.configMutex.RLock()

View File

@ -2,6 +2,8 @@ package awsec2
import (
"crypto/subtle"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"strings"
@ -26,7 +28,7 @@ func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: "login$",
Fields: map[string]*framework.FieldSchema{
"role": &framework.FieldSchema{
"role": {
Type: framework.TypeString,
Description: `Name of the role against which the login is being attempted.
If 'role' is not specified, then the login endpoint looks for a role
@ -34,12 +36,12 @@ bearing the name of the AMI ID of the EC2 instance that is trying to login.
If a matching role is not found, login fails.`,
},
"pkcs7": &framework.FieldSchema{
"pkcs7": {
Type: framework.TypeString,
Description: "PKCS7 signature of the identity document.",
},
"nonce": &framework.FieldSchema{
"nonce": {
Type: framework.TypeString,
Description: `The nonce to be used for subsequent login requests.
If this parameter is not specified at all and if reauthentication is allowed,
@ -52,6 +54,14 @@ provided but with an empty value, it indicates intent to disable
reauthentication. Note that, when 'disallow_reauthentication' option is enabled
on either the role or the role tag, the 'nonce' holds no significance.`,
},
"identity": {
Type: framework.TypeString,
Description: `Base64 encoded EC2 instance identity document.`,
},
"signature": {
Type: framework.TypeString,
Description: `Base64 encoded SHA256 RSA signature of the instance identity document.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -100,8 +110,8 @@ func (b *backend) instanceIamRoleARN(s logical.Storage, instanceProfileName, reg
return *profile.InstanceProfile.Roles[0].Arn, nil
}
// validateInstance queries the status of the EC2 instance using AWS EC2 API and
// checks if the instance is running and is healthy.
// validateInstance queries the status of the EC2 instance using AWS EC2 API
// and checks if the instance is running and is healthy
func (b *backend) validateInstance(s logical.Storage, instanceID, region string) (*ec2.DescribeInstancesOutput, error) {
// Create an EC2 client to pull the instance information
ec2Client, err := b.clientEC2(s, region)
@ -120,7 +130,7 @@ func (b *backend) validateInstance(s logical.Storage, instanceID, region string)
},
})
if err != nil {
return nil, fmt.Errorf("error fetching description for instance ID %s: %s\n", instanceID, err)
return nil, fmt.Errorf("error fetching description for instance ID %q: %q\n", instanceID, err)
}
if status == nil {
return nil, fmt.Errorf("nil output from describe instances")
@ -144,9 +154,9 @@ func (b *backend) validateInstance(s logical.Storage, instanceID, region string)
return status, nil
}
// validateMetadata matches the given client nonce and pending time with the one cached
// in the identity whitelist during the previous login. But, if reauthentication is
// disabled, login attempt is failed immediately.
// validateMetadata matches the given client nonce and pending time with the
// one cached in the identity whitelist during the previous login. But, if
// reauthentication is disabled, login attempt is failed immediately.
func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelistIdentity, roleEntry *awsRoleEntry) error {
// For sanity
if !storedIdentity.DisallowReauthentication && storedIdentity.ClientNonce == "" {
@ -171,23 +181,23 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist
return err
}
// When the presented client nonce does not match the cached entry, it is
// either that a rogue client is trying to login or that a valid client
// suffered a migration. The migration is detected via pendingTime in the
// instance metadata, which sadly is only updated when an instance is
// stopped and started but *not* when the instance is rebooted. If reboot
// survivability is needed, either instrumentation to delete the instance
// ID from the whitelist is necessary, or the client must durably store
// the nonce.
// When the presented client nonce does not match the cached entry, it
// is either that a rogue client is trying to login or that a valid
// client suffered a migration. The migration is detected via
// pendingTime in the instance metadata, which sadly is only updated
// when an instance is stopped and started but *not* when the instance
// is rebooted. If reboot survivability is needed, either
// instrumentation to delete the instance ID from the whitelist is
// necessary, or the client must durably store the nonce.
//
// If the `allow_instance_migration` property of the registered role is
// enabled, then the client nonce mismatch is ignored, as long as the
// pending time in the presented instance identity document is newer than
// the cached pending time. The new pendingTime is stored and used for
// future checks.
// pending time in the presented instance identity document is newer
// than the cached pending time. The new pendingTime is stored and used
// for future checks.
//
// This is a weak criterion and hence the `allow_instance_migration` option
// should be used with caution.
// This is a weak criterion and hence the `allow_instance_migration`
// option should be used with caution.
if subtle.ConstantTimeCompare([]byte(clientNonce), []byte(storedIdentity.ClientNonce)) != 1 {
if !roleEntry.AllowInstanceMigration {
return fmt.Errorf("client nonce mismatch")
@ -197,38 +207,81 @@ func validateMetadata(clientNonce, pendingTime string, storedIdentity *whitelist
}
}
// Ensure that the 'pendingTime' on the given identity document is not before the
// 'pendingTime' that was used for previous login. This disallows old metadata documents
// from being used to perform login.
// Ensure that the 'pendingTime' on the given identity document is not
// before the 'pendingTime' that was used for previous login. This
// disallows old metadata documents from being used to perform login.
if givenPendingTime.Before(storedPendingTime) {
return fmt.Errorf("instance meta-data is older than the one used for previous login")
}
return nil
}
// Verifies the integrity of the instance identity document using its SHA256
// RSA signature. After verification, returns the unmarshaled instance identity
// document.
func (b *backend) verifyInstanceIdentitySignature(s logical.Storage, identity, signature string) (*identityDocument, error) {
identity = strings.TrimSpace(identity)
if identity == "" {
return nil, fmt.Errorf("missing instance identity document")
}
signature = strings.TrimSpace(signature)
if signature == "" {
return nil, fmt.Errorf("missing SHA256 RSA signature of the instance identity document")
}
// Get the public certificates that are used to verify the signature.
// This returns a slice of certificates containing the default
// certificate and all the registered certificates via
// 'config/certificate/<cert_name>' endpoint, for verifying the RSA
// digest.
publicCerts, err := b.awsPublicCertificates(s, false)
if err != nil {
return nil, err
}
if publicCerts == nil || len(publicCerts) == 0 {
return nil, fmt.Errorf("certificates to verify the signature are not found")
}
// Check if any of the certs registered at the backend can verify the
// signature
for _, cert := range publicCerts {
err := cert.CheckSignature(x509.SHA256WithRSA, []byte(identity), []byte(signature))
if err == nil {
var identityDoc identityDocument
if decErr := jsonutil.DecodeJSON([]byte(identity), &identityDoc); decErr != nil {
return nil, decErr
}
return &identityDoc, nil
}
}
return nil, fmt.Errorf("instance identity verification using SHA256 RSA signature is unsuccessful")
}
// Verifies the correctness of the authenticated attributes present in the PKCS#7
// signature. After verification, extracts the instance identity document from the
// signature, parses it and returns it.
func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*identityDocument, error) {
// Insert the header and footer for the signature to be able to pem decode it.
// Insert the header and footer for the signature to be able to pem decode it
pkcs7B64 = fmt.Sprintf("-----BEGIN PKCS7-----\n%s\n-----END PKCS7-----", pkcs7B64)
// Decode the PEM encoded signature.
// 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.
// 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)
return nil, fmt.Errorf("failed to parse the BER encoded PKCS#7 signature: %v\n", err)
}
// Get the public certificates that are used to verify the signature.
// This returns a slice of certificates containing the default certificate
// and all the registered certificates via 'config/certificate/<cert_name>' endpoint
publicCerts, err := b.awsPublicCertificates(s)
publicCerts, err := b.awsPublicCertificates(s, true)
if err != nil {
return nil, err
}
@ -246,7 +299,7 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id
return nil, fmt.Errorf("failed to verify the signature")
}
// Check if the signature has content inside of it.
// 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")
}
@ -265,60 +318,97 @@ func (b *backend) parseIdentityDocument(s logical.Storage, pkcs7B64 string) (*id
// option is enabled on the registered role.
func (b *backend) pathLoginUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
identityDocB64 := data.Get("identity").(string)
identityDoc := ""
if identityDocB64 != "" {
identityDocBytes, err := base64.StdEncoding.DecodeString(identityDocB64)
if err != nil {
return logical.ErrorResponse("failed to base64 decode the instance identity document"), nil
}
identityDoc = string(identityDocBytes)
}
signatureB64 := data.Get("signature").(string)
signature := ""
if signatureB64 != "" {
signatureBytes, err := base64.StdEncoding.DecodeString(signatureB64)
if err != nil {
return logical.ErrorResponse("failed to base64 decode the SHA256 RSA signature of the instance identity document"), nil
}
signature = string(signatureBytes)
}
pkcs7B64 := data.Get("pkcs7").(string)
if pkcs7B64 == "" {
return logical.ErrorResponse("missing pkcs7"), nil
// Either the pkcs7 signature of the instance identity document, or
// the identity document itself along with its SHA256 RSA signature
// needs to be provided.
if pkcs7B64 == "" && (identityDoc == "" && signature == "") {
return logical.ErrorResponse("either pkcs7 or a tuple containing the instance identity document and its SHA256 RSA signature needs to be provided"), nil
} else if pkcs7B64 != "" && (identityDoc != "" && signature != "") {
return logical.ErrorResponse("both pkcs7 and a tuple containing the instance identity document and its SHA256 RSA signature is supplied; provide only one"), nil
}
// Verify the signature of the identity document.
identityDoc, err := b.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
// Verify the signature of the identity document and unmarshal it
var identityDocParsed *identityDocument
var err error
if pkcs7B64 != "" {
identityDocParsed, err = b.parseIdentityDocument(req.Storage, pkcs7B64)
if err != nil {
return nil, err
}
if identityDocParsed == nil {
return logical.ErrorResponse("failed to verify the instance identity document using pkcs7"), nil
}
} else {
identityDocParsed, err = b.verifyInstanceIdentitySignature(req.Storage, identityDoc, signature)
if err != nil {
return nil, err
}
if identityDocParsed == nil {
return logical.ErrorResponse("failed to verify the instance identity document using the SHA256 RSA digest"), nil
}
}
roleName := data.Get("role").(string)
// If roleName is not supplied, a role in the name of the instance's AMI ID will be looked for.
// If roleName is not supplied, a role in the name of the instance's AMI ID will be looked for
if roleName == "" {
roleName = identityDoc.AmiID
roleName = identityDocParsed.AmiID
}
// Validate the instance ID by making a call to AWS EC2 DescribeInstances API
// and fetching the instance description. Validation succeeds only if the
// instance is in 'running' state.
instanceDesc, err := b.validateInstance(req.Storage, identityDoc.InstanceID, identityDoc.Region)
instanceDesc, err := b.validateInstance(req.Storage, identityDocParsed.InstanceID, identityDocParsed.Region)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %s", err)), nil
return logical.ErrorResponse(fmt.Sprintf("failed to verify instance ID: %v", err)), nil
}
// Get the entry for the role used by the instance.
// Get the entry for the role used by the instance
roleEntry, err := b.lockedAWSRole(req.Storage, roleName)
if err != nil {
return nil, err
}
if roleEntry == nil {
return logical.ErrorResponse(fmt.Sprintf("entry for role '%s' not found", roleName)), nil
return logical.ErrorResponse(fmt.Sprintf("entry for role %q not found", roleName)), nil
}
// Verify that the AMI ID of the instance trying to login matches the
// AMI ID specified as a constraint on the role.
if roleEntry.BoundAmiID != "" && identityDoc.AmiID != roleEntry.BoundAmiID {
return logical.ErrorResponse(fmt.Sprintf("AMI ID '%s' does not belong to role '%s'", identityDoc.AmiID, roleName)), nil
// AMI ID specified as a constraint on the role
if roleEntry.BoundAmiID != "" && identityDocParsed.AmiID != roleEntry.BoundAmiID {
return logical.ErrorResponse(fmt.Sprintf("AMI ID %q does not belong to role %q", identityDocParsed.AmiID, roleName)), nil
}
// Verify that the AccountID of the instance trying to login matches the
// AccountID specified as a constraint on the role.
if roleEntry.BoundAccountID != "" && identityDoc.AccountID != roleEntry.BoundAccountID {
return logical.ErrorResponse(fmt.Sprintf("Account ID '%s' does not belong to role '%s'", identityDoc.AccountID, roleName)), nil
// AccountID specified as a constraint on the role
if roleEntry.BoundAccountID != "" && identityDocParsed.AccountID != roleEntry.BoundAccountID {
return logical.ErrorResponse(fmt.Sprintf("Account ID %q does not belong to role %q", identityDocParsed.AccountID, roleName)), nil
}
// Check if the IAM instance profile ARN of the instance trying to
// login, matches the IAM instance profile ARN specified as a constraint
// on the role
// on the role.
if roleEntry.BoundIamInstanceProfileARN != "" {
if instanceDesc.Reservations[0].Instances[0].IamInstanceProfile == nil {
return nil, fmt.Errorf("IAM instance profile in the instance description is nil")
@ -359,7 +449,7 @@ func (b *backend) pathLoginUpdate(
}
// Use instance profile ARN to fetch the associated role ARN
iamRoleARN, err := b.instanceIamRoleARN(req.Storage, iamInstanceProfileName, identityDoc.Region)
iamRoleARN, err := b.instanceIamRoleARN(req.Storage, iamInstanceProfileName, identityDocParsed.Region)
if err != nil {
return nil, fmt.Errorf("IAM role ARN could not be fetched: %v", err)
}
@ -373,7 +463,7 @@ func (b *backend) pathLoginUpdate(
}
// Get the entry from the identity whitelist, if there is one
storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDoc.InstanceID)
storedIdentity, err := whitelistIdentityEntry(req.Storage, identityDocParsed.InstanceID)
if err != nil {
return nil, err
}
@ -411,7 +501,7 @@ func (b *backend) pathLoginUpdate(
// of the identity document is not before the pending time of the document
// with which previous login was made. If 'allow_instance_migration' is
// enabled on the registered role, client nonce requirement is relaxed.
if err = validateMetadata(clientNonce, identityDoc.PendingTime, storedIdentity, roleEntry); err != nil {
if err = validateMetadata(clientNonce, identityDocParsed.PendingTime, storedIdentity, roleEntry); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
@ -454,10 +544,12 @@ func (b *backend) pathLoginUpdate(
rTagMaxTTL := time.Duration(0)
if roleEntry.RoleTag != "" {
//
// Role tag is enabled on the role.
//
// Overwrite the policies with the ones returned from processing the role tag.
resp, err := b.handleRoleTagLogin(req.Storage, identityDoc, roleName, roleEntry, instanceDesc)
// Overwrite the policies with the ones returned from processing the role tag
resp, err := b.handleRoleTagLogin(req.Storage, identityDocParsed, roleName, roleEntry, instanceDesc)
if err != nil {
return nil, err
}
@ -479,10 +571,10 @@ func (b *backend) pathLoginUpdate(
disallowReauthentication = resp.DisallowReauthentication
}
// Cache the value of role tag's max_ttl value.
// Cache the value of role tag's max_ttl value
rTagMaxTTL = resp.MaxTTL
// Scope the shortestMaxTTL to the value set on the role tag.
// Scope the shortestMaxTTL to the value set on the role tag
if resp.MaxTTL > time.Duration(0) && resp.MaxTTL < shortestMaxTTL {
shortestMaxTTL = resp.MaxTTL
}
@ -491,7 +583,7 @@ func (b *backend) pathLoginUpdate(
}
}
// Save the login attempt in the identity whitelist.
// Save the login attempt in the identity whitelist
currentTime := time.Now()
if storedIdentity == nil {
// Role, ClientNonce and CreationTime of the identity entry,
@ -507,7 +599,7 @@ func (b *backend) pathLoginUpdate(
// ExpirationTime may change.
storedIdentity.LastUpdatedTime = currentTime
storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL)
storedIdentity.PendingTime = identityDoc.PendingTime
storedIdentity.PendingTime = identityDocParsed.PendingTime
storedIdentity.DisallowReauthentication = disallowReauthentication
// Don't cache the nonce if DisallowReauthentication is set
@ -520,7 +612,7 @@ func (b *backend) pathLoginUpdate(
return logical.ErrorResponse("client nonce exceeding the limit of 128 characters"), nil
}
if err = setWhitelistIdentityEntry(req.Storage, identityDoc.InstanceID, storedIdentity); err != nil {
if err = setWhitelistIdentityEntry(req.Storage, identityDocParsed.InstanceID, storedIdentity); err != nil {
return nil, err
}
@ -528,11 +620,11 @@ func (b *backend) pathLoginUpdate(
Auth: &logical.Auth{
Policies: policies,
Metadata: map[string]string{
"instance_id": identityDoc.InstanceID,
"region": identityDoc.Region,
"instance_id": identityDocParsed.InstanceID,
"region": identityDocParsed.Region,
"role_tag_max_ttl": rTagMaxTTL.String(),
"role": roleName,
"ami_id": identityDoc.AmiID,
"ami_id": identityDocParsed.AmiID,
},
LeaseOptions: logical.LeaseOptions{
Renewable: true,
@ -565,24 +657,25 @@ func (b *backend) pathLoginUpdate(
}
// handleRoleTagLogin is used to fetch the role tag of the instance and verifies it to be correct.
// Then the policies for the login request will be set off of the role tag, if certain creteria satisfies.
func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDocument, roleName string, roleEntry *awsRoleEntry, instanceDesc *ec2.DescribeInstancesOutput) (*roleTagLoginResponse, error) {
if identityDoc == nil {
return nil, fmt.Errorf("nil identityDoc")
// handleRoleTagLogin is used to fetch the role tag of the instance and
// verifies it to be correct. Then the policies for the login request will be
// set off of the role tag, if certain creteria satisfies.
func (b *backend) handleRoleTagLogin(s logical.Storage, identityDocParsed *identityDocument, roleName string, roleEntry *awsRoleEntry, instanceDesc *ec2.DescribeInstancesOutput) (*roleTagLoginResponse, error) {
if identityDocParsed == nil {
return nil, fmt.Errorf("nil parsed identity document")
}
if roleEntry == nil {
return nil, fmt.Errorf("nil roleEntry")
return nil, fmt.Errorf("nil role entry")
}
if instanceDesc == nil {
return nil, fmt.Errorf("nil instanceDesc")
return nil, fmt.Errorf("nil instance description")
}
// Input validation on instanceDesc is not performed here considering
// that it would have been done in validateInstance method.
tags := instanceDesc.Reservations[0].Instances[0].Tags
if tags == nil || len(tags) == 0 {
return nil, fmt.Errorf("missing tag with key %s on the instance", roleEntry.RoleTag)
return nil, fmt.Errorf("missing tag with key %q on the instance", roleEntry.RoleTag)
}
// Iterate through the tags attached on the instance and look for
@ -598,10 +691,10 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc
// If 'role_tag' is enabled on the role, and if a corresponding tag is not found
// to be attached to the instance, fail.
if rTagValue == "" {
return nil, fmt.Errorf("missing tag with key %s on the instance", roleEntry.RoleTag)
return nil, fmt.Errorf("missing tag with key %q on the instance", roleEntry.RoleTag)
}
// Parse the role tag into a struct, extract the plaintext part of it and verify its HMAC.
// Parse the role tag into a struct, extract the plaintext part of it and verify its HMAC
rTag, err := b.parseAndVerifyRoleTagValue(s, rTagValue)
if err != nil {
return nil, err
@ -613,12 +706,12 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc
return nil, fmt.Errorf("role on the tag is not matching the role supplied")
}
// If instance_id was set on the role tag, check if the same instance is attempting to login.
if rTag.InstanceID != "" && rTag.InstanceID != identityDoc.InstanceID {
// If instance_id was set on the role tag, check if the same instance is attempting to login
if rTag.InstanceID != "" && rTag.InstanceID != identityDocParsed.InstanceID {
return nil, fmt.Errorf("role tag is being used by an unauthorized instance.")
}
// Check if the role tag is blacklisted.
// Check if the role tag is blacklisted
blacklistEntry, err := b.lockedBlacklistRoleTagEntry(s, rTagValue)
if err != nil {
return nil, err
@ -639,7 +732,7 @@ func (b *backend) handleRoleTagLogin(s logical.Storage, identityDoc *identityDoc
}, nil
}
// pathLoginRenew is used to renew an authenticated token.
// pathLoginRenew is used to renew an authenticated token
func (b *backend) pathLoginRenew(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
instanceID := req.Auth.Metadata["instance_id"]
@ -655,7 +748,7 @@ func (b *backend) pathLoginRenew(
// Cross check that the instance is still in 'running' state
_, err := b.validateInstance(req.Storage, instanceID, region)
if err != nil {
return nil, fmt.Errorf("failed to verify instance ID '%s': %s", instanceID, err)
return nil, fmt.Errorf("failed to verify instance ID %q: %q", instanceID, err)
}
storedIdentity, err := whitelistIdentityEntry(req.Storage, instanceID)
@ -663,10 +756,10 @@ func (b *backend) pathLoginRenew(
return nil, err
}
if storedIdentity == nil {
return nil, fmt.Errorf("failed to verify the whitelist identity entry for instance ID: %s", instanceID)
return nil, fmt.Errorf("failed to verify the whitelist identity entry for instance ID: %q", instanceID)
}
// Ensure that role entry is not deleted.
// Ensure that role entry is not deleted
roleEntry, err := b.lockedAWSRole(req.Storage, storedIdentity.Role)
if err != nil {
return nil, err
@ -683,7 +776,7 @@ func (b *backend) pathLoginRenew(
return nil, err
}
// Re-evaluate the maxTTL bounds.
// Re-evaluate the maxTTL bounds
shortestMaxTTL := b.System().MaxLeaseTTL()
longestMaxTTL := b.System().MaxLeaseTTL()
if roleEntry.MaxTTL > time.Duration(0) && roleEntry.MaxTTL < shortestMaxTTL {
@ -699,7 +792,7 @@ func (b *backend) pathLoginRenew(
longestMaxTTL = rTagMaxTTL
}
// Cap the TTL value.
// Cap the TTL value
shortestTTL := b.System().DefaultLeaseTTL()
if roleEntry.TTL > time.Duration(0) && roleEntry.TTL < shortestTTL {
shortestTTL = roleEntry.TTL
@ -708,7 +801,7 @@ func (b *backend) pathLoginRenew(
shortestTTL = shortestMaxTTL
}
// Only LastUpdatedTime and ExpirationTime change and all other fields remain the same.
// Only LastUpdatedTime and ExpirationTime change and all other fields remain the same
currentTime := time.Now()
storedIdentity.LastUpdatedTime = currentTime
storedIdentity.ExpirationTime = currentTime.Add(longestMaxTTL)
@ -720,7 +813,8 @@ func (b *backend) pathLoginRenew(
return framework.LeaseExtend(shortestTTL, shortestMaxTTL, b.System())(req, data)
}
// Struct to represent items of interest from the EC2 instance identity document.
// identityDocument represents the 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"`
@ -730,6 +824,8 @@ type identityDocument struct {
PendingTime string `json:"pendingTime,omitempty" structs:"pendingTime" mapstructure:"pendingTime"`
}
// roleTagLoginResponse represents the return values required after the process
// of verifying a role tag login
type roleTagLoginResponse struct {
Policies []string `json:"policies" structs:"policies" mapstructure:"policies"`
MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"`