Skip to content

Commit 30b93c5

Browse files
agithomasEC2 Default User
authored andcommitted
Add support for EntraID authentication
Made-with: Cursor
1 parent acec49c commit 30b93c5

File tree

5 files changed

+194
-2
lines changed

5 files changed

+194
-2
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# REQUIRED
2+
# Kind can be one of:
3+
# - breaking-change: a change to previously-documented behavior
4+
# - deprecation: functionality that is being removed in a later release
5+
# - bug-fix: fixes a problem in a previous version
6+
# - enhancement: extends functionality but does not break or fix existing behavior
7+
# - feature: new functionality
8+
# - known-issue: problems that we are aware of in a given version
9+
# - security: impacts on the security of a product or a user’s deployment.
10+
# - upgrade: important information for someone upgrading from a prior version
11+
# - other: does not fit into any of the other categories
12+
kind: feature
13+
14+
# REQUIRED for all kinds
15+
# Change summary; a 80ish characters long description of the change.
16+
summary: Add Azure Entra ID federated authentication for AWS via STS AssumeRoleWithWebIdentity
17+
18+
# REQUIRED for breaking-change, deprecation, known-issue
19+
# Long description; in case the summary is not enough to describe the change
20+
# this field accommodate a description without length limits.
21+
# description:
22+
23+
# REQUIRED for breaking-change, deprecation, known-issue
24+
# impact:
25+
26+
# REQUIRED for breaking-change, deprecation, known-issue
27+
# action:
28+
29+
# REQUIRED for all kinds
30+
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
31+
component: all
32+
33+
# AUTOMATED
34+
# OPTIONAL to manually add other PR URLs
35+
# PR URL: A link the PR that added the changeset.
36+
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
37+
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
38+
# Please provide it if you are adding a fragment for a different PR.
39+
# pr: https://github.com/owner/repo/1234
40+
41+
# AUTOMATED
42+
# OPTIONAL to manually add other issue URLs
43+
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
44+
# If not present is automatically filled by the tooling with the issue linked to the PR number.
45+
# issue: https://github.com/owner/repo/1234
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package aws
6+
7+
import (
8+
awssdk "github.com/aws/aws-sdk-go-v2/aws"
9+
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
10+
"github.com/aws/aws-sdk-go-v2/service/sts"
11+
12+
azureidentity "github.com/elastic/beats/v7/x-pack/libbeat/common/azure/identity"
13+
"github.com/elastic/elastic-agent-libs/logp"
14+
)
15+
16+
// addAzureADWebIdentityCredentials configures the AWS config to authenticate
17+
// using an Azure AD token via STS AssumeRoleWithWebIdentity.
18+
func addAzureADWebIdentityCredentials(config ConfigAWS, awsConfig *awssdk.Config, logger *logp.Logger) error {
19+
logger = logger.Named("addAzureADWebIdentityCredentials")
20+
logger.Debug("Switching credentials provider to Azure AD web identity")
21+
22+
tokenProvider, err := azureidentity.NewTokenProvider(config.AzureAD)
23+
if err != nil {
24+
return err
25+
}
26+
27+
provider := stscreds.NewWebIdentityRoleProvider(
28+
sts.NewFromConfig(*awsConfig),
29+
config.RoleArn,
30+
tokenProvider,
31+
func(opt *stscreds.WebIdentityRoleOptions) {
32+
if config.AssumeRoleDuration > 0 {
33+
opt.Duration = config.AssumeRoleDuration
34+
}
35+
},
36+
)
37+
38+
awsConfig.Credentials = awssdk.NewCredentialsCache(provider, func(options *awssdk.CredentialsCacheOptions) {
39+
if config.AssumeRoleExpiryWindow > 0 {
40+
options.ExpiryWindow = config.AssumeRoleExpiryWindow
41+
}
42+
})
43+
44+
return nil
45+
}

x-pack/libbeat/common/aws/credentials.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
awsConfig "github.com/aws/aws-sdk-go-v2/config"
2121
"github.com/aws/aws-sdk-go-v2/credentials"
2222

23+
azureidentity "github.com/elastic/beats/v7/x-pack/libbeat/common/azure/identity"
2324
"github.com/elastic/elastic-agent-libs/logp"
2425
"github.com/elastic/elastic-agent-libs/transport/httpcommon"
2526
"github.com/elastic/elastic-agent-libs/transport/tlscommon"
@@ -56,6 +57,10 @@ type ConfigAWS struct {
5657
// UseCloudConnectors indicates whether the cloud connectors flow is used.
5758
// If this is true, the InitializeAWSConfig should initialize the AWS cloud connector role chaining flow.
5859
UseCloudConnectors bool `config:"use_cloud_connectors"`
60+
61+
// AzureAD enables Azure AD federated authentication via STS AssumeRoleWithWebIdentity.
62+
// Requires role_arn to be set.
63+
AzureAD azureidentity.Config `config:"azure_ad"`
5964
}
6065

6166
// InitializeAWSConfig function creates the awssdk.Config object from the provided config
@@ -69,8 +74,13 @@ func InitializeAWSConfig(beatsConfig ConfigAWS, logger *logp.Logger) (awssdk.Con
6974
}
7075
}
7176

72-
// Assume IAM role if iam_role config parameter is given
73-
if beatsConfig.RoleArn != "" && !beatsConfig.UseCloudConnectors {
77+
// Azure AD federated authentication via STS AssumeRoleWithWebIdentity.
78+
if beatsConfig.AzureAD.Enabled() && beatsConfig.RoleArn != "" {
79+
if err := addAzureADWebIdentityCredentials(beatsConfig, &awsConfig, logger); err != nil {
80+
return awsConfig, fmt.Errorf("failed to initialize Azure AD web identity credentials: %w", err)
81+
}
82+
} else if beatsConfig.RoleArn != "" && !beatsConfig.UseCloudConnectors {
83+
// Assume IAM role if iam_role config parameter is given
7484
addAssumeRoleProviderToAwsConfig(beatsConfig, &awsConfig, logger)
7585
}
7686

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package identity
6+
7+
// Config holds Azure AD credentials for obtaining OAuth2 tokens.
8+
type Config struct {
9+
TenantID string `config:"tenant_id"`
10+
ClientID string `config:"client_id"`
11+
ClientSecret string `config:"client_secret"`
12+
13+
// Scope is the OAuth2 scope to request, e.g. "api://<app-id>/.default".
14+
Scope string `config:"scope"`
15+
}
16+
17+
// Enabled returns true when the minimum required fields are set.
18+
func (c *Config) Enabled() bool {
19+
return c.TenantID != "" && c.ClientID != ""
20+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package identity
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"time"
11+
12+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
13+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
14+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
15+
)
16+
17+
const defaultTokenTimeout = 30 * time.Second
18+
19+
// TokenProvider retrieves OAuth2 access tokens from Azure AD.
20+
// It is safe for concurrent use; the underlying azidentity credential
21+
// handles token caching and refresh internally.
22+
type TokenProvider struct {
23+
cred azcore.TokenCredential
24+
scope string
25+
}
26+
27+
// NewTokenProvider builds a TokenProvider from the given config.
28+
// When ClientSecret is provided it uses ClientSecretCredential.
29+
// Otherwise it falls back to DefaultAzureCredential which tries
30+
// managed identity, Azure CLI, environment variables, etc.
31+
func NewTokenProvider(cfg Config) (*TokenProvider, error) {
32+
var cred azcore.TokenCredential
33+
var err error
34+
35+
if cfg.ClientSecret != "" {
36+
cred, err = azidentity.NewClientSecretCredential(cfg.TenantID, cfg.ClientID, cfg.ClientSecret, nil)
37+
} else {
38+
cred, err = azidentity.NewDefaultAzureCredential(nil)
39+
}
40+
if err != nil {
41+
return nil, fmt.Errorf("azure identity: failed to create credential: %w", err)
42+
}
43+
44+
return &TokenProvider{
45+
cred: cred,
46+
scope: cfg.Scope,
47+
}, nil
48+
}
49+
50+
// Token retrieves a fresh access token from Azure AD.
51+
func (p *TokenProvider) Token(ctx context.Context) (string, error) {
52+
tk, err := p.cred.GetToken(ctx, policy.TokenRequestOptions{
53+
Scopes: []string{p.scope},
54+
})
55+
if err != nil {
56+
return "", fmt.Errorf("azure identity: failed to get token: %w", err)
57+
}
58+
return tk.Token, nil
59+
}
60+
61+
// GetIdentityToken implements the stscreds.IdentityTokenRetriever interface
62+
// from the AWS SDK, returning the Azure AD token as bytes.
63+
func (p *TokenProvider) GetIdentityToken() ([]byte, error) {
64+
ctx, cancel := context.WithTimeout(context.Background(), defaultTokenTimeout)
65+
defer cancel()
66+
67+
token, err := p.Token(ctx)
68+
if err != nil {
69+
return nil, err
70+
}
71+
return []byte(token), nil
72+
}

0 commit comments

Comments
 (0)