Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions changelog/fragments/1773290855-oidc-authentication-azure.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# REQUIRED
# Kind can be one of:
# - breaking-change: a change to previously-documented behavior
# - deprecation: functionality that is being removed in a later release
# - bug-fix: fixes a problem in a previous version
# - enhancement: extends functionality but does not break or fix existing behavior
# - feature: new functionality
# - known-issue: problems that we are aware of in a given version
# - security: impacts on the security of a product or a user’s deployment.
# - upgrade: important information for someone upgrading from a prior version
# - other: does not fit into any of the other categories
kind: feature

# REQUIRED for all kinds
# Change summary; a 80ish characters long description of the change.
summary: Add Azure Entra ID federated authentication for AWS via STS AssumeRoleWithWebIdentity

# REQUIRED for breaking-change, deprecation, known-issue
# Long description; in case the summary is not enough to describe the change
# this field accommodate a description without length limits.
# description:

# REQUIRED for breaking-change, deprecation, known-issue
# impact:

# REQUIRED for breaking-change, deprecation, known-issue
# action:

# REQUIRED for all kinds
# Affected component; usually one of "elastic-agent", "fleet-server", "filebeat", "metricbeat", "auditbeat", "all", etc.
component: all

# AUTOMATED
# OPTIONAL to manually add other PR URLs
# PR URL: A link the PR that added the changeset.
# If not present is automatically filled by the tooling finding the PR where this changelog fragment has been added.
# NOTE: the tooling supports backports, so it's able to fill the original PR number instead of the backport PR number.
# Please provide it if you are adding a fragment for a different PR.
pr: https://github.com/elastic/beats/pull/49416

# AUTOMATED
# OPTIONAL to manually add other issue URLs
# Issue URL; optional; the GitHub issue related to this changeset (either closes or is part of).
# If not present is automatically filled by the tooling with the issue linked to the PR number.
# issue: https://github.com/owner/repo/1234
45 changes: 45 additions & 0 deletions x-pack/libbeat/common/aws/azure_ad_credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package aws

import (
awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts"

azureidentity "github.com/elastic/beats/v7/x-pack/libbeat/common/azure/identity"
"github.com/elastic/elastic-agent-libs/logp"
)

// addAzureADWebIdentityCredentials configures the AWS config to authenticate
// using an Azure AD token via STS AssumeRoleWithWebIdentity.
func addAzureADWebIdentityCredentials(config ConfigAWS, awsConfig *awssdk.Config, logger *logp.Logger) error {
logger = logger.Named("addAzureADWebIdentityCredentials")
logger.Debug("Switching credentials provider to Azure AD web identity")

tokenProvider, err := azureidentity.NewTokenProvider(config.AzureAD)
if err != nil {
return err
}

provider := stscreds.NewWebIdentityRoleProvider(
sts.NewFromConfig(*awsConfig),
config.RoleArn,
tokenProvider,
func(opt *stscreds.WebIdentityRoleOptions) {
if config.AssumeRoleDuration > 0 {
opt.Duration = config.AssumeRoleDuration
}
},
)

awsConfig.Credentials = awssdk.NewCredentialsCache(provider, func(options *awssdk.CredentialsCacheOptions) {
if config.AssumeRoleExpiryWindow > 0 {
options.ExpiryWindow = config.AssumeRoleExpiryWindow
}
})

return nil
}
14 changes: 12 additions & 2 deletions x-pack/libbeat/common/aws/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"

azureidentity "github.com/elastic/beats/v7/x-pack/libbeat/common/azure/identity"
"github.com/elastic/elastic-agent-libs/logp"
"github.com/elastic/elastic-agent-libs/transport/httpcommon"
"github.com/elastic/elastic-agent-libs/transport/tlscommon"
Expand Down Expand Up @@ -56,6 +57,10 @@ type ConfigAWS struct {
// UseCloudConnectors indicates whether the cloud connectors flow is used.
// If this is true, the InitializeAWSConfig should initialize the AWS cloud connector role chaining flow.
UseCloudConnectors bool `config:"use_cloud_connectors"`

// AzureAD enables Azure AD federated authentication via STS AssumeRoleWithWebIdentity.
// Requires role_arn to be set.
AzureAD azureidentity.Config `config:"azure_ad"`
}

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

// Assume IAM role if iam_role config parameter is given
if beatsConfig.RoleArn != "" && !beatsConfig.UseCloudConnectors {
// Azure AD federated authentication via STS AssumeRoleWithWebIdentity.
if beatsConfig.AzureAD.Enabled() && beatsConfig.RoleArn != "" {
if err := addAzureADWebIdentityCredentials(beatsConfig, &awsConfig, logger); err != nil {
return awsConfig, fmt.Errorf("failed to initialize Azure AD web identity credentials: %w", err)
}
} else if beatsConfig.RoleArn != "" && !beatsConfig.UseCloudConnectors {
// Assume IAM role if iam_role config parameter is given
addAssumeRoleProviderToAwsConfig(beatsConfig, &awsConfig, logger)
}

Expand Down
20 changes: 20 additions & 0 deletions x-pack/libbeat/common/azure/identity/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package identity

// Config holds Azure AD credentials for obtaining OAuth2 tokens.
type Config struct {
TenantID string `config:"tenant_id"`
ClientID string `config:"client_id"`
ClientSecret string `config:"client_secret"`

// Scope is the OAuth2 scope to request, e.g. "api://<app-id>/.default".
Scope string `config:"scope"`
}

// Enabled returns true when the minimum required fields are set.
func (c *Config) Enabled() bool {
return c.TenantID != "" && c.ClientID != ""
}
72 changes: 72 additions & 0 deletions x-pack/libbeat/common/azure/identity/token_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package identity

import (
"context"
"fmt"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
)

const defaultTokenTimeout = 30 * time.Second

// TokenProvider retrieves OAuth2 access tokens from Azure AD.
// It is safe for concurrent use; the underlying azidentity credential
// handles token caching and refresh internally.
type TokenProvider struct {
cred azcore.TokenCredential
scope string
}

// NewTokenProvider builds a TokenProvider from the given config.
// When ClientSecret is provided it uses ClientSecretCredential.
// Otherwise it falls back to DefaultAzureCredential which tries
// managed identity, Azure CLI, environment variables, etc.
func NewTokenProvider(cfg Config) (*TokenProvider, error) {
var cred azcore.TokenCredential
var err error

if cfg.ClientSecret != "" {
cred, err = azidentity.NewClientSecretCredential(cfg.TenantID, cfg.ClientID, cfg.ClientSecret, nil)
} else {
cred, err = azidentity.NewDefaultAzureCredential(nil)
}
if err != nil {
return nil, fmt.Errorf("azure identity: failed to create credential: %w", err)
}

return &TokenProvider{
cred: cred,
scope: cfg.Scope,
}, nil
}

// Token retrieves a fresh access token from Azure AD.
func (p *TokenProvider) Token(ctx context.Context) (string, error) {
tk, err := p.cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: []string{p.scope},
})
if err != nil {
return "", fmt.Errorf("azure identity: failed to get token: %w", err)
}
return tk.Token, nil
}

// GetIdentityToken implements the stscreds.IdentityTokenRetriever interface
// from the AWS SDK, returning the Azure AD token as bytes.
func (p *TokenProvider) GetIdentityToken() ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTokenTimeout)
defer cancel()

token, err := p.Token(ctx)
if err != nil {
return nil, err
}
return []byte(token), nil
}
Loading