diff --git a/lib/promscrape/discovery/ec2/api.go b/lib/promscrape/discovery/ec2/api.go
index e20c59edf..c407a2afa 100644
--- a/lib/promscrape/discovery/ec2/api.go
+++ b/lib/promscrape/discovery/ec2/api.go
@@ -18,13 +18,17 @@ import (
const (
awsAccessKeyEnv = "AWS_ACCESS_KEY_ID"
awsSecretKeyEnv = "AWS_SECRET_ACCESS_KEY"
+ awsRegionEnv = "AWS_REGION"
+ awsRoleARNEnv = "AWS_ROLE_ARN"
+ awsWITPath = "AWS_WEB_IDENTITY_TOKEN_FILE"
)
type apiConfig struct {
- region string
- roleARN string
- filters string
- port int
+ region string
+ roleARN string
+ webTokenPath string
+ filters string
+ port int
ec2Endpoint string
stsEndpoint string
@@ -79,6 +83,14 @@ func newAPIConfig(sdc *SDConfig) (*apiConfig, error) {
cfg.ec2Endpoint = buildAPIEndpoint(sdc.Endpoint, region, "ec2")
cfg.stsEndpoint = buildAPIEndpoint(sdc.Endpoint, region, "sts")
+ envARN := os.Getenv(awsRoleARNEnv)
+ if envARN != "" {
+ cfg.roleARN = envARN
+ }
+ cfg.webTokenPath = os.Getenv(awsWITPath)
+ if cfg.webTokenPath != "" && cfg.roleARN == "" {
+ return nil, fmt.Errorf("roleARN is missing for %q, set it with cfg or env var %q", awsWITPath, awsRoleARNEnv)
+ }
// explicitly set credentials has priority over env variables
cfg.defaultAccessKey = os.Getenv(awsAccessKeyEnv)
cfg.defaultSecretKey = os.Getenv(awsSecretKeyEnv)
@@ -108,6 +120,11 @@ func getFiltersQueryString(filters []Filter) string {
}
func getDefaultRegion() (string, error) {
+ envRegion := os.Getenv(awsRegionEnv)
+ if envRegion != "" {
+ return envRegion, nil
+ }
+
data, err := getMetadataByPath("dynamic/instance-identity/document")
if err != nil {
return "", err
@@ -156,6 +173,13 @@ func getAPICredentials(cfg *apiConfig) (*apiCredentials, error) {
AccessKeyID: cfg.defaultAccessKey,
SecretAccessKey: cfg.defaultSecretKey,
}
+ 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))
+ }
// we need instance credentials if dont have access keys
if len(acNew.AccessKeyID) == 0 && len(acNew.SecretAccessKey) == 0 {
@@ -263,42 +287,73 @@ func getMetadataByPath(apiPath string) ([]byte, error) {
return readResponseBody(resp, apiURL)
}
-// getRoleARNCredentials obtains credentials fo the given roleARN.
-func getRoleARNCredentials(region, stsEndpoint, roleARN string, creds *apiCredentials) (*apiCredentials, error) {
- data, err := getSTSAPIResponse(region, stsEndpoint, roleARN, creds)
+// 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) {
+ apiURL += fmt.Sprintf("&WebIdentityToken=%s", token)
+ return http.NewRequest("GET", apiURL, nil)
+ })
if err != nil {
return nil, err
}
- return parseARNCredentials(data)
+ return parseARNCredentials(data, "AssumeRoleWithWebIdentity")
+}
+
+// getRoleARNCredentials obtains credentials fo the given roleARN.
+func getRoleARNCredentials(region, stsEndpoint, roleARN string, creds *apiCredentials) (*apiCredentials, error) {
+ data, err := getSTSAPIResponse("AssumeRole", stsEndpoint, roleARN, func(apiURL string) (*http.Request, error) {
+ return newSignedRequest(apiURL, "sts", region, creds)
+ })
+ if err != nil {
+ return nil, err
+ }
+ return parseARNCredentials(data, "AssumeRole")
}
// parseARNCredentials parses apiCredentials from AssumeRole response.
//
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
-func parseARNCredentials(data []byte) (*apiCredentials, error) {
+func parseARNCredentials(data []byte, role string) (*apiCredentials, error) {
var arr AssumeRoleResponse
if err := xml.Unmarshal(data, &arr); err != nil {
return nil, fmt.Errorf("cannot parse AssumeRoleResponse response from %q: %w", data, err)
}
+ var cred assumeCredentials
+ switch role {
+ case "AssumeRole":
+ cred = arr.AssumeRoleResult.Credentials
+ case "AssumeRoleWithWebIdentity":
+ cred = arr.AssumeRoleWithWebIdentityResult.Credentials
+ default:
+ return nil, fmt.Errorf("bug, unexpected role: %q", role)
+ }
return &apiCredentials{
- AccessKeyID: arr.AssumeRoleResult.Credentials.AccessKeyID,
- SecretAccessKey: arr.AssumeRoleResult.Credentials.SecretAccessKey,
- Token: arr.AssumeRoleResult.Credentials.SessionToken,
- Expiration: arr.AssumeRoleResult.Credentials.Expiration,
+ AccessKeyID: cred.AccessKeyID,
+ SecretAccessKey: cred.SecretAccessKey,
+ Token: cred.SessionToken,
+ Expiration: cred.Expiration,
}, nil
}
+type assumeCredentials struct {
+ AccessKeyID string `xml:"AccessKeyId"`
+ SecretAccessKey string `xml:"SecretAccessKey"`
+ SessionToken string `xml:"SessionToken"`
+ Expiration time.Time `xml:"Expiration"`
+}
+
// AssumeRoleResponse represents AssumeRole response
//
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
type AssumeRoleResponse struct {
AssumeRoleResult struct {
- Credentials struct {
- AccessKeyID string `xml:"AccessKeyId"`
- SecretAccessKey string `xml:"SecretAccessKey"`
- SessionToken string `xml:"SessionToken"`
- Expiration time.Time `xml:"Expiration"`
- }
+ Credentials assumeCredentials
+ }
+ AssumeRoleWithWebIdentityResult struct {
+ Credentials assumeCredentials
}
}
@@ -323,14 +378,14 @@ func buildAPIEndpoint(customEndpoint, region, service string) string {
// and returns temporary credentials with expiration time
//
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
-func getSTSAPIResponse(region, stsEndpoint, roleARN string, creds *apiCredentials) ([]byte, error) {
+func getSTSAPIResponse(action, stsEndpoint, roleARN string, reqBuilder func(apiURL string) (*http.Request, error)) ([]byte, error) {
// See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Query-Requests.html
- apiURL := fmt.Sprintf("%s?Action=%s", stsEndpoint, "AssumeRole")
+ apiURL := fmt.Sprintf("%s?Action=%s", stsEndpoint, action)
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"
- req, err := newSignedRequest(apiURL, "sts", region, creds)
+ req, err := reqBuilder(apiURL)
if err != nil {
return nil, fmt.Errorf("cannot create signed request: %w", err)
}
diff --git a/lib/promscrape/discovery/ec2/api_test.go b/lib/promscrape/discovery/ec2/api_test.go
index b067880b1..cd4b4a6cc 100644
--- a/lib/promscrape/discovery/ec2/api_test.go
+++ b/lib/promscrape/discovery/ec2/api_test.go
@@ -51,7 +51,7 @@ func TestParseMetadataSecurityCredentialsSuccess(t *testing.T) {
func TestParseARNCredentialsFailure(t *testing.T) {
f := func(s string) {
t.Helper()
- creds, err := parseARNCredentials([]byte(s))
+ creds, err := parseARNCredentials([]byte(s), "")
if err == nil {
t.Fatalf("expecting non-nil error")
}
@@ -64,6 +64,19 @@ func TestParseARNCredentialsFailure(t *testing.T) {
}
func TestParseARNCredentialsSuccess(t *testing.T) {
+
+ f := func(data, role string, credsExpected *apiCredentials) {
+ t.Helper()
+ creds, err := parseARNCredentials([]byte(data), role)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+
+ if !reflect.DeepEqual(creds, credsExpected) {
+ t.Fatalf("unexpected creds;\ngot\n%+v\nwant\n%+v", creds, credsExpected)
+ }
+
+ }
// See https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
s := `
@@ -90,10 +103,6 @@ func TestParseARNCredentialsSuccess(t *testing.T) {
`
- creds, err := parseARNCredentials([]byte(s))
- if err != nil {
- t.Fatalf("unexpected error: %s", err)
- }
credsExpected := &apiCredentials{
AccessKeyID: "ASIAIOSFODNN7EXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY",
@@ -106,9 +115,35 @@ func TestParseARNCredentialsSuccess(t *testing.T) {
`,
Expiration: mustParseRFC3339("2019-11-09T13:34:41Z"),
}
- if !reflect.DeepEqual(creds, credsExpected) {
- t.Fatalf("unexpected creds;\ngot\n%+v\nwant\n%+v", creds, credsExpected)
+ s2 := `
+
+ sts.amazonaws.com
+
+ AROA2X6NOXN27E3OGMK3T:vmagent-ec2-discovery
+ arn:aws:sts::111111111:assumed-role/eks-role-9N0EFKEDJ1X/vmagent-ec2-discovery
+
+ arn:aws:iam::111111111:oidc-provider/oidc.eks.eu-west-1.amazonaws.com/id/111111111
+
+ ASIABYASSDASF
+ asffasfasf/RvxIQpCid4iRMGm56nnRs2oKgV
+ asfafsassssssssss/MlyKUPOYAiEAq5HgS19Mf8SJ3kIKU3NCztDeZW5EUW4NrPrPyXQ8om0q/AQIjv//////////
+ 2021-03-01T13:38:15Z
+
+ system:serviceaccount:default:vmagent
+
+
+ 1214124-7bb0-4673-ad6d-af9e67fc1141
+
+`
+ credsExpected2 := &apiCredentials{
+ AccessKeyID: "ASIABYASSDASF",
+ SecretAccessKey: "asffasfasf/RvxIQpCid4iRMGm56nnRs2oKgV",
+ Token: "asfafsassssssssss/MlyKUPOYAiEAq5HgS19Mf8SJ3kIKU3NCztDeZW5EUW4NrPrPyXQ8om0q/AQIjv//////////",
+ Expiration: mustParseRFC3339("2021-03-01T13:38:15Z"),
}
+
+ f(s, "AssumeRole", credsExpected)
+ f(s2, "AssumeRoleWithWebIdentity", credsExpected2)
}
func mustParseRFC3339(s string) time.Time {