2019-04-24 19:12:36 +00:00
package provisioner
import (
2019-07-29 22:54:07 +00:00
"context"
2019-04-24 19:12:36 +00:00
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
"github.com/pkg/errors"
2019-12-20 21:30:05 +00:00
"github.com/smallstep/certificates/errs"
2020-08-24 21:44:11 +00:00
"go.step.sm/crypto/jose"
2020-08-10 18:26:51 +00:00
"go.step.sm/crypto/sshutil"
2020-08-05 23:02:46 +00:00
"go.step.sm/crypto/x509util"
2019-04-24 19:12:36 +00:00
)
// awsIssuer is the string used as issuer in the generated tokens.
const awsIssuer = "ec2.amazonaws.com"
// awsIdentityURL is the url used to retrieve the instance identity document.
const awsIdentityURL = "http://169.254.169.254/latest/dynamic/instance-identity/document"
// awsSignatureURL is the url used to retrieve the instance identity signature.
const awsSignatureURL = "http://169.254.169.254/latest/dynamic/instance-identity/signature"
2020-05-19 22:42:12 +00:00
// awsAPITokenURL is the url used to get the IMDSv2 API token
const awsAPITokenURL = "http://169.254.169.254/latest/api/token"
// awsAPITokenTTL is the default TTL to use when requesting IMDSv2 API tokens
// -- we keep this short-lived since we get a new token with every call to readURL()
const awsAPITokenTTL = "30"
// awsMetadataTokenHeader is the header that must be passed with every IMDSv2 request
const awsMetadataTokenHeader = "X-aws-ec2-metadata-token"
// awsMetadataTokenTTLHeader is the header used to indicate the token TTL requested
const awsMetadataTokenTTLHeader = "X-aws-ec2-metadata-token-ttl-seconds"
2019-04-24 19:12:36 +00:00
// awsCertificate is the certificate used to validate the instance identity
// signature.
2020-10-29 00:47:44 +00:00
//
// The first certificate is used in:
// ap-northeast-2, ap-south-1, ap-southeast-1, ap-southeast-2
// eu-central-1, eu-north-1, eu-west-1, eu-west-2, eu-west-3
// us-east-1, us-east-2, us-west-1, us-west-2
// ca-central-1, sa-east-1
//
// The second certificate is used in:
// eu-south-1
//
// The third certificate is used in:
// ap-east-1
//
// The fourth certificate is used in:
// af-south-1
//
// The fifth certificate is used in:
// me-south-1
2019-04-24 19:12:36 +00:00
const awsCertificate = ` -- -- - BEGIN CERTIFICATE -- -- -
MIIDIjCCAougAwIBAgIJAKnL4UEDMN / FMA0GCSqGSIb3DQEBBQUAMGoxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgw
FgYDVQQKEw9BbWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3Mu
Y29tMB4XDTE0MDYwNTE0MjgwMloXDTI0MDYwNTE0MjgwMlowajELMAkGA1UEBhMC
VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1NlYXR0bGUxGDAWBgNV
BAoTD0FtYXpvbi5jb20gSW5jLjEaMBgGA1UEAxMRZWMyLmFtYXpvbmF3cy5jb20w
gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAIe9GN //SRK2knbjySG0ho3yqQM3
e2TDhWO8D2e8 + XZqck754gFSo99AbT2RmXClambI7xsYHZFapbELC4H91ycihvrD
jbST1ZjkLQgga0NE1q43eS68ZeTDccScXQSNivSlzJZS8HJZjgqzBlXjZftjtdJL
XeE4hwvo0sD4f3j9AgMBAAGjgc8wgcwwHQYDVR0OBBYEFCXWzAgVyrbwnFncFFIs
77 VBdlE4MIGcBgNVHSMEgZQwgZGAFCXWzAgVyrbwnFncFFIs77VBdlE4oW6kbDBq
MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2Vh
dHRsZTEYMBYGA1UEChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1h
em9uYXdzLmNvbYIJAKnL4UEDMN / FMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
BQADgYEAFYcz1OgEhQBXIwIdsgCOS8vEtiJYF + j9uO6jz7VOmJqO + pRlAbRlvY8T
C1haGgSI / A1uZUKs / Zfnph0oEI0 / hu1IIJ / SKBDtN5lvmZ / IzbOPIJWirlsllQIQ
7 zvWbGd9c9 + Rm3p04oTvhup99la7kZqevJK0QRdD / 6 NpCKsqP / 0 =
2020-10-29 00:47:44 +00:00
-- -- - END CERTIFICATE -- -- -
-- -- - BEGIN CERTIFICATE -- -- -
MIICNjCCAZ + gAwIBAgIJAOZ3GEIaDcugMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTEwMjQx
NTE5MDlaGA8yMTk5MDMyOTE1MTkwOVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT
EFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft
YXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQCjiPgW3vsXRj4JoA16WQDyoPc / eh3QBARaApJEc4nPIGoUolpAXcjFhWplo2O +
ivgfCsc4AU9OpYdAPha3spLey / bhHPRi1JZHRNqScKP0hzsCNmKhfnZTIEQCFvsp
DRp4zr91 / WS06 / flJFBYJ6JHhp0KwM81XQG59lV6kkoW7QIDAQABMA0GCSqGSIb3
DQEBCwUAA4GBAGLLrY3P + HH6C57dYgtJkuGZGT2 + rMkk2n81 / abzTJvsqRqGRrWv
XRKRXlKdM / dfiuYGokDGxiC0Mg6TYy6wvsR2qRhtXW1OtZkiHWcQCnOttz + 8 vpew
wx8JGMvowtuKB1iMsbwyRqZkFYLcvH + Opfb / Aayi20 / ChQLdI6M2R5VU
-- -- - END CERTIFICATE -- -- -
-- -- - BEGIN CERTIFICATE -- -- -
MIICSzCCAbQCCQDtQvkVxRvK9TANBgkqhkiG9w0BAQsFADBqMQswCQYDVQQGEwJV
UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHU2VhdHRsZTEYMBYGA1UE
ChMPQW1hem9uLmNvbSBJbmMuMRowGAYDVQQDExFlYzIuYW1hem9uYXdzLmNvbTAe
Fw0xOTAyMDMwMzAwMDZaFw0yOTAyMDIwMzAwMDZaMGoxCzAJBgNVBAYTAlVTMRMw
EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdTZWF0dGxlMRgwFgYDVQQKEw9B
bWF6b24uY29tIEluYy4xGjAYBgNVBAMTEWVjMi5hbWF6b25hd3MuY29tMIGfMA0G
CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1kkHXYTfc7gY5Q55JJhjTieHAgacaQkiR
Pity9QPDE3b + NXDh4UdP1xdIw73JcIIG3sG9RhWiXVCHh6KkuCTqJfPUknIKk8vs
M3RXflUpBe8Pf + P92pxqPMCz1Fr2NehS3JhhpkCZVGxxwLC5gaG0Lr4rFORubjYY
Rh84dK98VwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAA6xV9f0HMqXjPHuGILDyaNN
dKcvplNFwDTydVg32MNubAGnecoEBtUPtxBsLoVYXCOb + b5 / ZMDubPF9tU / vSXuo
TpYM5Bq57gJzDRaBOntQbX9bgHiUxw6XZWaTS / 6 xjRJDT5p3S1E0mPI3lP / eJv4o
Ezk5zb3eIf10 / sqt4756
-- -- - END CERTIFICATE -- -- -
-- -- - BEGIN CERTIFICATE -- -- -
MIICNjCCAZ + gAwIBAgIJAKumfZiRrNvHMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV
BAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0
dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAgFw0xOTExMjcw
NzE0MDVaGA8yMTk5MDUwMjA3MTQwNVowXDELMAkGA1UEBhMCVVMxGTAXBgNVBAgT
EFdhc2hpbmd0b24gU3RhdGUxEDAOBgNVBAcTB1NlYXR0bGUxIDAeBgNVBAoTF0Ft
YXpvbiBXZWIgU2VydmljZXMgTExDMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
gQDFd571nUzVtke3rPyRkYfvs3jh0C0EMzzG72boyUNjnfw1 + m0TeFraTLKb9T6F
7 TuB / ZEN + vmlYqr2 + 5 Va8U8qLbPF0bRH + FdaKjhgWZdYXxGzQzU3ioy5W5ZM1VyB
7i UsxEAlxsybC3ziPYaHI42UiTkQNahmoroNeqVyHNnBpQIDAQABMA0GCSqGSIb3
DQEBCwUAA4GBAAJLylWyElEgOpW4B1XPyRVD4pAds8Guw2 + krgqkY0HxLCdjosuH
RytGDGN + q75aAoXzW5a7SGpxLxk6Hfv0xp3RjDHsoeP0i1d8MD3hAC5ezxS4oukK
s5gbPOnokhKTMPXbTdRn5ZifCbWlx + bYN / mTYKvxho7b5SVg2o1La9aK
-- -- - END CERTIFICATE -- -- -
-- -- - BEGIN CERTIFICATE -- -- -
MIIDPDCCAqWgAwIBAgIJAMl6uIV / zqJFMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV
BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMSAw
HgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwRZWMyLmFt
YXpvbmF3cy5jb20wIBcNMTkwNDI2MTQzMjQ3WhgPMjE5ODA5MjkxNDMyNDdaMHIx
CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0
dGxlMSAwHgYDVQQKDBdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzEaMBgGA1UEAwwR
ZWMyLmFtYXpvbmF3cy5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALVN
CDTZEnIeoX1SEYqq6k1BV0ZlpY5y3KnoOreCAE589TwS4MX5 + 8 Fzd6AmACmugeBP
Qk7Hm6b2 + g / d4tWycyxLaQlcq81DB1GmXehRkZRgGeRge1ePWd1TUA0I8P / QBT7S
gUePm / kANSFU + P7s7u1NNl + vynyi0wUUrw7 / wIZTAgMBAAGjgdcwgdQwHQYDVR0O
BBYEFILtMd + T4YgH1cgc + hVsVOV + 480 FMIGkBgNVHSMEgZwwgZmAFILtMd + T4YgH
1 cgc + hVsVOV + 480 FoXakdDByMQswCQYDVQQGEwJVUzETMBEGA1UECAwKV2FzaGlu
Z3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEgMB4GA1UECgwXQW1hem9uIFdlYiBTZXJ2
aWNlcyBMTEMxGjAYBgNVBAMMEWVjMi5hbWF6b25hd3MuY29tggkAyXq4hX / OokUw
DAYDVR0TBAUwAwEB / zANBgkqhkiG9w0BAQsFAAOBgQBhkNTBIFgWFd + ZhC / LhRUY
4 OjEiykmbEp6hlzQ79T0Tfbn5A4NYDI2icBP0 + hmf6qSnIhwJF6typyd1yPK5Fqt
NTpxxcXmUKquX + pHmIkK1LKDO8rNE84jqxrxRsfDi6by82fjVYf2pgjJW8R1FAw +
mL5WQRFexbfB5aXhcMo0AA ==
2019-04-24 19:12:36 +00:00
-- -- - END CERTIFICATE -- -- - `
// awsSignatureAlgorithm is the signature algorithm used to verify the identity
// document signature.
const awsSignatureAlgorithm = x509 . SHA256WithRSA
type awsConfig struct {
identityURL string
signatureURL string
2020-05-20 13:03:35 +00:00
tokenURL string
tokenTTL string
2020-10-14 00:51:24 +00:00
certificates [ ] * x509 . Certificate
2019-04-24 19:12:36 +00:00
signatureAlgorithm x509 . SignatureAlgorithm
}
2020-10-14 00:51:24 +00:00
func newAWSConfig ( certPath string ) ( * awsConfig , error ) {
var certBytes [ ] byte
if certPath == "" {
certBytes = [ ] byte ( awsCertificate )
} else {
if b , err := ioutil . ReadFile ( certPath ) ; err == nil {
certBytes = b
} else {
return nil , errors . Wrapf ( err , "error reading %s" , certPath )
}
2019-04-24 19:12:36 +00:00
}
2020-10-14 00:51:24 +00:00
// Read all the certificates.
var certs [ ] * x509 . Certificate
for len ( certBytes ) > 0 {
var block * pem . Block
block , certBytes = pem . Decode ( certBytes )
if block == nil {
break
}
if block . Type != "CERTIFICATE" || len ( block . Headers ) != 0 {
continue
}
cert , err := x509 . ParseCertificate ( block . Bytes )
if err != nil {
return nil , errors . Wrap ( err , "error parsing AWS IID certificate" )
}
certs = append ( certs , cert )
2019-04-24 19:12:36 +00:00
}
2020-10-14 00:51:24 +00:00
if len ( certs ) == 0 {
return nil , errors . New ( "error parsing AWS IID certificate: no certificates found" )
}
2019-04-24 19:12:36 +00:00
return & awsConfig {
identityURL : awsIdentityURL ,
signatureURL : awsSignatureURL ,
2020-05-20 13:03:35 +00:00
tokenURL : awsAPITokenURL ,
tokenTTL : awsAPITokenTTL ,
2020-10-14 00:51:24 +00:00
certificates : certs ,
2019-04-24 19:12:36 +00:00
signatureAlgorithm : awsSignatureAlgorithm ,
} , nil
}
type awsPayload struct {
jose . Claims
Amazon awsAmazonPayload ` json:"amazon" `
SANs [ ] string ` json:"sans" `
document awsInstanceIdentityDocument
}
type awsAmazonPayload struct {
Document [ ] byte ` json:"document" `
Signature [ ] byte ` json:"signature" `
}
type awsInstanceIdentityDocument struct {
AccountID string ` json:"accountId" `
Architecture string ` json:"architecture" `
AvailabilityZone string ` json:"availabilityZone" `
BillingProducts [ ] string ` json:"billingProducts" `
DevpayProductCodes [ ] string ` json:"devpayProductCodes" `
ImageID string ` json:"imageId" `
InstanceID string ` json:"instanceId" `
InstanceType string ` json:"instanceType" `
KernelID string ` json:"kernelId" `
PendingTime time . Time ` json:"pendingTime" `
PrivateIP string ` json:"privateIp" `
RamdiskID string ` json:"ramdiskId" `
Region string ` json:"region" `
Version string ` json:"version" `
}
// AWS is the provisioner that supports identity tokens created from the Amazon
// Web Services Instance Identity Documents.
//
// If DisableCustomSANs is true, only the internal DNS and IP will be added as a
// SAN. By default it will accept any SAN in the CSR.
//
// If DisableTrustOnFirstUse is true, multiple sign request for this provisioner
// with the same instance will be accepted. By default only the first request
// will be accepted.
2019-06-05 18:04:00 +00:00
//
2019-06-07 18:24:56 +00:00
// If InstanceAge is set, only the instances with a pendingTime within the given
// period will be accepted.
2019-06-05 18:04:00 +00:00
//
2020-10-14 00:51:24 +00:00
// IIDRoots can be used to specify a path to the certificates used to verify the
// identity certificate signature.
//
2019-06-05 18:04:00 +00:00
// Amazon Identity docs are available at
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
2019-04-24 19:12:36 +00:00
type AWS struct {
2019-10-28 18:50:43 +00:00
* base
2021-05-20 01:23:20 +00:00
ID string ` json:"-" `
2020-07-31 01:44:52 +00:00
Type string ` json:"type" `
Name string ` json:"name" `
Accounts [ ] string ` json:"accounts" `
DisableCustomSANs bool ` json:"disableCustomSANs" `
DisableTrustOnFirstUse bool ` json:"disableTrustOnFirstUse" `
2020-05-20 17:15:51 +00:00
IMDSVersions [ ] string ` json:"imdsVersions" `
2020-07-31 01:44:52 +00:00
InstanceAge Duration ` json:"instanceAge,omitempty" `
2020-10-14 00:51:24 +00:00
IIDRoots string ` json:"iidRoots,omitempty" `
2020-07-31 01:44:52 +00:00
Claims * Claims ` json:"claims,omitempty" `
Options * Options ` json:"options,omitempty" `
2019-04-24 19:12:36 +00:00
claimer * Claimer
config * awsConfig
2019-06-06 19:49:51 +00:00
audiences Audiences
2019-04-24 19:12:36 +00:00
}
// GetID returns the provisioner unique identifier.
func ( p * AWS ) GetID ( ) string {
2021-05-20 01:23:20 +00:00
if p . ID != "" {
return p . ID
}
return p . GetIDForToken ( )
}
// GetIDForToken returns an identifier that will be used to load the provisioner
// from a token.
func ( p * AWS ) GetIDForToken ( ) string {
2019-06-06 19:49:51 +00:00
return "aws/" + p . Name
2019-04-24 19:12:36 +00:00
}
// GetTokenID returns the identifier of the token.
func ( p * AWS ) GetTokenID ( token string ) ( string , error ) {
payload , err := p . authorizeToken ( token )
if err != nil {
return "" , err
}
// If TOFU is disabled create an ID for the token, so it cannot be reused.
// The timestamps, document and signatures should be mostly unique.
if p . DisableTrustOnFirstUse {
sum := sha256 . Sum256 ( [ ] byte ( token ) )
return strings . ToLower ( hex . EncodeToString ( sum [ : ] ) ) , nil
}
2020-12-17 22:52:34 +00:00
// Use provisioner + instance-id as the identifier.
2021-05-03 19:48:20 +00:00
unique := fmt . Sprintf ( "%s.%s" , p . GetIDForToken ( ) , payload . document . InstanceID )
2020-12-17 22:52:34 +00:00
sum := sha256 . Sum256 ( [ ] byte ( unique ) )
return strings . ToLower ( hex . EncodeToString ( sum [ : ] ) ) , nil
2019-04-24 19:12:36 +00:00
}
// GetName returns the name of the provisioner.
func ( p * AWS ) GetName ( ) string {
return p . Name
}
// GetType returns the type of provisioner.
func ( p * AWS ) GetType ( ) Type {
return TypeAWS
}
// GetEncryptedKey is not available in an AWS provisioner.
func ( p * AWS ) GetEncryptedKey ( ) ( kid string , key string , ok bool ) {
return "" , "" , false
}
// GetIdentityToken retrieves the identity document and it's signature and
// generates a token with them.
2019-07-15 22:52:36 +00:00
func ( p * AWS ) GetIdentityToken ( subject , caURL string ) ( string , error ) {
2019-04-24 19:12:36 +00:00
// Initialize the config if this method is used from the cli.
if err := p . assertConfig ( ) ; err != nil {
return "" , err
}
var idoc awsInstanceIdentityDocument
doc , err := p . readURL ( p . config . identityURL )
if err != nil {
2020-05-20 18:43:25 +00:00
return "" , errors . Wrap ( err , "error retrieving identity document:\n Are you in an AWS VM?\n Is the metadata service enabled?\n Are you using the proper metadata service version?" )
2019-04-24 19:12:36 +00:00
}
if err := json . Unmarshal ( doc , & idoc ) ; err != nil {
return "" , errors . Wrap ( err , "error unmarshaling identity document" )
}
sig , err := p . readURL ( p . config . signatureURL )
if err != nil {
2020-05-20 18:43:25 +00:00
return "" , errors . Wrap ( err , "error retrieving identity document:\n Are you in an AWS VM?\n Is the metadata service enabled?\n Are you using the proper metadata service version?" )
2019-04-24 19:12:36 +00:00
}
signature , err := base64 . StdEncoding . DecodeString ( string ( sig ) )
if err != nil {
return "" , errors . Wrap ( err , "error decoding identity document signature" )
}
if err := p . checkSignature ( doc , signature ) ; err != nil {
return "" , err
}
2021-05-03 19:48:20 +00:00
audience , err := generateSignAudience ( caURL , p . GetIDForToken ( ) )
2019-06-06 19:49:51 +00:00
if err != nil {
return "" , err
}
2019-04-24 19:12:36 +00:00
// Create unique ID for Trust On First Use (TOFU). Only the first instance
// per provisioner is allowed as we don't have a way to trust the given
// sans.
2021-05-03 19:48:20 +00:00
unique := fmt . Sprintf ( "%s.%s" , p . GetIDForToken ( ) , idoc . InstanceID )
2019-04-24 19:12:36 +00:00
sum := sha256 . Sum256 ( [ ] byte ( unique ) )
// Create a JWT from the identity document
signer , err := jose . NewSigner (
jose . SigningKey { Algorithm : jose . HS256 , Key : signature } ,
new ( jose . SignerOptions ) . WithType ( "JWT" ) ,
)
2019-04-24 21:59:01 +00:00
if err != nil {
return "" , errors . Wrap ( err , "error creating signer" )
}
2019-04-24 19:12:36 +00:00
now := time . Now ( )
payload := awsPayload {
Claims : jose . Claims {
Issuer : awsIssuer ,
2019-07-15 22:52:36 +00:00
Subject : subject ,
2019-06-06 19:49:51 +00:00
Audience : [ ] string { audience } ,
2019-04-24 19:12:36 +00:00
Expiry : jose . NewNumericDate ( now . Add ( 5 * time . Minute ) ) ,
NotBefore : jose . NewNumericDate ( now ) ,
IssuedAt : jose . NewNumericDate ( now ) ,
ID : strings . ToLower ( hex . EncodeToString ( sum [ : ] ) ) ,
} ,
Amazon : awsAmazonPayload {
Document : doc ,
Signature : signature ,
} ,
}
tok , err := jose . Signed ( signer ) . Claims ( payload ) . CompactSerialize ( )
if err != nil {
2020-10-14 00:51:24 +00:00
return "" , errors . Wrap ( err , "error serializing token" )
2019-04-24 19:12:36 +00:00
}
return tok , nil
}
// Init validates and initializes the AWS provisioner.
func ( p * AWS ) Init ( config Config ) ( err error ) {
switch {
case p . Type == "" :
return errors . New ( "provisioner type cannot be empty" )
case p . Name == "" :
return errors . New ( "provisioner name cannot be empty" )
2019-06-04 23:31:33 +00:00
case p . InstanceAge . Value ( ) < 0 :
return errors . New ( "provisioner instanceAge cannot be negative" )
2019-04-24 19:12:36 +00:00
}
// Update claims with global ones
if p . claimer , err = NewClaimer ( p . Claims , config . Claims ) ; err != nil {
return err
}
// Add default config
2020-10-14 00:51:24 +00:00
if p . config , err = newAWSConfig ( p . IIDRoots ) ; err != nil {
2019-04-24 19:12:36 +00:00
return err
}
2021-05-03 19:48:20 +00:00
p . audiences = config . Audiences . WithFragment ( p . GetIDForToken ( ) )
2020-05-20 17:24:45 +00:00
// validate IMDS versions
if len ( p . IMDSVersions ) == 0 {
p . IMDSVersions = [ ] string { "v2" , "v1" }
}
for _ , v := range p . IMDSVersions {
switch v {
case "v1" :
// valid
case "v2" :
// valid
default :
return errors . Errorf ( "%s: not a supported AWS Instance Metadata Service version" , v )
}
}
2019-04-24 19:12:36 +00:00
return nil
}
// AuthorizeSign validates the given token and returns the sign options that
// will be used on certificate creation.
2019-07-29 22:54:07 +00:00
func ( p * AWS ) AuthorizeSign ( ctx context . Context , token string ) ( [ ] SignOption , error ) {
2019-04-24 19:12:36 +00:00
payload , err := p . authorizeToken ( token )
if err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusInternalServerError , err , "aws.AuthorizeSign" )
2019-04-24 19:12:36 +00:00
}
2019-07-29 22:54:07 +00:00
doc := payload . document
2020-07-13 19:47:26 +00:00
// Template options
data := x509util . NewTemplateData ( )
data . SetCommonName ( payload . Claims . Subject )
2020-07-21 18:41:36 +00:00
if v , err := unsafeParseSigned ( token ) ; err == nil {
data . SetToken ( v )
}
2020-07-13 19:47:26 +00:00
2019-07-15 22:52:36 +00:00
// Enforce known CN and default DNS and IP if configured.
// By default we'll accept the CN and SANs in the CSR.
2019-04-24 20:05:46 +00:00
// There's no way to trust them other than TOFU.
2019-04-24 19:12:36 +00:00
var so [ ] SignOption
if p . DisableCustomSANs {
2020-07-13 19:47:26 +00:00
dnsName := fmt . Sprintf ( "ip-%s.%s.compute.internal" , strings . Replace ( doc . PrivateIP , "." , "-" , - 1 ) , doc . Region )
so = append ( so , dnsNamesValidator ( [ ] string { dnsName } ) )
2019-04-24 19:12:36 +00:00
so = append ( so , ipAddressesValidator ( [ ] net . IP {
net . ParseIP ( doc . PrivateIP ) ,
} ) )
2020-06-23 18:10:45 +00:00
so = append ( so , emailAddressesValidator ( nil ) )
so = append ( so , urisValidator ( nil ) )
2020-07-13 23:09:40 +00:00
// Template options
data . SetSANs ( [ ] string { dnsName , doc . PrivateIP } )
2019-04-24 19:12:36 +00:00
}
2020-07-13 19:47:26 +00:00
templateOptions , err := CustomTemplateOptions ( p . Options , data , x509util . DefaultIIDLeafTemplate )
if err != nil {
return nil , errs . Wrap ( http . StatusInternalServerError , err , "aws.AuthorizeSign" )
}
2019-04-24 19:12:36 +00:00
return append ( so ,
2020-07-13 19:47:26 +00:00
templateOptions ,
2019-09-05 01:31:09 +00:00
// modifiers / withOptions
newProvisionerExtensionOption ( TypeAWS , p . Name , doc . AccountID , "InstanceID" , doc . InstanceID ) ,
profileDefaultDuration ( p . claimer . DefaultTLSCertDuration ( ) ) ,
// validators
2019-08-27 00:52:49 +00:00
defaultPublicKeyValidator { } ,
2019-07-15 22:52:36 +00:00
commonNameValidator ( payload . Claims . Subject ) ,
2019-04-24 19:12:36 +00:00
newValidityValidator ( p . claimer . MinTLSCertDuration ( ) , p . claimer . MaxTLSCertDuration ( ) ) ,
) , nil
}
2019-10-28 18:50:43 +00:00
// AuthorizeRenew returns an error if the renewal is disabled.
// NOTE: This method does not actually validate the certificate or check it's
// revocation status. Just confirms that the provisioner that created the
// certificate was configured to allow renewals.
func ( p * AWS ) AuthorizeRenew ( ctx context . Context , cert * x509 . Certificate ) error {
2019-04-24 19:12:36 +00:00
if p . claimer . IsDisableRenewal ( ) {
2021-05-03 19:48:20 +00:00
return errs . Unauthorized ( "aws.AuthorizeRenew; renew is disabled for aws provisioner '%s'" , p . GetName ( ) )
2019-04-24 19:12:36 +00:00
}
return nil
}
// assertConfig initializes the config if it has not been initialized
func ( p * AWS ) assertConfig ( ) ( err error ) {
if p . config != nil {
return
}
2020-10-14 00:51:24 +00:00
p . config , err = newAWSConfig ( p . IIDRoots )
2019-04-24 19:12:36 +00:00
return err
}
// checkSignature returns an error if the signature is not valid.
func ( p * AWS ) checkSignature ( signed , signature [ ] byte ) error {
2020-10-14 00:51:24 +00:00
for _ , crt := range p . config . certificates {
if err := crt . CheckSignature ( p . config . signatureAlgorithm , signed , signature ) ; err == nil {
return nil
}
2019-04-24 19:12:36 +00:00
}
2020-10-14 00:51:24 +00:00
return errors . New ( "error validating identity document signature" )
2019-04-24 19:12:36 +00:00
}
// readURL does a GET request to the given url and returns the body. It's not
// using pkg/errors to avoid verbose errors, the caller should use it and write
// the appropriate error.
func ( p * AWS ) readURL ( url string ) ( [ ] byte , error ) {
2020-05-20 17:15:51 +00:00
var resp * http . Response
var err error
2021-08-27 00:55:42 +00:00
// Initialize IMDS versions when this is called from the cli.
if len ( p . IMDSVersions ) == 0 {
p . IMDSVersions = [ ] string { "v2" , "v1" }
}
2020-05-20 17:15:51 +00:00
for _ , v := range p . IMDSVersions {
switch v {
case "v1" :
resp , err = p . readURLv1 ( url )
if err == nil && resp . StatusCode < 400 {
return p . readResponseBody ( resp )
}
case "v2" :
resp , err = p . readURLv2 ( url )
if err == nil && resp . StatusCode < 400 {
return p . readResponseBody ( resp )
}
default :
return nil , fmt . Errorf ( "%s: not a supported AWS Instance Metadata Service version" , v )
}
2020-07-22 23:52:06 +00:00
if resp != nil {
resp . Body . Close ( )
}
2020-05-20 17:15:51 +00:00
}
// all versions have been exhausted and we haven't returned successfully yet so pass
// the error on to the caller
2019-04-24 19:12:36 +00:00
if err != nil {
return nil , err
}
2020-05-20 17:15:51 +00:00
return nil , fmt . Errorf ( "Request for metadata returned non-successful status code %d" ,
resp . StatusCode )
}
func ( p * AWS ) readURLv1 ( url string ) ( * http . Response , error ) {
client := http . Client { }
2020-05-19 22:42:12 +00:00
2020-07-23 01:39:46 +00:00
req , err := http . NewRequest ( http . MethodGet , url , http . NoBody )
2020-05-20 17:15:51 +00:00
if err != nil {
return nil , err
}
resp , err := client . Do ( req )
if err != nil {
return nil , err
}
return resp , nil
}
func ( p * AWS ) readURLv2 ( url string ) ( * http . Response , error ) {
client := http . Client { }
// first get the token
2020-05-20 13:03:35 +00:00
req , err := http . NewRequest ( http . MethodPut , p . config . tokenURL , nil )
2020-05-19 22:42:12 +00:00
if err != nil {
return nil , err
}
2020-05-20 13:03:35 +00:00
req . Header . Set ( awsMetadataTokenTTLHeader , p . config . tokenTTL )
2020-05-20 17:15:51 +00:00
resp , err := client . Do ( req )
2019-04-24 19:12:36 +00:00
if err != nil {
return nil , err
}
2020-05-20 17:15:51 +00:00
defer resp . Body . Close ( )
if resp . StatusCode >= 400 {
return nil , fmt . Errorf ( "Request for API token returned non-successful status code %d" , resp . StatusCode )
2020-05-20 03:57:09 +00:00
}
2020-05-20 17:15:51 +00:00
token , err := ioutil . ReadAll ( resp . Body )
2019-04-24 19:12:36 +00:00
if err != nil {
return nil , err
2020-05-19 22:42:12 +00:00
}
2020-05-20 17:15:51 +00:00
// now make the request
2020-07-23 01:39:46 +00:00
req , err = http . NewRequest ( http . MethodGet , url , http . NoBody )
2020-05-19 22:42:12 +00:00
if err != nil {
return nil , err
}
2020-05-20 17:15:51 +00:00
req . Header . Set ( awsMetadataTokenHeader , string ( token ) )
resp , err = client . Do ( req )
2020-05-19 22:42:12 +00:00
if err != nil {
return nil , err
}
2020-05-20 17:15:51 +00:00
return resp , nil
}
func ( p * AWS ) readResponseBody ( resp * http . Response ) ( [ ] byte , error ) {
defer resp . Body . Close ( )
b , err := ioutil . ReadAll ( resp . Body )
2019-04-24 19:12:36 +00:00
if err != nil {
return nil , err
}
return b , nil
}
// authorizeToken performs common jwt authorization actions and returns the
// claims for case specific downstream parsing.
// e.g. a Sign request will auth/validate different fields than a Revoke request.
func ( p * AWS ) authorizeToken ( token string ) ( * awsPayload , error ) {
jwt , err := jose . ParseSigned ( token )
if err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrapf ( http . StatusUnauthorized , err , "aws.authorizeToken; error parsing aws token" )
2019-04-24 19:12:36 +00:00
}
if len ( jwt . Headers ) == 0 {
2020-01-24 06:04:34 +00:00
return nil , errs . InternalServer ( "aws.authorizeToken; error parsing token, header is missing" )
2019-04-24 19:12:36 +00:00
}
var unsafeClaims awsPayload
if err := jwt . UnsafeClaimsWithoutVerification ( & unsafeClaims ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusUnauthorized , err , "aws.authorizeToken; error unmarshaling claims" )
2019-04-24 19:12:36 +00:00
}
var payload awsPayload
if err := jwt . Claims ( unsafeClaims . Amazon . Signature , & payload ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusUnauthorized , err , "aws.authorizeToken; error verifying claims" )
2019-04-24 19:12:36 +00:00
}
// Validate identity document signature
if err := p . checkSignature ( payload . Amazon . Document , payload . Amazon . Signature ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusUnauthorized , err , "aws.authorizeToken; invalid aws token signature" )
2019-04-24 19:12:36 +00:00
}
var doc awsInstanceIdentityDocument
if err := json . Unmarshal ( payload . Amazon . Document , & doc ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusUnauthorized , err , "aws.authorizeToken; error unmarshaling aws identity document" )
2019-04-24 19:12:36 +00:00
}
switch {
case doc . AccountID == "" :
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document accountId cannot be empty" )
2019-04-24 19:12:36 +00:00
case doc . InstanceID == "" :
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document instanceId cannot be empty" )
2019-04-24 19:12:36 +00:00
case doc . PrivateIP == "" :
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document privateIp cannot be empty" )
2019-04-24 19:12:36 +00:00
case doc . Region == "" :
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document region cannot be empty" )
2019-04-24 19:12:36 +00:00
}
2019-04-25 02:52:58 +00:00
// According to "rfc7519 JSON Web Token" acceptable skew should be no
// more than a few minutes.
2019-06-04 23:31:33 +00:00
now := time . Now ( ) . UTC ( )
2019-04-25 02:52:58 +00:00
if err = payload . ValidateWithLeeway ( jose . Expected {
2019-07-15 22:52:36 +00:00
Issuer : awsIssuer ,
Time : now ,
2019-04-25 02:52:58 +00:00
} , time . Minute ) ; err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrapf ( http . StatusUnauthorized , err , "aws.authorizeToken; invalid aws token" )
2019-04-25 02:52:58 +00:00
}
2019-06-06 19:49:51 +00:00
// validate audiences with the defaults
if ! matchesAudience ( payload . Audience , p . audiences . Sign ) {
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; invalid token - invalid audience claim (aud)" )
2019-06-06 19:49:51 +00:00
}
2019-07-15 22:52:36 +00:00
// Validate subject, it has to be known if disableCustomSANs is enabled
if p . DisableCustomSANs {
if payload . Subject != doc . InstanceID &&
payload . Subject != doc . PrivateIP &&
payload . Subject != fmt . Sprintf ( "ip-%s.%s.compute.internal" , strings . Replace ( doc . PrivateIP , "." , "-" , - 1 ) , doc . Region ) {
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; invalid token - invalid subject claim (sub)" )
2019-07-15 22:52:36 +00:00
}
}
2019-04-24 19:12:36 +00:00
// validate accounts
if len ( p . Accounts ) > 0 {
var found bool
for _ , sa := range p . Accounts {
if sa == doc . AccountID {
found = true
break
}
}
if ! found {
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; invalid aws identity document - accountId is not valid" )
2019-04-24 19:12:36 +00:00
}
}
2019-06-04 23:31:33 +00:00
// validate instance age
if d := p . InstanceAge . Value ( ) ; d > 0 {
if now . Sub ( doc . PendingTime ) > d {
2020-01-24 06:04:34 +00:00
return nil , errs . Unauthorized ( "aws.authorizeToken; aws identity document pendingTime is too old" )
2019-06-04 23:31:33 +00:00
}
}
2019-04-24 19:12:36 +00:00
payload . document = doc
return & payload , nil
}
2019-07-29 22:54:07 +00:00
2019-10-28 18:50:43 +00:00
// AuthorizeSSHSign returns the list of SignOption for a SignSSH request.
func ( p * AWS ) AuthorizeSSHSign ( ctx context . Context , token string ) ( [ ] SignOption , error ) {
if ! p . claimer . IsSSHCAEnabled ( ) {
2021-05-03 19:48:20 +00:00
return nil , errs . Unauthorized ( "aws.AuthorizeSSHSign; ssh ca is disabled for aws provisioner '%s'" , p . GetName ( ) )
2019-10-28 18:50:43 +00:00
}
claims , err := p . authorizeToken ( token )
if err != nil {
2019-12-20 21:30:05 +00:00
return nil , errs . Wrap ( http . StatusInternalServerError , err , "aws.AuthorizeSSHSign" )
2019-10-28 18:50:43 +00:00
}
2019-07-30 00:54:38 +00:00
doc := claims . document
2020-08-03 22:11:42 +00:00
signOptions := [ ] SignOption { }
// Enforce host certificate.
defaults := SignSSHOptions {
CertType : SSHHostCert ,
}
2019-07-30 00:54:38 +00:00
2020-07-30 01:05:35 +00:00
// Validated principals.
principals := [ ] string {
doc . PrivateIP ,
fmt . Sprintf ( "ip-%s.%s.compute.internal" , strings . Replace ( doc . PrivateIP , "." , "-" , - 1 ) , doc . Region ) ,
}
2020-03-05 22:33:42 +00:00
// Only enforce known principals if disable custom sans is true.
if p . DisableCustomSANs {
2020-07-30 01:05:35 +00:00
defaults . Principals = principals
2020-08-03 22:11:42 +00:00
} else {
// Check that at least one principal is sent in the request.
signOptions = append ( signOptions , & sshCertOptionsRequireValidator {
Principals : true ,
} )
2020-03-05 22:33:42 +00:00
}
2020-07-30 01:05:35 +00:00
// Certificate templates.
data := sshutil . CreateTemplateData ( sshutil . HostCert , doc . InstanceID , principals )
if v , err := unsafeParseSigned ( token ) ; err == nil {
data . SetToken ( v )
}
2020-08-10 18:26:51 +00:00
templateOptions , err := CustomSSHTemplateOptions ( p . Options , data , sshutil . DefaultIIDTemplate )
2020-07-30 01:05:35 +00:00
if err != nil {
return nil , errs . Wrap ( http . StatusInternalServerError , err , "aws.AuthorizeSSHSign" )
}
signOptions = append ( signOptions , templateOptions )
2019-07-30 00:54:38 +00:00
return append ( signOptions ,
2020-08-03 22:11:42 +00:00
// Validate user SignSSHOptions.
sshCertOptionsValidator ( defaults ) ,
2019-09-05 01:31:09 +00:00
// Set the validity bounds if not set.
2020-01-04 02:22:02 +00:00
& sshDefaultDuration { p . claimer } ,
2019-09-05 01:31:09 +00:00
// Validate public key
2019-09-11 00:04:13 +00:00
& sshDefaultPublicKeyValidator { } ,
2019-09-05 01:31:09 +00:00
// Validate the validity period.
2020-01-24 06:04:34 +00:00
& sshCertValidityValidator { p . claimer } ,
2019-09-05 01:31:09 +00:00
// Require all the fields in the SSH certificate
2020-01-24 06:04:34 +00:00
& sshCertDefaultValidator { } ,
2019-07-30 00:54:38 +00:00
) , nil
2019-07-29 22:54:07 +00:00
}