mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-01 16:03:55 +08:00
AWS IAM Roles Anywhere: add support for Web/Console Access (#54594)
* AWS Web/Console Access using IAM Roles Anywhere Integration This PR adds support for AWS Web/Console Access using the IAM Roles Anywhere Integration. When trying to access an AWS App which has an associated: - integration of AWS RA kind - Roles Anywhere metadata: profile ARN - IAM Role There is a new flow which generates credentials using the: - trust anchor present in the integration metadata - profile arn present in the AppServer metadata - target IAM Role - and a X.509 certificate generated from the AWS Roles Anywhere CA Those credentials are then used to generate the signing URL using the federation service. * move assume role details to helper func * remove dead code, fix comment and error message
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package integrationv1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/authz"
|
||||
"github.com/gravitational/teleport/lib/integrations/awsra"
|
||||
)
|
||||
|
||||
// GenerateAWSRACredentials generates a set of AWS credentials which uses the AWS Roles Anywhere integration.
|
||||
func (s *Service) GenerateAWSRACredentials(ctx context.Context, req *integrationpb.GenerateAWSRACredentialsRequest) (*integrationpb.GenerateAWSRACredentialsResponse, error) {
|
||||
authCtx, err := s.authorizer.Authorize(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
for _, allowedRole := range []types.SystemRole{types.RoleAuth, types.RoleProxy} {
|
||||
if authz.HasBuiltinRole(*authCtx, string(allowedRole)) {
|
||||
return s.generateAWSRACredentialsWithoutAuthZ(ctx, req)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, trace.AccessDenied("credential generation is only available to auth or proxy services")
|
||||
}
|
||||
|
||||
// generateAWSRACredentialsWithoutAuthZ generates a set of AWS credentials which uses the AWS Roles Anywhere integration.
|
||||
// Bypasses authz and should only be used by other methods that validate AuthZ.
|
||||
func (s *Service) generateAWSRACredentialsWithoutAuthZ(ctx context.Context, req *integrationpb.GenerateAWSRACredentialsRequest) (*integrationpb.GenerateAWSRACredentialsResponse, error) {
|
||||
integration, err := s.cache.GetIntegration(ctx, req.GetIntegration())
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
spec := integration.GetAWSRolesAnywhereIntegrationSpec()
|
||||
if spec == nil {
|
||||
return nil, trace.BadParameter("integration %q is not an AWSRA integration", req.Integration)
|
||||
}
|
||||
|
||||
var durationSeconds *int
|
||||
if req.GetSessionMaxDuration().AsDuration() != 0 {
|
||||
d := int(req.GetSessionMaxDuration().AsDuration().Seconds())
|
||||
durationSeconds = &d
|
||||
}
|
||||
|
||||
awsCredentials, err := awsra.GenerateCredentials(ctx, awsra.GenerateCredentialsRequest{
|
||||
Clock: s.clock,
|
||||
TrustAnchorARN: spec.TrustAnchorARN,
|
||||
ProfileARN: req.GetProfileArn(),
|
||||
RoleARN: req.GetRoleArn(),
|
||||
SubjectCommonName: req.GetSubjectName(),
|
||||
DurationSeconds: durationSeconds,
|
||||
AcceptRoleSessionName: req.GetProfileAcceptsRoleSessionName(),
|
||||
KeyStoreManager: s.keyStoreManager,
|
||||
Cache: s.cache,
|
||||
CreateSession: s.awsRolesAnywhereCreateSessionFn,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &integrationpb.GenerateAWSRACredentialsResponse{
|
||||
AccessKeyId: awsCredentials.AccessKeyID,
|
||||
SecretAccessKey: awsCredentials.SecretAccessKey,
|
||||
SessionToken: awsCredentials.SessionToken,
|
||||
Expiration: timestamppb.New(awsCredentials.Expiration),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2025 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package integrationv1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/authz"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
)
|
||||
|
||||
func TestGenerateAWSRACredentials(t *testing.T) {
|
||||
t.Parallel()
|
||||
clusterName := "test-cluster"
|
||||
integrationName := "my-integration"
|
||||
proxyPublicAddr := "example.com:443"
|
||||
|
||||
ca := newCertAuthority(t, types.AWSRACA, clusterName)
|
||||
ctx, localClient, resourceSvc := initSvc(t, ca, clusterName, proxyPublicAddr)
|
||||
|
||||
ig, err := types.NewIntegrationAWSRA(
|
||||
types.Metadata{Name: integrationName},
|
||||
&types.AWSRAIntegrationSpecV1{
|
||||
TrustAnchorARN: "arn:aws:rolesanywhere:eu-west-2:123456789012:trust-anchor/12345678-1234-1234-1234-123456789012",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
_, err = localClient.CreateIntegration(ctx, ig)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx = authorizerForDummyUser(t, ctx, types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{
|
||||
{Resources: []string{types.KindIntegration}, Verbs: []string{types.VerbUse}},
|
||||
}},
|
||||
}, localClient)
|
||||
|
||||
t.Run("requesting with an user should return access denied", func(t *testing.T) {
|
||||
ctx = authorizerForDummyUser(t, ctx, types.RoleSpecV6{
|
||||
Allow: types.RoleConditions{Rules: []types.Rule{
|
||||
{Resources: []string{types.KindIntegration}, Verbs: []string{types.VerbUse}},
|
||||
}},
|
||||
}, localClient)
|
||||
|
||||
_, err := resourceSvc.GenerateAWSRACredentials(ctx, &integrationv1.GenerateAWSRACredentialsRequest{
|
||||
Integration: integrationName,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/OpsTeam",
|
||||
ProfileArn: "arn:aws:rolesanywhere:eu-west-2:123456789012:profile/12345678-1234-1234-1234-123456789012",
|
||||
SubjectName: "test",
|
||||
})
|
||||
require.True(t, trace.IsAccessDenied(err), "expected AccessDenied error, got %T", err)
|
||||
})
|
||||
|
||||
t.Run("auth and proxy can request credentials", func(t *testing.T) {
|
||||
for _, allowedRole := range []types.SystemRole{types.RoleAuth, types.RoleProxy} {
|
||||
ctx = authz.ContextWithUser(ctx, authz.BuiltinRole{
|
||||
Role: types.RoleInstance,
|
||||
AdditionalSystemRoles: []types.SystemRole{allowedRole},
|
||||
Username: string(allowedRole),
|
||||
Identity: tlsca.Identity{
|
||||
Username: string(allowedRole),
|
||||
},
|
||||
})
|
||||
|
||||
_, err := resourceSvc.GenerateAWSRACredentials(ctx, &integrationv1.GenerateAWSRACredentialsRequest{
|
||||
Integration: integrationName,
|
||||
RoleArn: "arn:aws:iam::123456789012:role/OpsTeam",
|
||||
ProfileArn: "arn:aws:rolesanywhere:eu-west-2:123456789012:profile/12345678-1234-1234-1234-123456789012",
|
||||
ProfileAcceptsRoleSessionName: true,
|
||||
SubjectName: "test",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/authz"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/integrations/awsra/createsession"
|
||||
"github.com/gravitational/teleport/lib/modules"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
)
|
||||
@@ -69,6 +70,9 @@ type KeyStoreManager interface {
|
||||
NewSSHKeyPair(ctx context.Context, purpose cryptosuites.KeyPurpose) (*types.SSHKeyPair, error)
|
||||
// GetSSHSignerFromKeySet selects a usable SSH keypair from the provided key set.
|
||||
GetSSHSignerFromKeySet(ctx context.Context, keySet types.CAKeySet) (ssh.Signer, error)
|
||||
// GetTLSCertAndSigner selects a usable TLS keypair from the given CA
|
||||
// and returns the PEM-encoded TLS certificate and a [crypto.Signer].
|
||||
GetTLSCertAndSigner(ctx context.Context, ca types.CertAuthority) ([]byte, crypto.Signer, error)
|
||||
}
|
||||
|
||||
// Backend defines the interface for all the backend services that the
|
||||
@@ -89,6 +93,11 @@ type ServiceConfig struct {
|
||||
Logger *slog.Logger
|
||||
Clock clockwork.Clock
|
||||
Emitter apievents.Emitter
|
||||
|
||||
// awsRolesAnywhereCreateSessionFn is a function that creates an AWS Roles Anywhere session.
|
||||
// This is used to allow mocking in tests, because the real implementation does
|
||||
// If not set, the default implementation is used.
|
||||
awsRolesAnywhereCreateSessionFn func(ctx context.Context, req createsession.CreateSessionRequest) (*createsession.CreateSessionResponse, error)
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults checks the ServiceConfig fields and returns an error if
|
||||
@@ -123,6 +132,10 @@ func (s *ServiceConfig) CheckAndSetDefaults() error {
|
||||
s.Clock = clockwork.NewRealClock()
|
||||
}
|
||||
|
||||
if s.awsRolesAnywhereCreateSessionFn == nil {
|
||||
s.awsRolesAnywhereCreateSessionFn = createsession.CreateSession
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -136,6 +149,8 @@ type Service struct {
|
||||
logger *slog.Logger
|
||||
clock clockwork.Clock
|
||||
emitter apievents.Emitter
|
||||
|
||||
awsRolesAnywhereCreateSessionFn func(ctx context.Context, req createsession.CreateSessionRequest) (*createsession.CreateSessionResponse, error)
|
||||
}
|
||||
|
||||
// NewService returns a new Integrations gRPC service.
|
||||
@@ -152,6 +167,8 @@ func NewService(cfg *ServiceConfig) (*Service, error) {
|
||||
backend: cfg.Backend,
|
||||
clock: cfg.Clock,
|
||||
emitter: cfg.Emitter,
|
||||
|
||||
awsRolesAnywhereCreateSessionFn: cfg.awsRolesAnywhereCreateSessionFn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,10 @@ package integrationv1
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gravitational/trace"
|
||||
@@ -38,6 +40,7 @@ import (
|
||||
"github.com/gravitational/teleport/lib/authz"
|
||||
"github.com/gravitational/teleport/lib/backend/memory"
|
||||
"github.com/gravitational/teleport/lib/events"
|
||||
"github.com/gravitational/teleport/lib/integrations/awsra/createsession"
|
||||
"github.com/gravitational/teleport/lib/modules"
|
||||
"github.com/gravitational/teleport/lib/services"
|
||||
"github.com/gravitational/teleport/lib/services/local"
|
||||
@@ -888,6 +891,15 @@ func initSvc(t *testing.T, ca types.CertAuthority, clusterName string, proxyPubl
|
||||
Cache: cache,
|
||||
KeyStoreManager: keystore.NewSoftwareKeystoreForTests(t),
|
||||
Emitter: events.NewDiscardEmitter(),
|
||||
awsRolesAnywhereCreateSessionFn: func(ctx context.Context, req createsession.CreateSessionRequest) (*createsession.CreateSessionResponse, error) {
|
||||
return &createsession.CreateSessionResponse{
|
||||
Version: 1,
|
||||
AccessKeyID: "access-key-id",
|
||||
SecretAccessKey: "secret-access-key",
|
||||
SessionToken: "session-token",
|
||||
Expiration: time.Now().Add(1 * time.Hour).Format(time.RFC3339),
|
||||
}, nil
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -958,6 +970,9 @@ func newCertAuthority(t *testing.T, caType types.CertAuthType, domain string) ty
|
||||
pub, priv, err := ta.GenerateJWT()
|
||||
require.NoError(t, err)
|
||||
|
||||
key, cert, err := tlsca.GenerateSelfSignedCA(pkix.Name{CommonName: domain}, nil, time.Minute)
|
||||
require.NoError(t, err)
|
||||
|
||||
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
|
||||
Type: caType,
|
||||
ClusterName: domain,
|
||||
@@ -967,6 +982,10 @@ func newCertAuthority(t *testing.T, caType types.CertAuthType, domain string) ty
|
||||
PrivateKey: priv,
|
||||
PrivateKeyType: types.PrivateKeyType_RAW,
|
||||
}},
|
||||
TLS: []*types.TLSKeyPair{{
|
||||
Key: key,
|
||||
Cert: cert,
|
||||
}},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -32,8 +32,11 @@ import (
|
||||
"github.com/aws/smithy-go/tracing/smithyoteltracing"
|
||||
"github.com/gravitational/trace"
|
||||
"go.opentelemetry.io/otel"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/lib/integrations/awsra"
|
||||
"github.com/gravitational/teleport/lib/modules"
|
||||
"github.com/gravitational/teleport/lib/utils/aws/stsutils"
|
||||
)
|
||||
@@ -50,17 +53,31 @@ const (
|
||||
credentialsSourceIntegration
|
||||
)
|
||||
|
||||
// IntegrationGetter is an interface that indicates which APIs are
|
||||
// required to get an integration.
|
||||
// Required when using integration credentials.
|
||||
type IntegrationGetter interface {
|
||||
// GetIntegration returns the specified integration resource.
|
||||
GetIntegration(ctx context.Context, name string) (types.Integration, error)
|
||||
}
|
||||
|
||||
// OIDCIntegrationClient is an interface that indicates which APIs are
|
||||
// required to generate an AWS OIDC integration token.
|
||||
type OIDCIntegrationClient interface {
|
||||
// GetIntegration returns the specified integration resource.
|
||||
GetIntegration(ctx context.Context, name string) (types.Integration, error)
|
||||
|
||||
IntegrationGetter
|
||||
// GenerateAWSOIDCToken generates a token to be used to execute an AWS OIDC
|
||||
// Integration action.
|
||||
GenerateAWSOIDCToken(ctx context.Context, integrationName string) (string, error)
|
||||
}
|
||||
|
||||
// RolesAnywhereIntegrationClient is an interface that indicates which APIs are
|
||||
// required to generate a set of AWS credentials using the AWS IAM Roles Anywhere integration.
|
||||
type RolesAnywhereIntegrationClient interface {
|
||||
IntegrationGetter
|
||||
// GenerateAWSRACredentials generates a token to be used to execute an AWS IAM Roles Anywhere integration.
|
||||
GenerateAWSRACredentials(ctx context.Context, req *integrationpb.GenerateAWSRACredentialsRequest) (*integrationpb.GenerateAWSRACredentialsResponse, error)
|
||||
}
|
||||
|
||||
// STSClient is a subset of the AWS STS API.
|
||||
type STSClient interface {
|
||||
stscreds.AssumeRoleAPIClient
|
||||
@@ -96,10 +113,21 @@ type options struct {
|
||||
credentialsSource credentialsSource
|
||||
// integration is the name of the integration to be used to fetch the credentials.
|
||||
integration string
|
||||
// integrationGetter provides APIs to get the AWS integration.
|
||||
// Required if integration credentials are requested.
|
||||
integrationGetter IntegrationGetter
|
||||
|
||||
// oidcIntegrationClient provides APIs to generate AWS OIDC tokens, which
|
||||
// can then be exchanged for IAM credentials.
|
||||
// Required if integration credentials are requested.
|
||||
// Required when integration uses IAM OIDC IdP to obtain credentials.
|
||||
oidcIntegrationClient OIDCIntegrationClient
|
||||
|
||||
// rolesAnywhereIntegrationClient provides APIs to generate AWS credentials.
|
||||
// Required when integration uses IAM Roles Anywhere service to obtain credentials.
|
||||
rolesAnywhereIntegrationClient RolesAnywhereIntegrationClient
|
||||
// rolesAnywhereIntegrationMetadata contains the Roles Anywhere Profile and IAM Role to use.
|
||||
rolesAnywhereIntegrationMetadata RolesAnywhereMetadata
|
||||
|
||||
// customRetryer is a custom retryer to use for the config.
|
||||
customRetryer func() aws.Retryer
|
||||
// maxRetries is the maximum number of retries to use for the config.
|
||||
@@ -129,11 +157,8 @@ func (o *options) checkAndSetDefaults() error {
|
||||
return trace.BadParameter("integration and ambient credentials cannot be used at the same time")
|
||||
}
|
||||
case credentialsSourceIntegration:
|
||||
if o.integration == "" {
|
||||
return trace.BadParameter("missing integration name")
|
||||
}
|
||||
if o.oidcIntegrationClient == nil {
|
||||
return trace.BadParameter("missing AWS OIDC integration client")
|
||||
if err := o.checkIntegrationCredentials(); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
default:
|
||||
return trace.BadParameter("missing credentials source (ambient or integration)")
|
||||
@@ -154,6 +179,22 @@ func (o *options) checkAndSetDefaults() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *options) checkIntegrationCredentials() error {
|
||||
if o.integration == "" {
|
||||
return trace.BadParameter("missing integration name")
|
||||
}
|
||||
|
||||
if o.integrationGetter == nil {
|
||||
return trace.BadParameter("missing integration getter")
|
||||
}
|
||||
|
||||
if o.oidcIntegrationClient == nil && o.rolesAnywhereIntegrationClient == nil {
|
||||
return trace.BadParameter("missing AWS integration client")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// OptionsFn is an option function for setting additional options
|
||||
// when getting an AWS config.
|
||||
type OptionsFn func(*options)
|
||||
@@ -198,23 +239,58 @@ func WithMaxRetries(maxRetries int) OptionsFn {
|
||||
}
|
||||
}
|
||||
|
||||
// IntegrationMetadata contains the metadata about the Integration to use
|
||||
// when using the integration credentials source.
|
||||
type IntegrationMetadata struct {
|
||||
// Name of the integration.
|
||||
// Will be empty when using ambient credentials.
|
||||
Name string
|
||||
|
||||
// RolesAnywhereMetadata contains the metadata about the Roles Anywhere.
|
||||
// Only set when the Integration is of AWS IAM Roles Anywhere subkind.
|
||||
RolesAnywhereMetadata RolesAnywhereMetadata
|
||||
}
|
||||
|
||||
// RolesAnywhereMetadata contains the metadata required to use AWS IAM Roles Anywhere
|
||||
// to generate credentials.
|
||||
type RolesAnywhereMetadata struct {
|
||||
// ProfileARN is the ARN of the Roles Anywhere profile.
|
||||
ProfileARN string
|
||||
// ProfileAcceptsRoleSessionName indicates whether the profile accepts a role session name.
|
||||
ProfileAcceptsRoleSessionName bool
|
||||
// RoleARN is the ARN of the role to assume.
|
||||
RoleARN string
|
||||
// IdentityUsername is the username to use when generating the AWS credentials.
|
||||
// This will be used as the Subject Common Name (CN) in the certificate, and logged in CloudTrail if ProfileAcceptsRoleSessionName is true.
|
||||
// Should be set to the teleport's username.
|
||||
IdentityUsername string
|
||||
// SessionDuration is used to calculate the expiration time for the AWS session.
|
||||
// Must be lower or equal to the maximum session duration of the role.
|
||||
// The actual session duration will be the minimum between this value (if not zero) and the Profile's max session duration.
|
||||
SessionDuration time.Duration
|
||||
}
|
||||
|
||||
// WithCredentialsMaybeIntegration sets the credential source to be
|
||||
// - ambient if the integration is an empty string
|
||||
// - integration, otherwise
|
||||
func WithCredentialsMaybeIntegration(integration string) OptionsFn {
|
||||
if integration != "" {
|
||||
return withIntegrationCredentials(integration)
|
||||
// When using integration, relevant integration metadata must be provided.
|
||||
func WithCredentialsMaybeIntegration(integrationMetadata IntegrationMetadata) OptionsFn {
|
||||
if integrationMetadata.Name == "" {
|
||||
return WithAmbientCredentials()
|
||||
}
|
||||
|
||||
return WithAmbientCredentials()
|
||||
}
|
||||
|
||||
// withIntegrationCredentials configures options with an Integration that must be used to fetch Credentials to assume a role.
|
||||
// This prevents the usage of AWS environment credentials.
|
||||
func withIntegrationCredentials(integration string) OptionsFn {
|
||||
return func(options *options) {
|
||||
options.credentialsSource = credentialsSourceIntegration
|
||||
options.integration = integration
|
||||
options.integration = integrationMetadata.Name
|
||||
options.rolesAnywhereIntegrationMetadata = integrationMetadata.RolesAnywhereMetadata
|
||||
}
|
||||
}
|
||||
|
||||
// WithRolesAnywhereIntegrationClient sets the Roles Anywhere integration client.
|
||||
func WithRolesAnywhereIntegrationClient(c RolesAnywhereIntegrationClient) OptionsFn {
|
||||
return func(options *options) {
|
||||
options.rolesAnywhereIntegrationClient = c
|
||||
options.integrationGetter = c
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +312,7 @@ func WithSTSClientProvider(fn STSClientProviderFunc) OptionsFn {
|
||||
func WithOIDCIntegrationClient(c OIDCIntegrationClient) OptionsFn {
|
||||
return func(options *options) {
|
||||
options.oidcIntegrationClient = c
|
||||
options.integrationGetter = c
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,14 +380,17 @@ func getBaseConfig(ctx context.Context, region string, opts *options) (aws.Confi
|
||||
}
|
||||
|
||||
if opts.credentialsSource == credentialsSourceIntegration {
|
||||
slog.DebugContext(ctx, "Initializing AWS config with OIDC integration credentials",
|
||||
slog.DebugContext(ctx, "Initializing AWS config with integration credentials",
|
||||
"region", region,
|
||||
"integration", opts.integration,
|
||||
)
|
||||
provider := &integrationCredentialsProvider{
|
||||
OIDCIntegrationClient: opts.oidcIntegrationClient,
|
||||
stsClt: opts.stsClientProvider(cfg),
|
||||
integrationName: opts.integration,
|
||||
stsClt: opts.stsClientProvider(cfg),
|
||||
integrationName: opts.integration,
|
||||
integrationGetter: opts.integrationGetter,
|
||||
oidcIntegrationClient: opts.oidcIntegrationClient,
|
||||
rolesAnywhereIntegrationClient: opts.rolesAnywhereIntegrationClient,
|
||||
rolesAnywhereProfileMetadata: opts.rolesAnywhereIntegrationMetadata,
|
||||
}
|
||||
cc := aws.NewCredentialsCache(provider, awsCredentialsCacheOptions)
|
||||
_, err := cc.Retrieve(ctx)
|
||||
@@ -365,33 +445,75 @@ func (t staticIdentityToken) GetIdentityToken() ([]byte, error) {
|
||||
return []byte(t), nil
|
||||
}
|
||||
|
||||
// integrationCredentialsProvider provides AWS OIDC integration credentials.
|
||||
// integrationCredentialsProvider provides AWS integration credentials.
|
||||
type integrationCredentialsProvider struct {
|
||||
OIDCIntegrationClient
|
||||
stsClt STSClient
|
||||
integrationName string
|
||||
|
||||
integrationGetter IntegrationGetter
|
||||
|
||||
oidcIntegrationClient OIDCIntegrationClient
|
||||
|
||||
rolesAnywhereIntegrationClient RolesAnywhereIntegrationClient
|
||||
rolesAnywhereProfileMetadata RolesAnywhereMetadata
|
||||
}
|
||||
|
||||
// Retrieve provides [aws.Credentials] for an AWS OIDC integration.
|
||||
// Retrieve provides [aws.Credentials] for an AWS integration.
|
||||
func (p *integrationCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
integration, err := p.GetIntegration(ctx, p.integrationName)
|
||||
integration, err := p.integrationGetter.GetIntegration(ctx, p.integrationName)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, trace.Wrap(err)
|
||||
}
|
||||
spec := integration.GetAWSOIDCIntegrationSpec()
|
||||
if spec == nil {
|
||||
return aws.Credentials{}, trace.BadParameter("invalid integration subkind, expected awsoidc, got %s", integration.GetSubKind())
|
||||
|
||||
switch integration.GetSubKind() {
|
||||
case types.IntegrationSubKindAWSOIDC:
|
||||
if p.oidcIntegrationClient == nil {
|
||||
return aws.Credentials{}, trace.BadParameter("missing OIDC integration client")
|
||||
}
|
||||
|
||||
spec := integration.GetAWSOIDCIntegrationSpec()
|
||||
if spec == nil {
|
||||
return aws.Credentials{}, trace.BadParameter("invalid integration subkind, expected awsoidc, got %s", integration.GetSubKind())
|
||||
}
|
||||
token, err := p.oidcIntegrationClient.GenerateAWSOIDCToken(ctx, p.integrationName)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, trace.Wrap(err)
|
||||
}
|
||||
cred, err := stscreds.NewWebIdentityRoleProvider(
|
||||
p.stsClt,
|
||||
spec.RoleARN,
|
||||
staticIdentityToken(token),
|
||||
).Retrieve(ctx)
|
||||
return cred, trace.Wrap(err)
|
||||
|
||||
case types.IntegrationSubKindAWSRolesAnywhere:
|
||||
if p.rolesAnywhereIntegrationClient == nil {
|
||||
return aws.Credentials{}, trace.BadParameter("missing roles anywhere integration client")
|
||||
}
|
||||
|
||||
resp, err := p.rolesAnywhereIntegrationClient.GenerateAWSRACredentials(ctx, &integrationpb.GenerateAWSRACredentialsRequest{
|
||||
Integration: p.integrationName,
|
||||
ProfileArn: p.rolesAnywhereProfileMetadata.ProfileARN,
|
||||
ProfileAcceptsRoleSessionName: p.rolesAnywhereProfileMetadata.ProfileAcceptsRoleSessionName,
|
||||
RoleArn: p.rolesAnywhereProfileMetadata.RoleARN,
|
||||
SubjectName: p.rolesAnywhereProfileMetadata.IdentityUsername,
|
||||
SessionMaxDuration: durationpb.New(p.rolesAnywhereProfileMetadata.SessionDuration),
|
||||
})
|
||||
if err != nil {
|
||||
return aws.Credentials{}, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return aws.Credentials{
|
||||
AccessKeyID: resp.AccessKeyId,
|
||||
SecretAccessKey: resp.SecretAccessKey,
|
||||
SessionToken: resp.SessionToken,
|
||||
Expires: resp.Expiration.AsTime(),
|
||||
Source: awsra.AWSCredentialsSourceRolesAnywhere,
|
||||
}, nil
|
||||
|
||||
default:
|
||||
return aws.Credentials{}, trace.BadParameter("invalid integration subkind, expected AWS OIDC or AWS Roles Anywhere, got %s", integration.GetSubKind())
|
||||
}
|
||||
token, err := p.GenerateAWSOIDCToken(ctx, p.integrationName)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, trace.Wrap(err)
|
||||
}
|
||||
cred, err := stscreds.NewWebIdentityRoleProvider(
|
||||
p.stsClt,
|
||||
spec.RoleARN,
|
||||
staticIdentityToken(token),
|
||||
).Retrieve(ctx)
|
||||
return cred, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// maybeHashRoleSessionName truncates the role session name and adds a hash
|
||||
|
||||
@@ -28,7 +28,9 @@ import (
|
||||
ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
|
||||
"github.com/gravitational/trace"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
)
|
||||
|
||||
@@ -111,11 +113,11 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
return &mockAssumeRoleAPIClient{}
|
||||
}
|
||||
|
||||
t.Run("without an integration client, must return missing credential provider error", func(t *testing.T) {
|
||||
t.Run("without an integration client, must return missing integration getter error", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, err := provider.GetConfig(ctx, dummyRegion, WithCredentialsMaybeIntegration(dummyIntegration))
|
||||
_, err := provider.GetConfig(ctx, dummyRegion, WithCredentialsMaybeIntegration(IntegrationMetadata{Name: dummyIntegration}))
|
||||
require.True(t, trace.IsBadParameter(err), "unexpected error: %v", err)
|
||||
require.ErrorContains(t, err, "missing AWS OIDC integration client")
|
||||
require.ErrorContains(t, err, "missing integration getter")
|
||||
})
|
||||
|
||||
t.Run("with an integration client, must return integration fetch error", func(t *testing.T) {
|
||||
@@ -126,7 +128,7 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
return nil, trace.NotFound("integration not found")
|
||||
}
|
||||
_, err := provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(dummyIntegration),
|
||||
WithCredentialsMaybeIntegration(IntegrationMetadata{Name: dummyIntegration}),
|
||||
WithOIDCIntegrationClient(&fakeIntegrationClt),
|
||||
WithSTSClientProvider(stsClt),
|
||||
)
|
||||
@@ -150,7 +152,7 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
return azureIntegration, nil
|
||||
}
|
||||
_, err = provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(dummyIntegration),
|
||||
WithCredentialsMaybeIntegration(IntegrationMetadata{Name: dummyIntegration}),
|
||||
WithOIDCIntegrationClient(&fakeIntegrationClt),
|
||||
WithSTSClientProvider(stsClt),
|
||||
)
|
||||
@@ -165,7 +167,7 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
return "", trace.BadParameter("failed to generate OIDC token")
|
||||
}
|
||||
_, err = provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(dummyIntegration),
|
||||
WithCredentialsMaybeIntegration(IntegrationMetadata{Name: dummyIntegration}),
|
||||
WithOIDCIntegrationClient(&fakeIntegrationClt),
|
||||
WithSTSClientProvider(stsClt),
|
||||
)
|
||||
@@ -177,7 +179,7 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
ctx := context.Background()
|
||||
|
||||
cfg, err := provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(dummyIntegration),
|
||||
WithCredentialsMaybeIntegration(IntegrationMetadata{Name: dummyIntegration}),
|
||||
WithOIDCIntegrationClient(&fakeIntegrationClt),
|
||||
WithSTSClientProvider(stsClt),
|
||||
)
|
||||
@@ -191,7 +193,7 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
ctx := context.Background()
|
||||
|
||||
cfg, err := provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(dummyIntegration),
|
||||
WithCredentialsMaybeIntegration(IntegrationMetadata{Name: dummyIntegration}),
|
||||
WithOIDCIntegrationClient(&fakeIntegrationClt),
|
||||
WithAssumeRole("roleA", "abc123"),
|
||||
WithSTSClientProvider(stsClt),
|
||||
@@ -206,7 +208,7 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
t.Run("with an integration credential provider assuming a role, must limit role chain length", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, err := provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(dummyIntegration),
|
||||
WithCredentialsMaybeIntegration(IntegrationMetadata{Name: dummyIntegration}),
|
||||
WithOIDCIntegrationClient(&fakeIntegrationClt),
|
||||
WithAssumeRole("roleA", "abc123"),
|
||||
WithAssumeRole("roleB", "abc123"),
|
||||
@@ -221,7 +223,7 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(""),
|
||||
WithCredentialsMaybeIntegration(IntegrationMetadata{}),
|
||||
WithOIDCIntegrationClient(&fakeOIDCIntegrationClient{unauth: true}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -251,7 +253,7 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
ctx := context.Background()
|
||||
|
||||
baseCfg, err := provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(""),
|
||||
WithCredentialsMaybeIntegration(IntegrationMetadata{}),
|
||||
WithOIDCIntegrationClient(&fakeOIDCIntegrationClient{unauth: true}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
@@ -268,6 +270,61 @@ func testGetConfigIntegration(t *testing.T, provider Provider) {
|
||||
require.Equal(t, "role: roleA, externalID: abc123", creds.AccessKeyID)
|
||||
require.Equal(t, "fake-session-token", creds.SessionToken)
|
||||
})
|
||||
|
||||
t.Run("with a Roles Anywhere integration", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
integrationClient := &mockRolesAnywhereClient{
|
||||
getIntegrationFn: func(context.Context, string) (types.Integration, error) {
|
||||
awsRAIntegration, err := types.NewIntegrationAWSRA(
|
||||
types.Metadata{Name: "integration-test"},
|
||||
&types.AWSRAIntegrationSpecV1{
|
||||
TrustAnchorARN: "arn:aws:rolesanywhere:eu-west-2:123456789012:trust-anchor/12345678-1234-1234-1234-123456789012",
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return awsRAIntegration, nil
|
||||
},
|
||||
}
|
||||
|
||||
integrationMetadata := IntegrationMetadata{
|
||||
Name: dummyIntegration,
|
||||
RolesAnywhereMetadata: RolesAnywhereMetadata{
|
||||
ProfileARN: "my-profile-arn",
|
||||
ProfileAcceptsRoleSessionName: true,
|
||||
RoleARN: "arn:aws:iam::123456789012:role/role",
|
||||
IdentityUsername: "alice",
|
||||
},
|
||||
}
|
||||
cfg, err := provider.GetConfig(ctx, dummyRegion,
|
||||
WithCredentialsMaybeIntegration(integrationMetadata),
|
||||
WithRolesAnywhereIntegrationClient(integrationClient),
|
||||
WithSTSClientProvider(stsClt),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
creds, err := cfg.Credentials.Retrieve(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "mock-access-key-id", creds.AccessKeyID)
|
||||
require.Equal(t, "mock-secret-access-key", creds.SecretAccessKey)
|
||||
require.Equal(t, "mock-session-token", creds.SessionToken)
|
||||
})
|
||||
}
|
||||
|
||||
type mockRolesAnywhereClient struct {
|
||||
getIntegrationFn func(context.Context, string) (types.Integration, error)
|
||||
}
|
||||
|
||||
func (f *mockRolesAnywhereClient) GetIntegration(ctx context.Context, name string) (types.Integration, error) {
|
||||
return f.getIntegrationFn(ctx, name)
|
||||
}
|
||||
|
||||
func (m *mockRolesAnywhereClient) GenerateAWSRACredentials(ctx context.Context, req *integrationpb.GenerateAWSRACredentialsRequest) (*integrationpb.GenerateAWSRACredentialsResponse, error) {
|
||||
return &integrationpb.GenerateAWSRACredentialsResponse{
|
||||
Expiration: timestamppb.New(time.Now().Add(1 * time.Hour)),
|
||||
AccessKeyId: "mock-access-key-id",
|
||||
SecretAccessKey: "mock-secret-access-key",
|
||||
SessionToken: "mock-session-token",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestNewCacheKey(t *testing.T) {
|
||||
@@ -275,12 +332,35 @@ func TestNewCacheKey(t *testing.T) {
|
||||
{RoleARN: "roleA"},
|
||||
{RoleARN: "roleB", ExternalID: "abc123", SessionName: "alice", Tags: map[string]string{"AKey": "AValue"}},
|
||||
}
|
||||
got, err := newCacheKey("integration-name", roleChain...)
|
||||
require.NoError(t, err)
|
||||
want := strings.TrimSpace(`
|
||||
{"integration":"integration-name","role_chain":[{"role_arn":"roleA"},{"role_arn":"roleB","external_id":"abc123","session_name":"alice","tags":{"AKey":"AValue"}}]}
|
||||
|
||||
t.Run("aws oidc integration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, err := newCacheKey("integration-name", RolesAnywhereMetadata{}, roleChain...)
|
||||
require.NoError(t, err)
|
||||
want := strings.TrimSpace(`
|
||||
{"integration":"integration-name","role_chain":[{"role_arn":"roleA"},{"role_arn":"roleB","external_id":"abc123","session_name":"alice","tags":{"AKey":"AValue"}}],"roles_anywhere_integration_metadata":{"ProfileARN":"","ProfileAcceptsRoleSessionName":false,"RoleARN":"","IdentityUsername":"","SessionDuration":0}}
|
||||
`)
|
||||
require.Equal(t, want, got)
|
||||
require.Equal(t, want, got)
|
||||
})
|
||||
|
||||
t.Run("aws roles anywhere integration", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
rolesAnywhereMetadata := RolesAnywhereMetadata{
|
||||
ProfileARN: "my-profile-arn",
|
||||
ProfileAcceptsRoleSessionName: true,
|
||||
RoleARN: "arn:aws:iam::123456789012:role/role",
|
||||
IdentityUsername: "alice",
|
||||
SessionDuration: time.Hour,
|
||||
}
|
||||
|
||||
got, err := newCacheKey("integration-name", rolesAnywhereMetadata, roleChain...)
|
||||
require.NoError(t, err)
|
||||
want := strings.TrimSpace(`
|
||||
{"integration":"integration-name","role_chain":[{"role_arn":"roleA"},{"role_arn":"roleB","external_id":"abc123","session_name":"alice","tags":{"AKey":"AValue"}}],"roles_anywhere_integration_metadata":{"ProfileARN":"my-profile-arn","ProfileAcceptsRoleSessionName":true,"RoleARN":"arn:aws:iam::123456789012:role/role","IdentityUsername":"alice","SessionDuration":3600000000000}}
|
||||
`)
|
||||
require.Equal(t, want, got)
|
||||
})
|
||||
}
|
||||
|
||||
type fakeOIDCIntegrationClient struct {
|
||||
|
||||
@@ -105,7 +105,7 @@ func (c *Cache) getBaseConfig(ctx context.Context, region string, opts *options)
|
||||
// loading.
|
||||
// We cache the entire config by integration name, which is empty for
|
||||
// non-integration config, but only use credentials from it on cache hit.
|
||||
cacheKey, err := newCacheKey(opts.integration)
|
||||
cacheKey, err := newCacheKey(opts.integration, opts.rolesAnywhereIntegrationMetadata)
|
||||
if err != nil {
|
||||
return aws.Config{}, trace.Wrap(err)
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func (c *Cache) getBaseConfig(ctx context.Context, region string, opts *options)
|
||||
func (c *Cache) getConfigForRoleChain(ctx context.Context, cfg aws.Config, opts *options) (aws.Config, error) {
|
||||
for i, r := range opts.assumeRoles {
|
||||
// cache credentials by integration and assumed-role chain.
|
||||
cacheKey, err := newCacheKey(opts.integration, opts.assumeRoles[:i+1]...)
|
||||
cacheKey, err := newCacheKey(opts.integration, opts.rolesAnywhereIntegrationMetadata, opts.assumeRoles[:i+1]...)
|
||||
if err != nil {
|
||||
return aws.Config{}, trace.Wrap(err)
|
||||
}
|
||||
@@ -164,14 +164,16 @@ func (c *Cache) getConfigForRoleChain(ctx context.Context, cfg aws.Config, opts
|
||||
// The cache key can be used to get role credentials without calling AWS STS.
|
||||
// Therefore, we marshal the key as JSON to be sure the input cannot be
|
||||
// manipulated to retrieve other credentials.
|
||||
func newCacheKey(integrationName string, roleChain ...AssumeRole) (string, error) {
|
||||
func newCacheKey(integrationName string, rolesAnywhereIntegrationMetadata RolesAnywhereMetadata, roleChain ...AssumeRole) (string, error) {
|
||||
type configCacheKey struct {
|
||||
Integration string `json:"integration"`
|
||||
RoleChain []AssumeRole `json:"role_chain"`
|
||||
Integration string `json:"integration"`
|
||||
RoleChain []AssumeRole `json:"role_chain"`
|
||||
RolesAnywhereIntegrationMetadata RolesAnywhereMetadata `json:"roles_anywhere_integration_metadata"`
|
||||
}
|
||||
out, err := json.Marshal(configCacheKey{
|
||||
Integration: integrationName,
|
||||
RoleChain: roleChain,
|
||||
Integration: integrationName,
|
||||
RoleChain: roleChain,
|
||||
RolesAnywhereIntegrationMetadata: rolesAnywhereIntegrationMetadata,
|
||||
})
|
||||
return string(out), trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -136,8 +136,8 @@ type Credentials struct {
|
||||
SecretAccessKey string `json:"SecretAccessKey"`
|
||||
// SessionToken is the the AWS session token for temporary credentials.
|
||||
SessionToken string `json:"SessionToken"`
|
||||
// Expiration is ISO8601 timestamp string when the credentials expire.
|
||||
Expiration string `json:"Expiration"`
|
||||
// Expiration is the timestamp when the credentials expire.
|
||||
Expiration time.Time `json:"Expiration"`
|
||||
// SerialNumber is the serial number of the certificate which was created and exchanged to obtain AWS Credentials.
|
||||
// When using these credentials, CloudTrail will log the certificate's Subject Common Name, if the profile accepts it.
|
||||
// Otherwise, the serial number is logged.
|
||||
@@ -223,12 +223,17 @@ func GenerateCredentials(ctx context.Context, req GenerateCredentialsRequest) (*
|
||||
return nil, trace.BadParameter("failed to create session %v", err)
|
||||
}
|
||||
|
||||
parsedExpiration, err := time.Parse(time.RFC3339, createSessionResp.Expiration)
|
||||
if err != nil {
|
||||
return nil, trace.BadParameter("failed to parse expiration time %q: %v", createSessionResp.Expiration, err)
|
||||
}
|
||||
|
||||
return &Credentials{
|
||||
Version: createSessionResp.Version,
|
||||
AccessKeyID: createSessionResp.AccessKeyID,
|
||||
SecretAccessKey: createSessionResp.SecretAccessKey,
|
||||
SessionToken: createSessionResp.SessionToken,
|
||||
Expiration: createSessionResp.Expiration,
|
||||
Expiration: parsedExpiration,
|
||||
SerialNumber: x509Cert.SerialNumber.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -4940,6 +4940,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
|
||||
ServiceComponent: teleport.ComponentWebProxy,
|
||||
AWSConfigOptions: []awsconfig.OptionsFn{
|
||||
awsconfig.WithOIDCIntegrationClient(conn.Client),
|
||||
awsconfig.WithRolesAnywhereIntegrationClient(conn.Client),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -176,7 +176,7 @@ func (s *signerHandler) serveCommonRequest(sessCtx *common.SessionContext, w htt
|
||||
ExternalID: sessCtx.App.GetAWSExternalID(),
|
||||
SessionName: sessCtx.Identity.Username,
|
||||
}),
|
||||
awsconfig.WithCredentialsMaybeIntegration(sessCtx.App.GetIntegration()),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: sessCtx.App.GetIntegration()}),
|
||||
)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
+80
-39
@@ -38,6 +38,7 @@ import (
|
||||
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
"github.com/gravitational/teleport/lib/cloud/awsconfig"
|
||||
"github.com/gravitational/teleport/lib/integrations/awsra"
|
||||
"github.com/gravitational/teleport/lib/tlsca"
|
||||
awsutils "github.com/gravitational/teleport/lib/utils/aws"
|
||||
)
|
||||
@@ -62,6 +63,9 @@ type AWSSigninRequest struct {
|
||||
// Integration is the Integration name to use to generate credentials.
|
||||
// If empty, it will use ambient credentials
|
||||
Integration string
|
||||
// RolesAnywhereMetadata contains the Profile/Role information to use when
|
||||
// sourcing the credentials from a Roles Anywhere integration.
|
||||
RolesAnywhereMetadata awsconfig.RolesAnywhereMetadata
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults validates the request.
|
||||
@@ -177,16 +181,41 @@ func (c *cloud) getAWSSigninToken(ctx context.Context, req *AWSSigninRequest, en
|
||||
// "SessionDuration" is not provided, the web console session duration will
|
||||
// be bound to the duration used in the next AssumeRole call.
|
||||
|
||||
integrationMetadata := awsconfig.IntegrationMetadata{
|
||||
Name: req.Integration,
|
||||
RolesAnywhereMetadata: req.RolesAnywhereMetadata,
|
||||
}
|
||||
|
||||
// When using Roles Anywhere integration, the session duration is set to the maximum allowed for temporary sessions: 1h.
|
||||
// TODO(marco): add support for longer sessions which Roles Anywhere allows for but, requires us to know the Role's maximum session duration.
|
||||
if req.RolesAnywhereMetadata.ProfileARN != "" {
|
||||
duration, err := c.getFederationDuration(req, true /* temporarySession */)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
req.RolesAnywhereMetadata.SessionDuration = duration
|
||||
}
|
||||
|
||||
// Sign In requests target IAM endpoints which don't require a region.
|
||||
region := ""
|
||||
baseCfg, err := c.awsCachedProvider.GetConfig(ctx, region,
|
||||
awsconfig.WithCredentialsMaybeIntegration(req.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(integrationMetadata),
|
||||
)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
temporarySession, err := isSessionUsingTemporaryCredentials(ctx, baseCfg)
|
||||
if baseCfg.Credentials == nil {
|
||||
return "", trace.NotFound("session credentials not found")
|
||||
}
|
||||
|
||||
baseCreds, err := baseCfg.Credentials.Retrieve(ctx)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
temporarySession, err := isSessionUsingTemporaryCredentials(baseCreds)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
@@ -196,39 +225,28 @@ func (c *cloud) getAWSSigninToken(ctx context.Context, req *AWSSigninRequest, en
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
|
||||
assumeRole := awsconfig.AssumeRole{
|
||||
RoleARN: req.Identity.RouteToApp.AWSRoleARN,
|
||||
ExternalID: req.ExternalID,
|
||||
// Setting role session name to Teleport username will allow to
|
||||
// associate CloudTrail events with the Teleport user.
|
||||
SessionName: req.Identity.Username,
|
||||
}
|
||||
awsConfigOptions := append(c.cfg.AWSConfigOptions,
|
||||
awsconfig.WithCredentialsMaybeIntegration(integrationMetadata),
|
||||
awsconfig.WithBaseCredentialsProvider(baseCfg.Credentials),
|
||||
)
|
||||
|
||||
// Setting web console session duration through AssumeRole call for AWS
|
||||
// sessions with temporary credentials.
|
||||
// Technically the session duration can be set this way for
|
||||
// non-temporary sessions. However, the AssumeRole call will fail if we
|
||||
// are requesting duration longer than the maximum session duration of
|
||||
// the role we are assuming. In addition, the session credentials may
|
||||
// not have permission to perform a get-role on the role. Therefore,
|
||||
// "SessionDuration" parameter will be defined when calling federation
|
||||
// endpoint below instead of here, for non-temporary sessions.
|
||||
// Most flows for providing AWS Access, require the following:
|
||||
// - access to credentials for a helper Role (EC2 instance profile's Role, AWS OIDC Integration Role, etc.)
|
||||
// - use the credentials to call sts:AssumeRole to obtain the credentials for the target role
|
||||
// - use the credentials to call the federation endpoint to obtain the federation URL
|
||||
//
|
||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
if temporarySession {
|
||||
c.cfg.Logger.DebugContext(ctx, "Temporary session")
|
||||
assumeRole.Duration = duration
|
||||
// The exception is the IAM Roles Anywhere integration which only uses the target role (no intermediate role).
|
||||
switch {
|
||||
case req.RolesAnywhereMetadata.ProfileARN != "":
|
||||
default:
|
||||
awsConfigOptions = append(awsConfigOptions,
|
||||
getAssumeDetailedRolesOption(ctx, req, temporarySession, duration),
|
||||
)
|
||||
}
|
||||
|
||||
// Do not use cache provider to avoid returning credentials with wrong
|
||||
// expiry duration.
|
||||
awsCfg, err := awsconfig.GetConfig(ctx, region,
|
||||
append(c.cfg.AWSConfigOptions,
|
||||
awsconfig.WithCredentialsMaybeIntegration(req.Integration),
|
||||
awsconfig.WithBaseCredentialsProvider(baseCfg.Credentials),
|
||||
awsconfig.WithDetailedAssumeRole(assumeRole),
|
||||
)...,
|
||||
)
|
||||
awsCfg, err := awsconfig.GetConfig(ctx, region, awsConfigOptions...)
|
||||
if err != nil {
|
||||
return "", trace.Wrap(err)
|
||||
}
|
||||
@@ -285,19 +303,38 @@ func (c *cloud) getAWSSigninToken(ctx context.Context, req *AWSSigninRequest, en
|
||||
return fedResp.SigninToken, nil
|
||||
}
|
||||
|
||||
func getAssumeDetailedRolesOption(ctx context.Context, req *AWSSigninRequest, temporarySession bool, duration time.Duration) awsconfig.OptionsFn {
|
||||
assumeRole := awsconfig.AssumeRole{
|
||||
RoleARN: req.Identity.RouteToApp.AWSRoleARN,
|
||||
ExternalID: req.ExternalID,
|
||||
// Setting role session name to Teleport username will allow to
|
||||
// associate CloudTrail events with the Teleport user.
|
||||
SessionName: req.Identity.Username,
|
||||
}
|
||||
|
||||
// Setting web console session duration through AssumeRole call for AWS
|
||||
// sessions with temporary credentials.
|
||||
// Technically the session duration can be set this way for
|
||||
// non-temporary sessions. However, the AssumeRole call will fail if we
|
||||
// are requesting duration longer than the maximum session duration of
|
||||
// the role we are assuming. In addition, the session credentials may
|
||||
// not have permission to perform a get-role on the role. Therefore,
|
||||
// "SessionDuration" parameter will be defined when calling federation
|
||||
// endpoint below instead of here, for non-temporary sessions.
|
||||
//
|
||||
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
|
||||
if temporarySession {
|
||||
assumeRole.Duration = duration
|
||||
}
|
||||
|
||||
return awsconfig.WithDetailedAssumeRole(assumeRole)
|
||||
}
|
||||
|
||||
// isSessionUsingTemporaryCredentials checks if the current aws session is
|
||||
// using temporary credentials.
|
||||
//
|
||||
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html
|
||||
func isSessionUsingTemporaryCredentials(ctx context.Context, cfg aws.Config) (bool, error) {
|
||||
if cfg.Credentials == nil {
|
||||
return false, trace.NotFound("session credentials not found")
|
||||
}
|
||||
|
||||
credentials, err := cfg.Credentials.Retrieve(ctx)
|
||||
if err != nil {
|
||||
return false, trace.Wrap(err)
|
||||
}
|
||||
func isSessionUsingTemporaryCredentials(credentials aws.Credentials) (bool, error) {
|
||||
|
||||
switch credentials.Source {
|
||||
case ec2rolecreds.ProviderName:
|
||||
@@ -320,7 +357,11 @@ func isSessionUsingTemporaryCredentials(ctx context.Context, cfg aws.Config) (bo
|
||||
// ssocreds.Provider is an AWS credential provider that retrieves
|
||||
// temporary AWS credentials by exchanging an SSO login token.
|
||||
// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials/ssocreds#Provider
|
||||
ssocreds.ProviderName:
|
||||
ssocreds.ProviderName,
|
||||
|
||||
// When using the AWS Roles Anywhere integration, the credentials are temporary:
|
||||
// https://docs.aws.amazon.com/rolesanywhere/latest/userguide/authentication-create-session.html#response-elements
|
||||
awsra.AWSCredentialsSourceRolesAnywhere:
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -84,25 +84,16 @@ func TestIsSessionUsingTemporaryCredentials(t *testing.T) {
|
||||
},
|
||||
expectBool: true,
|
||||
},
|
||||
{
|
||||
name: "bad config",
|
||||
credentials: nil,
|
||||
expectError: trace.IsNotFound,
|
||||
},
|
||||
{
|
||||
name: "failed to get credentials",
|
||||
credentials: &mockCredentialsProvider{
|
||||
retrieveError: trace.AccessDenied(""),
|
||||
},
|
||||
expectError: trace.IsAccessDenied,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
// capture range variable
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
isTemporary, err := isSessionUsingTemporaryCredentials(ctx, aws.Config{Credentials: test.credentials})
|
||||
awsCredentials, err := test.credentials.Retrieve(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
isTemporary, err := isSessionUsingTemporaryCredentials(awsCredentials)
|
||||
|
||||
if test.expectError != nil {
|
||||
require.True(t, test.expectError(err))
|
||||
|
||||
@@ -481,6 +481,12 @@ func (c *ConnectionsHandler) serveAWSWebConsole(w http.ResponseWriter, r *http.R
|
||||
Issuer: app.GetPublicAddr(),
|
||||
ExternalID: app.GetAWSExternalID(),
|
||||
Integration: app.GetIntegration(),
|
||||
RolesAnywhereMetadata: awsconfig.RolesAnywhereMetadata{
|
||||
ProfileARN: app.GetAWSRolesAnywhereProfileARN(),
|
||||
ProfileAcceptsRoleSessionName: app.GetAWSRolesAnywhereAcceptRoleSessionName(),
|
||||
RoleARN: identity.RouteToApp.AWSRoleARN,
|
||||
IdentityUsername: identity.Username,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
@@ -858,7 +858,7 @@ func consumeTillErr(stream accessgraphv1alpha.AccessGraphService_AWSCloudTrailSt
|
||||
|
||||
func getOptions(matcher *types.AccessGraphAWSSync) []awsconfig.OptionsFn {
|
||||
opts := []awsconfig.OptionsFn{
|
||||
awsconfig.WithCredentialsMaybeIntegration(matcher.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: matcher.Integration}),
|
||||
}
|
||||
if matcher.AssumeRole != nil {
|
||||
opts = append(opts, awsconfig.WithAssumeRole(matcher.AssumeRole.RoleARN, matcher.AssumeRole.ExternalID))
|
||||
|
||||
@@ -1118,7 +1118,7 @@ func (s *Server) handleEC2RemoteInstallation(instances *server.EC2Instances) err
|
||||
// TODO(gavin): support assume_role_arn for ec2.
|
||||
ssmClient, err := s.GetSSMClient(s.ctx,
|
||||
instances.Region,
|
||||
awsconfig.WithCredentialsMaybeIntegration(instances.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: instances.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
@@ -368,7 +368,7 @@ func (a *Fetcher) poll(ctx context.Context, features Features) (*Resources, erro
|
||||
// with the v2 sdk.
|
||||
func (a *Fetcher) getAWSOptions() []awsconfig.OptionsFn {
|
||||
opts := []awsconfig.OptionsFn{
|
||||
awsconfig.WithCredentialsMaybeIntegration(a.Config.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: a.Config.Integration}),
|
||||
}
|
||||
|
||||
if a.Config.AssumeRole != nil {
|
||||
|
||||
@@ -49,7 +49,7 @@ func (f *rdsDocumentDBFetcher) ComponentShortName() string {
|
||||
func (f *rdsDocumentDBFetcher) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -59,7 +59,7 @@ func (f *elastiCachePlugin) ComponentShortName() string {
|
||||
func (f *elastiCachePlugin) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -56,7 +56,7 @@ func (f *memoryDBPlugin) ComponentShortName() string {
|
||||
func (f *memoryDBPlugin) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -56,7 +56,7 @@ func (f *openSearchPlugin) ComponentShortName() string {
|
||||
func (f *openSearchPlugin) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -60,7 +60,7 @@ func (f *rdsDBInstancesPlugin) ComponentShortName() string {
|
||||
func (f *rdsDBInstancesPlugin) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
@@ -157,7 +157,7 @@ func (f *rdsAuroraClustersPlugin) ComponentShortName() string {
|
||||
func (f *rdsAuroraClustersPlugin) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -49,7 +49,7 @@ func (f *rdsDBProxyPlugin) ComponentShortName() string {
|
||||
func (f *rdsDBProxyPlugin) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -49,7 +49,7 @@ type redshiftPlugin struct{}
|
||||
func (f *redshiftPlugin) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -65,7 +65,7 @@ func (f *redshiftServerlessPlugin) ComponentShortName() string {
|
||||
func (f *redshiftServerlessPlugin) GetDatabases(ctx context.Context, cfg *awsFetcherConfig) (types.Databases, error) {
|
||||
awsCfg, err := cfg.AWSConfigProvider.GetConfig(ctx, cfg.Region,
|
||||
awsconfig.WithAssumeRole(cfg.AssumeRole.RoleARN, cfg.AssumeRole.ExternalID),
|
||||
awsconfig.WithCredentialsMaybeIntegration(cfg.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: cfg.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -738,7 +738,7 @@ func (a *eksFetcher) getAWSOpts() []awsconfig.OptionsFn {
|
||||
a.AssumeRole.RoleARN,
|
||||
a.AssumeRole.ExternalID,
|
||||
),
|
||||
awsconfig.WithCredentialsMaybeIntegration(a.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: a.Integration}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ func MatchersToEC2InstanceFetchers(ctx context.Context, matchers []types.AWSMatc
|
||||
for _, region := range matcher.Regions {
|
||||
// TODO(gavin): support assume_role_arn for ec2.
|
||||
ec2Client, err := getEC2Client(ctx, region,
|
||||
awsconfig.WithCredentialsMaybeIntegration(matcher.Integration),
|
||||
awsconfig.WithCredentialsMaybeIntegration(awsconfig.IntegrationMetadata{Name: matcher.Integration}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
Reference in New Issue
Block a user