2020-04-27 18:25:45 +02:00
package ec2
import (
"encoding/json"
2020-09-21 15:04:15 +02:00
"encoding/xml"
2020-04-27 18:25:45 +02:00
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
2020-09-21 17:42:53 +02:00
"sync"
2020-09-21 15:04:15 +02:00
"time"
2020-04-29 16:27:08 +02:00
2021-03-02 12:46:26 +01:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
2020-04-29 16:27:08 +02:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
2020-04-27 18:25:45 +02:00
)
type apiConfig struct {
2021-03-02 12:27:09 +01:00
region string
roleARN string
webTokenPath string
filters string
port int
2020-09-21 17:42:53 +02:00
ec2Endpoint string
stsEndpoint string
// these keys are needed for obtaining creds.
2020-09-21 15:04:15 +02:00
defaultAccessKey string
defaultSecretKey string
2020-09-21 17:42:53 +02:00
// Real credentials used for accessing EC2 API.
creds * apiCredentials
credsLock sync . Mutex
2020-09-21 15:04:15 +02:00
}
// apiCredentials represents aws api credentials
type apiCredentials struct {
2020-09-21 17:42:53 +02:00
AccessKeyID string
SecretAccessKey string
Token string
Expiration time . Time
2020-04-27 18:25:45 +02:00
}
2020-05-04 15:21:24 +02:00
var configMap = discoveryutils . NewConfigMap ( )
2020-04-27 18:25:45 +02:00
2020-05-04 15:21:24 +02:00
func getAPIConfig ( sdc * SDConfig ) ( * apiConfig , error ) {
2020-09-21 17:42:53 +02:00
v , err := configMap . Get ( sdc , func ( ) ( interface { } , error ) { return newAPIConfig ( sdc ) } )
2020-04-27 18:25:45 +02:00
if err != nil {
return nil , err
}
2020-05-04 15:21:24 +02:00
return v . ( * apiConfig ) , nil
2020-04-27 18:25:45 +02:00
}
func newAPIConfig ( sdc * SDConfig ) ( * apiConfig , error ) {
region := sdc . Region
if len ( region ) == 0 {
r , err := getDefaultRegion ( )
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot determine default ec2 region; probably, `region` param in `ec2_sd_configs` is missing; the error: %w" , err )
2020-04-27 18:25:45 +02:00
}
region = r
}
filters := getFiltersQueryString ( sdc . Filters )
port := 80
if sdc . Port != nil {
port = * sdc . Port
}
2020-09-21 15:04:15 +02:00
cfg := & apiConfig {
2020-09-21 17:42:53 +02:00
region : region ,
roleARN : sdc . RoleARN ,
filters : filters ,
port : port ,
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
cfg . ec2Endpoint = buildAPIEndpoint ( sdc . Endpoint , region , "ec2" )
cfg . stsEndpoint = buildAPIEndpoint ( sdc . Endpoint , region , "sts" )
2021-03-02 12:46:26 +01:00
if cfg . roleARN == "" {
cfg . roleARN = os . Getenv ( "AWS_ROLE_ARN" )
2021-03-02 12:27:09 +01:00
}
2021-03-02 12:46:26 +01:00
cfg . webTokenPath = os . Getenv ( "AWS_WEB_IDENTITY_TOKEN_FILE" )
2021-03-02 12:27:09 +01:00
if cfg . webTokenPath != "" && cfg . roleARN == "" {
2021-03-02 12:46:26 +01:00
return nil , fmt . Errorf ( "roleARN is missing for AWS_WEB_IDENTITY_TOKEN_FILE=%q, set it either in `ec2_sd_config` or via env var AWS_ROLE_ARN" , cfg . webTokenPath )
2021-03-02 12:27:09 +01:00
}
2020-09-21 15:04:15 +02:00
// explicitly set credentials has priority over env variables
2021-03-02 12:46:26 +01:00
cfg . defaultAccessKey = os . Getenv ( "AWS_ACCESS_KEY_ID" )
cfg . defaultSecretKey = os . Getenv ( "AWS_SECRET_ACCESS_KEY" )
2020-09-21 15:04:15 +02:00
if len ( sdc . AccessKey ) > 0 {
cfg . defaultAccessKey = sdc . AccessKey
}
if len ( sdc . SecretKey ) > 0 {
cfg . defaultSecretKey = sdc . SecretKey
}
2020-09-21 17:42:53 +02:00
cfg . creds = & apiCredentials {
AccessKeyID : cfg . defaultAccessKey ,
SecretAccessKey : cfg . defaultSecretKey ,
2020-09-21 15:04:15 +02:00
}
return cfg , nil
2020-04-27 18:25:45 +02:00
}
func getFiltersQueryString ( filters [ ] Filter ) string {
// See how to build filters query string at examples at https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html
var args [ ] string
for i , f := range filters {
args = append ( args , fmt . Sprintf ( "Filter.%d.Name=%s" , i + 1 , url . QueryEscape ( f . Name ) ) )
for j , v := range f . Values {
args = append ( args , fmt . Sprintf ( "Filter.%d.Value.%d=%s" , i + 1 , j + 1 , url . QueryEscape ( v ) ) )
}
}
return strings . Join ( args , "&" )
}
func getDefaultRegion ( ) ( string , error ) {
2021-03-02 12:46:26 +01:00
envRegion := os . Getenv ( "AWS_REGION" )
2021-03-02 12:27:09 +01:00
if envRegion != "" {
return envRegion , nil
}
2020-04-27 18:25:45 +02:00
data , err := getMetadataByPath ( "dynamic/instance-identity/document" )
if err != nil {
return "" , err
}
var id IdentityDocument
if err := json . Unmarshal ( data , & id ) ; err != nil {
2020-06-30 21:58:18 +02:00
return "" , fmt . Errorf ( "cannot parse identity document: %w" , err )
2020-04-27 18:25:45 +02:00
}
return id . Region , nil
}
2020-09-21 17:42:53 +02:00
// IdentityDocument is identity document returned from AWS metadata server.
//
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html
type IdentityDocument struct {
Region string
}
// getFreshAPICredentials returns fresh EC2 API credentials.
//
// The credentials are refreshed if needed.
func ( cfg * apiConfig ) getFreshAPICredentials ( ) ( * apiCredentials , error ) {
cfg . credsLock . Lock ( )
defer cfg . credsLock . Unlock ( )
2020-09-21 15:04:15 +02:00
2020-09-21 17:42:53 +02:00
if len ( cfg . defaultAccessKey ) > 0 && len ( cfg . defaultSecretKey ) > 0 && len ( cfg . roleARN ) == 0 {
// There is no need in refreshing statically set api credentials if `role_arn` isn't set.
return cfg . creds , nil
}
if time . Until ( cfg . creds . Expiration ) > 10 * time . Second {
// Credentials aren't expired yet.
return cfg . creds , nil
}
// Credentials have been expired. Update them.
ac , err := getAPICredentials ( cfg )
if err != nil {
return nil , err
}
cfg . creds = ac
return ac , nil
}
// getAPICredentials obtains new EC2 API credentials from instance metadata and role_arn.
func getAPICredentials ( cfg * apiConfig ) ( * apiCredentials , error ) {
acNew := & apiCredentials {
2020-09-21 15:04:15 +02:00
AccessKeyID : cfg . defaultAccessKey ,
SecretAccessKey : cfg . defaultSecretKey ,
}
2021-03-02 12:27:09 +01:00
if len ( cfg . webTokenPath ) > 0 {
token , err := ioutil . ReadFile ( cfg . webTokenPath )
if err != nil {
return nil , fmt . Errorf ( "cannot read webToken from path: %q, err: %w" , cfg . webTokenPath , err )
}
return getRoleWebIdentityCredentials ( cfg . stsEndpoint , cfg . roleARN , string ( token ) )
}
2020-09-21 15:04:15 +02:00
2020-09-21 17:42:53 +02:00
// we need instance credentials if dont have access keys
if len ( acNew . AccessKeyID ) == 0 && len ( acNew . SecretAccessKey ) == 0 {
2020-09-21 15:04:15 +02:00
ac , err := getInstanceRoleCredentials ( )
if err != nil {
2020-09-21 17:42:53 +02:00
return nil , err
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
acNew = ac
2020-09-21 15:04:15 +02:00
}
// read credentials from sts api, if role_arn is defined
2020-09-21 17:42:53 +02:00
if len ( cfg . roleARN ) > 0 {
ac , err := getRoleARNCredentials ( cfg . region , cfg . stsEndpoint , cfg . roleARN , acNew )
2020-09-21 15:04:15 +02:00
if err != nil {
2020-09-21 17:42:53 +02:00
return nil , fmt . Errorf ( "cannot get credentials for role_arn %q: %w" , cfg . roleARN , err )
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
acNew = ac
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
if len ( acNew . AccessKeyID ) == 0 {
2021-03-02 12:46:26 +01:00
return nil , fmt . Errorf ( "missing `access_key`, you can set it with env var AWS_ACCESS_KEY_ID, " +
"directly at `ec2_sd_config` as `access_key` or use instance iam role" )
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
if len ( acNew . SecretAccessKey ) == 0 {
2021-03-02 12:46:26 +01:00
return nil , fmt . Errorf ( "missing `secret_key`, you can set it with env var AWS_SECRET_ACCESS_KEY," +
"directly at `ec2_sd_config` as `secret_key` or use instance iam role" )
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
return acNew , nil
2020-09-21 15:04:15 +02:00
}
// getInstanceRoleCredentials makes request to local ec2 instance metadata service
// and tries to retrieve credentials from assigned iam role.
2020-09-21 17:42:53 +02:00
//
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
2020-09-21 15:04:15 +02:00
func getInstanceRoleCredentials ( ) ( * apiCredentials , error ) {
instanceRoleName , err := getMetadataByPath ( "meta-data/iam/security-credentials/" )
if err != nil {
return nil , fmt . Errorf ( "cannot get instanceRoleName: %w" , err )
}
2020-09-21 17:42:53 +02:00
data , err := getMetadataByPath ( "meta-data/iam/security-credentials/" + string ( instanceRoleName ) )
2020-09-21 15:04:15 +02:00
if err != nil {
2020-09-21 17:42:53 +02:00
return nil , fmt . Errorf ( "cannot get security credentails for instanceRoleName %q: %w" , instanceRoleName , err )
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
return parseMetadataSecurityCredentials ( data )
}
// parseMetadataSecurityCredentials parses apiCredentials from metadata response to http://169.254.169.254/latest/meta-data/iam/security-credentials/*
//
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
func parseMetadataSecurityCredentials ( data [ ] byte ) ( * apiCredentials , error ) {
var msc MetadataSecurityCredentials
if err := json . Unmarshal ( data , & msc ) ; err != nil {
return nil , fmt . Errorf ( "cannot parse metadata security credentials from %q: %w" , data , err )
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
return & apiCredentials {
AccessKeyID : msc . AccessKeyID ,
SecretAccessKey : msc . SecretAccessKey ,
Token : msc . Token ,
Expiration : msc . Expiration ,
} , nil
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
// MetadataSecurityCredentials represents credentials obtained from http://169.254.169.254/latest/meta-data/iam/security-credentials/*
2020-04-27 18:25:45 +02:00
//
2020-09-21 17:42:53 +02:00
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
type MetadataSecurityCredentials struct {
AccessKeyID string ` json:"AccessKeyId" `
SecretAccessKey string ` json:"SecretAccessKey" `
Token string ` json:"Token" `
Expiration time . Time ` json:"Expiration" `
2020-04-27 18:25:45 +02:00
}
2020-09-21 15:04:15 +02:00
// getMetadataByPath returns instance metadata by url path
2020-04-27 18:25:45 +02:00
func getMetadataByPath ( apiPath string ) ( [ ] byte , error ) {
// See https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
2020-09-21 17:42:53 +02:00
client := discoveryutils . GetHTTPClient ( )
2020-04-27 18:25:45 +02:00
// Obtain session token
sessionTokenURL := "http://169.254.169.254/latest/api/token"
req , err := http . NewRequest ( "PUT" , sessionTokenURL , nil )
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot create request for IMDSv2 session token at url %q: %w" , sessionTokenURL , err )
2020-04-27 18:25:45 +02:00
}
req . Header . Set ( "X-aws-ec2-metadata-token-ttl-seconds" , "60" )
2020-09-21 17:42:53 +02:00
resp , err := client . Do ( req )
2020-04-27 18:25:45 +02:00
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot obtain IMDSv2 session token from %q; probably, `region` is missing in `ec2_sd_config`; error: %w" , sessionTokenURL , err )
2020-04-27 18:25:45 +02:00
}
token , err := readResponseBody ( resp , sessionTokenURL )
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot read IMDSv2 session token from %q; probably, `region` is missing in `ec2_sd_config`; error: %w" , sessionTokenURL , err )
2020-04-27 18:25:45 +02:00
}
// Use session token in the request.
apiURL := "http://169.254.169.254/latest/" + apiPath
req , err = http . NewRequest ( "GET" , apiURL , nil )
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot create request to %q: %w" , apiURL , err )
2020-04-27 18:25:45 +02:00
}
req . Header . Set ( "X-aws-ec2-metadata-token" , string ( token ) )
2020-09-21 17:42:53 +02:00
resp , err = client . Do ( req )
2020-04-27 18:25:45 +02:00
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot obtain response for %q: %w" , apiURL , err )
2020-04-27 18:25:45 +02:00
}
return readResponseBody ( resp , apiURL )
}
2021-03-02 12:27:09 +01:00
// getRoleWebIdentityCredentials obtains credentials fo the given roleARN with webToken.
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
// aws IRSA for kubernetes.
// https://aws.amazon.com/blogs/opensource/introducing-fine-grained-iam-roles-service-accounts/
func getRoleWebIdentityCredentials ( stsEndpoint , roleARN string , token string ) ( * apiCredentials , error ) {
data , err := getSTSAPIResponse ( "AssumeRoleWithWebIdentity" , stsEndpoint , roleARN , func ( apiURL string ) ( * http . Request , error ) {
2021-03-02 12:46:26 +01:00
apiURL += fmt . Sprintf ( "&WebIdentityToken=%s" , url . QueryEscape ( token ) )
2021-03-02 12:27:09 +01:00
return http . NewRequest ( "GET" , apiURL , nil )
} )
if err != nil {
return nil , err
}
return parseARNCredentials ( data , "AssumeRoleWithWebIdentity" )
}
2020-09-21 17:42:53 +02:00
// getRoleARNCredentials obtains credentials fo the given roleARN.
func getRoleARNCredentials ( region , stsEndpoint , roleARN string , creds * apiCredentials ) ( * apiCredentials , error ) {
2021-03-02 12:27:09 +01:00
data , err := getSTSAPIResponse ( "AssumeRole" , stsEndpoint , roleARN , func ( apiURL string ) ( * http . Request , error ) {
return newSignedRequest ( apiURL , "sts" , region , creds )
} )
2020-09-21 15:04:15 +02:00
if err != nil {
return nil , err
}
2021-03-02 12:27:09 +01:00
return parseARNCredentials ( data , "AssumeRole" )
2020-09-21 17:42:53 +02:00
}
// parseARNCredentials parses apiCredentials from AssumeRole response.
//
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
2021-03-02 12:27:09 +01:00
func parseARNCredentials ( data [ ] byte , role string ) ( * apiCredentials , error ) {
2020-09-21 17:42:53 +02:00
var arr AssumeRoleResponse
if err := xml . Unmarshal ( data , & arr ) ; err != nil {
return nil , fmt . Errorf ( "cannot parse AssumeRoleResponse response from %q: %w" , data , err )
2020-09-21 15:04:15 +02:00
}
2021-03-02 12:27:09 +01:00
var cred assumeCredentials
switch role {
case "AssumeRole" :
cred = arr . AssumeRoleResult . Credentials
case "AssumeRoleWithWebIdentity" :
cred = arr . AssumeRoleWithWebIdentityResult . Credentials
default :
2021-03-02 12:46:26 +01:00
logger . Panicf ( "BUG: unexpected role: %q" , role )
2021-03-02 12:27:09 +01:00
}
2020-09-21 15:04:15 +02:00
return & apiCredentials {
2021-03-02 12:27:09 +01:00
AccessKeyID : cred . AccessKeyID ,
SecretAccessKey : cred . SecretAccessKey ,
Token : cred . SessionToken ,
Expiration : cred . Expiration ,
2020-09-21 15:04:15 +02:00
} , nil
2020-09-21 17:42:53 +02:00
}
2020-09-21 15:04:15 +02:00
2021-03-02 12:27:09 +01:00
type assumeCredentials struct {
AccessKeyID string ` xml:"AccessKeyId" `
SecretAccessKey string ` xml:"SecretAccessKey" `
SessionToken string ` xml:"SessionToken" `
Expiration time . Time ` xml:"Expiration" `
}
2020-09-21 17:42:53 +02:00
// AssumeRoleResponse represents AssumeRole response
//
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
type AssumeRoleResponse struct {
AssumeRoleResult struct {
2021-03-02 12:27:09 +01:00
Credentials assumeCredentials
}
AssumeRoleWithWebIdentityResult struct {
Credentials assumeCredentials
2020-09-21 17:42:53 +02:00
}
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
// buildAPIEndpoint creates endpoint for aws api access
func buildAPIEndpoint ( customEndpoint , region , service string ) string {
2020-04-27 18:25:45 +02:00
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
2020-09-21 17:42:53 +02:00
if len ( customEndpoint ) == 0 {
return fmt . Sprintf ( "https://%s.%s.amazonaws.com/" , service , region )
}
endpoint := customEndpoint
// endpoint may contain only hostname. Convert it to proper url then.
if ! strings . Contains ( endpoint , "://" ) {
endpoint = "https://" + endpoint
2020-09-21 15:04:15 +02:00
}
2020-09-21 17:42:53 +02:00
if ! strings . HasSuffix ( endpoint , "/" ) {
endpoint += "/"
}
return endpoint
2020-09-21 15:04:15 +02:00
}
2021-03-02 12:46:26 +01:00
// getSTSAPIResponse makes request to aws sts api with roleARN
2020-09-21 15:04:15 +02:00
// and returns temporary credentials with expiration time
2020-09-21 17:42:53 +02:00
//
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
2021-03-02 12:27:09 +01:00
func getSTSAPIResponse ( action , stsEndpoint , roleARN string , reqBuilder func ( apiURL string ) ( * http . Request , error ) ) ( [ ] byte , error ) {
2020-09-21 15:04:15 +02:00
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
2021-03-02 12:27:09 +01:00
apiURL := fmt . Sprintf ( "%s?Action=%s" , stsEndpoint , action )
2020-09-21 15:04:15 +02:00
apiURL += "&Version=2011-06-15"
apiURL += fmt . Sprintf ( "&RoleArn=%s" , roleARN )
// we have to provide unique session name for cloudtrail audit
apiURL += "&RoleSessionName=vmagent-ec2-discovery"
2021-03-02 12:27:09 +01:00
req , err := reqBuilder ( apiURL )
2020-09-21 15:04:15 +02:00
if err != nil {
return nil , fmt . Errorf ( "cannot create signed request: %w" , err )
}
resp , err := discoveryutils . GetHTTPClient ( ) . Do ( req )
if err != nil {
return nil , fmt . Errorf ( "cannot perform http request to %q: %w" , apiURL , err )
}
return readResponseBody ( resp , apiURL )
}
2020-09-21 17:42:53 +02:00
// getEC2APIResponse performs EC2 API request with given action.
2020-09-21 15:04:15 +02:00
func getEC2APIResponse ( cfg * apiConfig , action , nextPageToken string ) ( [ ] byte , error ) {
2020-09-21 17:42:53 +02:00
ac , err := cfg . getFreshAPICredentials ( )
if err != nil {
return nil , fmt . Errorf ( "cannot obtain fresh credentials for EC2 API: %w" , err )
2020-04-27 18:25:45 +02:00
}
2020-09-21 17:42:53 +02:00
apiURL := fmt . Sprintf ( "%s?Action=%s" , cfg . ec2Endpoint , url . QueryEscape ( action ) )
2020-04-27 18:25:45 +02:00
if len ( cfg . filters ) > 0 {
apiURL += "&" + cfg . filters
}
if len ( nextPageToken ) > 0 {
apiURL += fmt . Sprintf ( "&NextToken=%s" , url . QueryEscape ( nextPageToken ) )
}
apiURL += "&Version=2013-10-15"
2020-09-21 17:42:53 +02:00
req , err := newSignedRequest ( apiURL , "ec2" , cfg . region , ac )
2020-04-27 18:25:45 +02:00
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot create signed request: %w" , err )
2020-04-27 18:25:45 +02:00
}
2020-04-29 16:27:08 +02:00
resp , err := discoveryutils . GetHTTPClient ( ) . Do ( req )
2020-04-27 18:25:45 +02:00
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot perform http request to %q: %w" , apiURL , err )
2020-04-27 18:25:45 +02:00
}
return readResponseBody ( resp , apiURL )
}
func readResponseBody ( resp * http . Response , apiURL string ) ( [ ] byte , error ) {
data , err := ioutil . ReadAll ( resp . Body )
_ = resp . Body . Close ( )
if err != nil {
2020-06-30 21:58:18 +02:00
return nil , fmt . Errorf ( "cannot read response from %q: %w" , apiURL , err )
2020-04-27 18:25:45 +02:00
}
if resp . StatusCode != http . StatusOK {
return nil , fmt . Errorf ( "unexpected status code for %q; got %d; want %d; response body: %q" ,
apiURL , resp . StatusCode , http . StatusOK , data )
}
return data , nil
}