From 0a3d2ca06d2a13b1019b3fa5be71f58bbcee0001 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Tue, 25 Mar 2025 15:06:20 +0000 Subject: [PATCH 1/2] Workload Identity: Roles Anywhere `tbot` service. (#52426) * Add config for WorkloadIdenttiyAWSRAService * Add CLI command * Start hacking on service impl * Write credentials in AWS credentials file format * Fix go.mod/go.sum * Go mod tidy * Add ARN validation * Add specific config for AWS session duration/renewal * Update golden file * Fix gomod/gosum * Refactor CheckAndSetDefaults * Initialize service * Update CLI flags * Refactor & add tests * Use *Context slog calls * Update service name to include full `roles-anywhere` * Add mocked AWS rolesanywhere API based test * Validate region --- go.mod | 6 +- go.sum | 12 +- integrations/event-handler/go.mod | 4 +- integrations/event-handler/go.sum | 8 +- integrations/terraform/go.mod | 7 +- integrations/terraform/go.sum | 12 +- lib/auth/transport_credentials_test.go | 13 +- .../cli/start_workload_identity_aws_ra.go | 172 ++++++++++ .../start_workload_identity_aws_ra_test.go | 121 +++++++ lib/tbot/config/config.go | 6 + .../service_workload_identity_aws_ra.go | 181 ++++++++++ .../service_workload_identity_aws_ra_test.go | 308 ++++++++++++++++++ .../full.golden | 11 + lib/tbot/service_workload_identity_aws_ra.go | 260 +++++++++++++++ .../service_workload_identity_aws_ra_test.go | 215 ++++++++++++ lib/tbot/tbot.go | 13 + .../TestBotWorkloadIdentityAWSRA.golden | 4 + lib/tbot/testdata/Test_renderAWSCreds.golden | 4 + lib/teleterm/teleterm_test.go | 9 + tool/tbot/main.go | 3 + 20 files changed, 1350 insertions(+), 19 deletions(-) create mode 100644 lib/tbot/cli/start_workload_identity_aws_ra.go create mode 100644 lib/tbot/cli/start_workload_identity_aws_ra_test.go create mode 100644 lib/tbot/config/service_workload_identity_aws_ra.go create mode 100644 lib/tbot/config/service_workload_identity_aws_ra_test.go create mode 100644 lib/tbot/config/testdata/TestWorkloadIdentityAWSRAService_YAML/full.golden create mode 100644 lib/tbot/service_workload_identity_aws_ra.go create mode 100644 lib/tbot/service_workload_identity_aws_ra_test.go create mode 100644 lib/tbot/testdata/TestBotWorkloadIdentityAWSRA.golden create mode 100644 lib/tbot/testdata/Test_renderAWSCreds.golden diff --git a/go.mod b/go.mod index a71ebf88b2dba..18a7fb33f5b2e 100644 --- a/go.mod +++ b/go.mod @@ -184,7 +184,8 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/snowflakedb/gosnowflake v1.11.1 github.com/spf13/cobra v1.8.1 - github.com/spiffe/go-spiffe/v2 v2.3.0 + github.com/spiffe/aws-spiffe-workload-helper v0.0.1-rc.8 + github.com/spiffe/go-spiffe/v2 v2.4.0 github.com/stretchr/testify v1.10.0 github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb github.com/vulcand/predicate v1.2.0 // replaced @@ -218,7 +219,7 @@ require ( golang.zx2c4.com/wireguard/windows v0.5.3 google.golang.org/api v0.197.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 - google.golang.org/grpc v1.66.3 + google.golang.org/grpc v1.67.1 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 google.golang.org/protobuf v1.35.2 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c @@ -288,6 +289,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.3 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.24.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect + github.com/aws/rolesanywhere-credential-helper v1.2.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect diff --git a/go.sum b/go.sum index cc4eda499db2f..469ca553a9e7a 100644 --- a/go.sum +++ b/go.sum @@ -937,6 +937,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 h1:wVnQ6tigGsRqSWDEEyH6lSAJ9OyF github.com/aws/aws-sdk-go-v2/service/sts v1.32.3/go.mod h1:VZa9yTFyj4o10YGsmDO4gbQJUvvhY72fhumT8W4LqsE= github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v1.1.0 h1:EJsHUYgFBV7/N1YtL73lsfZODAOU+CnNSZfEAlqqQaA= github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v1.1.0/go.mod h1:AxKuXHc0zv2yYaeueUG7R3ONbcnQIuDj0bkdFmPVRzU= +github.com/aws/rolesanywhere-credential-helper v1.2.0 h1:eLqJvSznH8nJk48dwFc0raWOpbTGgBeNYH3Q8UQFVx4= +github.com/aws/rolesanywhere-credential-helper v1.2.0/go.mod h1:YRxmRrAaqbVVXPNH1gHT76nWaMGvpAziHAHw8UwKrpU= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= @@ -2160,8 +2162,10 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8= -github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY= +github.com/spiffe/aws-spiffe-workload-helper v0.0.1-rc.8 h1:KFHLQuNqUG4YZXo+QwMurqFxU5qWchFDCxYbl+uJz9w= +github.com/spiffe/aws-spiffe-workload-helper v0.0.1-rc.8/go.mod h1:xhhvkBenvvbuEEw9YR2HwGzUPv0sQiHd3wv8avhra+U= +github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c= +github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -3086,8 +3090,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= -google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= diff --git a/integrations/event-handler/go.mod b/integrations/event-handler/go.mod index 3d00724907acc..1eec3a5636636 100644 --- a/integrations/event-handler/go.mod +++ b/integrations/event-handler/go.mod @@ -258,7 +258,7 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spiffe/go-spiffe/v2 v2.3.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.4.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/thales-e-security/pool v0.0.2 // indirect github.com/vulcand/predicate v1.2.0 // indirect @@ -298,7 +298,7 @@ require ( google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.66.3 // indirect + google.golang.org/grpc v1.67.1 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/integrations/event-handler/go.sum b/integrations/event-handler/go.sum index bea1d564eeb92..b2393a73f2dc3 100644 --- a/integrations/event-handler/go.sum +++ b/integrations/event-handler/go.sum @@ -764,8 +764,8 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8= -github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY= +github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c= +github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -1026,8 +1026,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= -google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod index 7c557f24accd5..f020539faaedf 100644 --- a/integrations/terraform/go.mod +++ b/integrations/terraform/go.mod @@ -23,7 +23,7 @@ require ( github.com/jonboulle/clockwork v0.4.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.66.3 + google.golang.org/grpc v1.67.1 google.golang.org/protobuf v1.35.2 ) @@ -107,6 +107,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssoadmin v1.29.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 // indirect + github.com/aws/rolesanywhere-credential-helper v1.2.0 // indirect github.com/aws/smithy-go v1.22.0 // indirect github.com/beevik/etree v1.4.1 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -316,7 +317,8 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spiffe/go-spiffe/v2 v2.3.0 // indirect + github.com/spiffe/aws-spiffe-workload-helper v0.0.1-rc.8 // indirect + github.com/spiffe/go-spiffe/v2 v2.4.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/thales-e-security/pool v0.0.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect @@ -375,6 +377,7 @@ require ( gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect helm.sh/helm/v3 v3.16.1 // indirect diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 439a7f6c8dba4..73425ee4127de 100644 --- a/integrations/terraform/go.sum +++ b/integrations/terraform/go.sum @@ -282,6 +282,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.32.3 h1:wVnQ6tigGsRqSWDEEyH6lSAJ9OyF github.com/aws/aws-sdk-go-v2/service/sts v1.32.3/go.mod h1:VZa9yTFyj4o10YGsmDO4gbQJUvvhY72fhumT8W4LqsE= github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v1.1.0 h1:EJsHUYgFBV7/N1YtL73lsfZODAOU+CnNSZfEAlqqQaA= github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v1.1.0/go.mod h1:AxKuXHc0zv2yYaeueUG7R3ONbcnQIuDj0bkdFmPVRzU= +github.com/aws/rolesanywhere-credential-helper v1.2.0 h1:eLqJvSznH8nJk48dwFc0raWOpbTGgBeNYH3Q8UQFVx4= +github.com/aws/rolesanywhere-credential-helper v1.2.0/go.mod h1:YRxmRrAaqbVVXPNH1gHT76nWaMGvpAziHAHw8UwKrpU= github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= @@ -1137,8 +1139,10 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8= -github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY= +github.com/spiffe/aws-spiffe-workload-helper v0.0.1-rc.8 h1:KFHLQuNqUG4YZXo+QwMurqFxU5qWchFDCxYbl+uJz9w= +github.com/spiffe/aws-spiffe-workload-helper v0.0.1-rc.8/go.mod h1:xhhvkBenvvbuEEw9YR2HwGzUPv0sQiHd3wv8avhra+U= +github.com/spiffe/go-spiffe/v2 v2.4.0 h1:j/FynG7hi2azrBG5cvjRcnQ4sux/VNj8FAVc99Fl66c= +github.com/spiffe/go-spiffe/v2 v2.4.0/go.mod h1:m5qJ1hGzjxjtrkGHZupoXHo/FDWwCB1MdSyBzfHugx0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -1645,8 +1649,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= -google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/lib/auth/transport_credentials_test.go b/lib/auth/transport_credentials_test.go index 602eed0ddb2dd..c0a4519d51dcb 100644 --- a/lib/auth/transport_credentials_test.go +++ b/lib/auth/transport_credentials_test.go @@ -24,6 +24,7 @@ import ( "crypto/x509" "io" "net" + "slices" "testing" "time" @@ -286,7 +287,17 @@ func TestTransportCredentials_ServerHandshake(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { require.NoError(t, conn.Close()) }) - clientConn := tls.Client(conn, test.clientTLSConf) + // this would be done by the grpc TransportCredential in the grpc + // client, but we're going to fake it with just a tls.Client, so we + // have to add the http2 next proto ourselves (enforced by grpc-go + // starting from v1.67, and required by the http2 spec when speaking + // http2 in TLS) + clientTLSConf := test.clientTLSConf + if !slices.Contains(clientTLSConf.NextProtos, "h2") { + clientTLSConf = clientTLSConf.Clone() + clientTLSConf.NextProtos = append(clientTLSConf.NextProtos, "h2") + } + clientConn := tls.Client(conn, clientTLSConf) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() diff --git a/lib/tbot/cli/start_workload_identity_aws_ra.go b/lib/tbot/cli/start_workload_identity_aws_ra.go new file mode 100644 index 0000000000000..db250831ccfb6 --- /dev/null +++ b/lib/tbot/cli/start_workload_identity_aws_ra.go @@ -0,0 +1,172 @@ +// 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 . + +package cli + +import ( + "fmt" + "log/slog" + "time" + + "github.com/alecthomas/kingpin/v2" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/tbot/config" +) + +// WorkloadIdentityAWSRACommand implements `tbot start workload-identity-aws-ra` +// and `tbot configure workload-identity-aws-ra`. +type WorkloadIdentityAWSRACommand struct { + *sharedStartArgs + *sharedDestinationArgs + *genericMutatorHandler + + // NameSelector is the name of the workload identity to use. + // --workload-identity-name foo + NameSelector string + // LabelSelector is the labels of the workload identity to use. + // --workload-identity-labels x=y,z=a + LabelSelector string + + // RoleARN is the ARN of the role to assume. + // Example: `arn:aws:iam::123456789012:role/example-role` + // Required. + RoleARN string + // ProfileARN is the ARN of the Roles Anywhere profile to use. + // Example: `arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000` + // Required. + ProfileARN string + // TrustAnchorARN is the ARN of the Roles Anywhere trust anchor to use. + // Example: `arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000` + // Required. + TrustAnchorARN string + // Region is the AWS region to use. + // Example: `us-east-1` + // Must be set here or in the environment or AWS config using the + // `AWS_REGION` environment variable. If set here, this will override the + // environment or AWS config. + Region string + + // SessionDuration is the duration of the resulting AWS session and + // credentials. This may be up to 12 hours. When unset, this defaults to + // 6 hours. + SessionDuration time.Duration + // SessionRenewalInterval is the interval at which the session should be + // renewed. This should be less than the session duration. When unset, this + // defaults to 1 hour. + SessionRenewalInterval time.Duration +} + +// NewWorkloadIdentityAWSRACommand initializes the command and flags for the +// `workload-identity-aws-ra` output and returns a struct that will contain the parse +// result. +func NewWorkloadIdentityAWSRACommand( + parentCmd *kingpin.CmdClause, action MutatorAction, mode CommandMode, +) *WorkloadIdentityAWSRACommand { + cmd := parentCmd.Command( + "workload-identity-aws-roles-anywhere", + fmt.Sprintf( + "%s tbot with an output containing AWS credentials generated via AWS Roles Anywhere.", + mode, + ), + ) + + c := &WorkloadIdentityAWSRACommand{} + c.sharedStartArgs = newSharedStartArgs(cmd) + c.sharedDestinationArgs = newSharedDestinationArgs(cmd) + c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action) + + cmd.Flag( + "name-selector", + "The name of the workload identity to issue", + ).StringVar(&c.NameSelector) + cmd.Flag( + "label-selector", + "A label-based selector for which workload identities to issue. Multiple labels can be provided using ','.", + ).StringVar(&c.LabelSelector) + cmd.Flag( + "role-arn", + "The ARN of the role to assume.", + ).Required().StringVar(&c.RoleARN) + cmd.Flag( + "profile-arn", + "The ARN of the Roles Anywhere profile to use.", + ).Required().StringVar(&c.ProfileARN) + cmd.Flag( + "trust-anchor-arn", + "The ARN of the Roles Anywhere trust anchor to use.", + ).Required().StringVar(&c.TrustAnchorARN) + cmd.Flag( + "region", + "The AWS region to use. If unset, value will be used from the AWS config or the AWS_REGION environment variable.", + ).StringVar(&c.Region) + + cmd.Flag( + "session-duration", + "The duration of the resulting AWS session and credentials. This may be up to 12 hours. When unset, this defaults to 6 hours.", + ).DurationVar(&c.SessionDuration) + cmd.Flag( + "session-renewal-interval", + "How often the session should be renewed. This should be less than the session duration. When unset, this defaults to 1 hour.", + ).DurationVar(&c.SessionRenewalInterval) + + return c +} + +// ApplyConfig applies the parsed flags to the bot configuration. +func (c *WorkloadIdentityAWSRACommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) error { + if err := c.sharedStartArgs.ApplyConfig(cfg, l); err != nil { + return trace.Wrap(err) + } + + dest, err := c.BuildDestination() + if err != nil { + return trace.Wrap(err) + } + + svc := &config.WorkloadIdentityAWSRAService{ + Destination: dest, + RoleARN: c.RoleARN, + ProfileARN: c.ProfileARN, + TrustAnchorARN: c.TrustAnchorARN, + Region: c.Region, + SessionDuration: c.SessionDuration, + SessionRenewalInterval: c.SessionRenewalInterval, + } + + switch { + case c.NameSelector != "" && c.LabelSelector != "": + return trace.BadParameter("name-selector and label-selector flags are mutually exclusive") + case c.NameSelector != "": + svc.Selector.Name = c.NameSelector + case c.LabelSelector != "": + labels, err := client.ParseLabelSpec(c.LabelSelector) + if err != nil { + return trace.Wrap(err, "parsing --label-selector") + } + svc.Selector.Labels = map[string][]string{} + for k, v := range labels { + svc.Selector.Labels[k] = []string{v} + } + default: + return trace.BadParameter("name-selector or label-selector must be specified") + } + + cfg.Services = append(cfg.Services, svc) + + return nil +} diff --git a/lib/tbot/cli/start_workload_identity_aws_ra_test.go b/lib/tbot/cli/start_workload_identity_aws_ra_test.go new file mode 100644 index 0000000000000..0b4e974b88091 --- /dev/null +++ b/lib/tbot/cli/start_workload_identity_aws_ra_test.go @@ -0,0 +1,121 @@ +// 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 . + +package cli + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/lib/tbot/config" +) + +func TestWorkloadIdentityAWSRACommand(t *testing.T) { + testStartConfigureCommand(t, NewWorkloadIdentityAWSRACommand, []startConfigureTestCase{ + { + name: "success", + args: []string{ + "start", + "workload-identity-aws-roles-anywhere", + "--destination=/bar", + "--token=foo", + "--join-method=github", + "--proxy-server=example.com:443", + "--label-selector=*=*,foo=bar", + "--role-arn=arn:aws:iam::123456789012:role/example-role", + "--trust-anchor-arn=arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + "--profile-arn=arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + "--region=us-east-1", + "--session-duration=2h", + "--session-renewal-interval=30m", + }, + assertConfig: func(t *testing.T, cfg *config.BotConfig) { + require.Len(t, cfg.Services, 1) + + svc := cfg.Services[0] + wis, ok := svc.(*config.WorkloadIdentityAWSRAService) + require.True(t, ok) + + dir, ok := wis.Destination.(*config.DestinationDirectory) + require.True(t, ok) + require.Equal(t, "/bar", dir.Path) + + require.Equal(t, map[string][]string{ + "*": {"*"}, + "foo": {"bar"}, + }, wis.Selector.Labels) + require.Equal(t, "arn:aws:iam::123456789012:role/example-role", wis.RoleARN) + require.Equal( + t, + "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + wis.TrustAnchorARN, + ) + require.Equal( + t, + "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + wis.ProfileARN, + ) + require.Equal(t, "us-east-1", wis.Region) + + require.Equal(t, 2*time.Hour, wis.SessionDuration) + require.Equal(t, 30*time.Minute, wis.SessionRenewalInterval) + }, + }, + { + name: "success name selector", + args: []string{ + "start", + "workload-identity-aws-roles-anywhere", + "--destination=/bar", + "--token=foo", + "--join-method=github", + "--proxy-server=example.com:443", + "--name-selector=jim", + "--role-arn=arn:aws:iam::123456789012:role/example-role", + "--trust-anchor-arn=arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + "--profile-arn=arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + "--region=us-east-1", + }, + assertConfig: func(t *testing.T, cfg *config.BotConfig) { + require.Len(t, cfg.Services, 1) + + svc := cfg.Services[0] + wis, ok := svc.(*config.WorkloadIdentityAWSRAService) + require.True(t, ok) + + dir, ok := wis.Destination.(*config.DestinationDirectory) + require.True(t, ok) + require.Equal(t, "/bar", dir.Path) + + require.Equal(t, "jim", wis.Selector.Name) + require.Equal(t, "arn:aws:iam::123456789012:role/example-role", wis.RoleARN) + require.Equal( + t, + "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + wis.TrustAnchorARN, + ) + require.Equal( + t, + "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + wis.ProfileARN, + ) + require.Equal(t, "us-east-1", wis.Region) + }, + }, + }) +} diff --git a/lib/tbot/config/config.go b/lib/tbot/config/config.go index 7c56766ce7ccd..6df5330b75549 100644 --- a/lib/tbot/config/config.go +++ b/lib/tbot/config/config.go @@ -456,6 +456,12 @@ func (o *ServiceConfigs) UnmarshalYAML(node *yaml.Node) error { return trace.Wrap(err) } out = append(out, v) + case WorkloadIdentityAWSRAType: + v := &WorkloadIdentityAWSRAService{} + if err := node.Decode(v); err != nil { + return trace.Wrap(err) + } + out = append(out, v) default: return trace.BadParameter("unrecognized service type (%s)", header.Type) } diff --git a/lib/tbot/config/service_workload_identity_aws_ra.go b/lib/tbot/config/service_workload_identity_aws_ra.go new file mode 100644 index 0000000000000..eb81a3c07beda --- /dev/null +++ b/lib/tbot/config/service_workload_identity_aws_ra.go @@ -0,0 +1,181 @@ +// 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 . + +package config + +import ( + "context" + "time" + + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/gravitational/trace" + "gopkg.in/yaml.v3" + + "github.com/gravitational/teleport/api/utils/aws" + "github.com/gravitational/teleport/lib/tbot/bot" +) + +const ( + WorkloadIdentityAWSRAType = "workload-identity-aws-roles-anywhere" + defaultAWSSessionDuration = 6 * time.Hour + maxAWSSessionDuration = 12 * time.Hour + defaultAWSSessionRenewalInterval = 1 * time.Hour +) + +var ( + _ ServiceConfig = &WorkloadIdentityAWSRAService{} + _ Initable = &WorkloadIdentityAWSRAService{} +) + +// WorkloadIdentityAWSRAService is the configuration for the +// WorkloadIdentityAWSRAService +type WorkloadIdentityAWSRAService struct { + // Selector is the selector for the WorkloadIdentity resource that will be + // used to issue WICs. + Selector WorkloadIdentitySelector `yaml:"selector"` + // Destination is where the credentials should be written to. + Destination bot.Destination `yaml:"destination"` + + // RoleARN is the ARN of the role to assume. + // Example: `arn:aws:iam::123456789012:role/example-role` + // Required. + RoleARN string `yaml:"role_arn"` + // ProfileARN is the ARN of the Roles Anywhere profile to use. + // Example: `arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000` + // Required. + ProfileARN string `yaml:"profile_arn"` + // TrustAnchorARN is the ARN of the Roles Anywhere trust anchor to use. + // Example: `arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000` + // Required. + TrustAnchorARN string `yaml:"trust_anchor_arn"` + // Region is the AWS region to use. + // Example: `us-east-1` + // Must be set here or in the environment or AWS config using the + // `AWS_REGION` environment variable. If set here, this will override the + // environment or AWS config. + Region string `yaml:"region"` + + // SessionDuration is the duration of the resulting AWS session and + // credentials. This may be up to 12 hours. When unset, this defaults to + // 6 hours. + SessionDuration time.Duration `yaml:"session_duration"` + // SessionRenewalInterval is the interval at which the session should be + // renewed. This should be less than the session duration. When unset, this + // defaults to 1 hour. + SessionRenewalInterval time.Duration `yaml:"session_renewal_interval"` + + // EndpointOverride is the endpoint to use for the AWS Roles Anywhere service. + // This is designed to be leveraged by tests and unset in production + // circumstances. + EndpointOverride string `yaml:"-"` +} + +// Init initializes the destination. +func (o *WorkloadIdentityAWSRAService) Init(ctx context.Context) error { + return trace.Wrap(o.Destination.Init(ctx, []string{})) +} + +// CheckAndSetDefaults checks the WorkloadIdentityAWSRAService values and sets any defaults. +func (o *WorkloadIdentityAWSRAService) CheckAndSetDefaults() error { + if err := validateOutputDestination(o.Destination); err != nil { + return trace.Wrap(err) + } + if err := o.Selector.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err, "validating selector") + } + + switch { + case o.RoleARN == "": + return trace.BadParameter("role_arn: must be set") + case o.ProfileARN == "": + return trace.BadParameter("profile_arn: must be set") + case o.TrustAnchorARN == "": + return trace.BadParameter("trust_anchor_arn: must be set") + } + if _, err := arn.Parse(o.RoleARN); err != nil { + return trace.Wrap(err, "parsing role_arn") + } + if _, err := arn.Parse(o.ProfileARN); err != nil { + return trace.Wrap(err, "parsing profile_arn") + } + if _, err := arn.Parse(o.TrustAnchorARN); err != nil { + return trace.Wrap(err, "parsing trust_anchor_arn") + } + if o.Region != "" { + if err := aws.IsValidRegion(o.Region); err != nil { + return trace.Wrap(err, "validating region") + } + } + + if o.SessionDuration == 0 { + o.SessionDuration = defaultAWSSessionDuration + } + if o.SessionDuration > maxAWSSessionDuration { + return trace.BadParameter("session_duration: must be less than or equal to 12 hours") + } + if o.SessionRenewalInterval == 0 { + o.SessionRenewalInterval = defaultAWSSessionRenewalInterval + } + if o.SessionRenewalInterval >= o.SessionDuration { + return trace.BadParameter("session_renewal_interval: must be less than session_duration") + } + + return nil +} + +// Describe returns the file descriptions for the WorkloadIdentityJWTService. +func (o *WorkloadIdentityAWSRAService) Describe() []FileDescription { + fds := []FileDescription{ + { + Name: JWTSVIDPath, + }, + } + return fds +} + +func (o *WorkloadIdentityAWSRAService) Type() string { + return WorkloadIdentityAWSRAType +} + +// MarshalYAML marshals the WorkloadIdentityJWTService into YAML. +func (o *WorkloadIdentityAWSRAService) MarshalYAML() (interface{}, error) { + type raw WorkloadIdentityAWSRAService + return withTypeHeader((*raw)(o), WorkloadIdentityAWSRAType) +} + +// UnmarshalYAML unmarshals the WorkloadIdentityJWTService from YAML. +func (o *WorkloadIdentityAWSRAService) UnmarshalYAML(node *yaml.Node) error { + dest, err := extractOutputDestination(node) + if err != nil { + return trace.Wrap(err) + } + // Alias type to remove UnmarshalYAML to avoid recursion + type raw WorkloadIdentityAWSRAService + if err := node.Decode((*raw)(o)); err != nil { + return trace.Wrap(err) + } + o.Destination = dest + return nil +} + +// GetDestination returns the destination. +func (o *WorkloadIdentityAWSRAService) GetDestination() bot.Destination { + return o.Destination +} + +func (o *WorkloadIdentityAWSRAService) GetCredentialLifetime() CredentialLifetime { + return CredentialLifetime{} +} diff --git a/lib/tbot/config/service_workload_identity_aws_ra_test.go b/lib/tbot/config/service_workload_identity_aws_ra_test.go new file mode 100644 index 0000000000000..0ed20e1e6e87a --- /dev/null +++ b/lib/tbot/config/service_workload_identity_aws_ra_test.go @@ -0,0 +1,308 @@ +// 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 . + +package config + +import ( + "testing" + "time" + + "github.com/gravitational/teleport/lib/tbot/botfs" +) + +func TestWorkloadIdentityAWSRAService_YAML(t *testing.T) { + t.Parallel() + + dest := &DestinationMemory{} + tests := []testYAMLCase[WorkloadIdentityAWSRAService]{ + { + name: "full", + in: WorkloadIdentityAWSRAService{ + Destination: dest, + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + SessionDuration: time.Minute * 59, + SessionRenewalInterval: time.Minute * 29, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + }, + }, + } + testYAML(t, tests) +} + +func TestWorkloadIdentityAWSRAService_CheckAndSetDefaults(t *testing.T) { + t.Parallel() + + tests := []testCheckAndSetDefaultsCase[*WorkloadIdentityAWSRAService]{ + { + name: "valid", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + want: &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + SessionDuration: defaultAWSSessionDuration, + SessionRenewalInterval: defaultAWSSessionRenewalInterval, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + }, + }, + { + name: "valid with labels", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Labels: map[string][]string{ + "key": {"value"}, + }, + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + SessionDuration: 1 * time.Hour, + SessionRenewalInterval: 30 * time.Minute, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + }, + { + name: "missing selectors", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{}, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + wantErr: "one of ['name', 'labels'] must be set", + }, + { + name: "too many selectors", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + Labels: map[string][]string{ + "key": {"value"}, + }, + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + wantErr: "at most one of ['name', 'labels'] can be set", + }, + { + name: "missing destination", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Destination: nil, + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + wantErr: "no destination configured for output", + }, + { + name: "missing role_arn", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + wantErr: "role_arn: must be set", + }, + { + name: "missing trust_anchor_arn", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + wantErr: "trust_anchor_arn: must be set", + }, + { + name: "missing profile_arn", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + Region: "us-east-1", + } + }, + wantErr: "profile_arn: must be set", + }, + { + name: "invalid role arn", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "foo", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + wantErr: "arn: invalid prefix", + }, + { + name: "invalid trust anchor arn", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "foo", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1", + } + }, + wantErr: "arn: invalid prefix", + }, + { + name: "invalid profile arn", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "foo", + Region: "us-east-1", + } + }, + wantErr: "arn: invalid prefix", + }, + { + name: "invalid region", + in: func() *WorkloadIdentityAWSRAService { + return &WorkloadIdentityAWSRAService{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + RoleARN: "arn:aws:iam::123456789012:role/example-role", + TrustAnchorARN: "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000", + ProfileARN: "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000", + Region: "us-east-1!!!!", + } + }, + wantErr: "validating region", + }, + } + testCheckAndSetDefaults(t, tests) +} diff --git a/lib/tbot/config/testdata/TestWorkloadIdentityAWSRAService_YAML/full.golden b/lib/tbot/config/testdata/TestWorkloadIdentityAWSRAService_YAML/full.golden new file mode 100644 index 0000000000000..4f3a9812064b9 --- /dev/null +++ b/lib/tbot/config/testdata/TestWorkloadIdentityAWSRAService_YAML/full.golden @@ -0,0 +1,11 @@ +type: workload-identity-aws-roles-anywhere +selector: + name: my-workload-identity +destination: + type: memory +role_arn: arn:aws:iam::123456789012:role/example-role +profile_arn: arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000 +trust_anchor_arn: arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000 +region: us-east-1 +session_duration: 59m0s +session_renewal_interval: 29m0s diff --git a/lib/tbot/service_workload_identity_aws_ra.go b/lib/tbot/service_workload_identity_aws_ra.go new file mode 100644 index 0000000000000..1f3167fc04b30 --- /dev/null +++ b/lib/tbot/service_workload_identity_aws_ra.go @@ -0,0 +1,260 @@ +// 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 . + +package tbot + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "fmt" + "log/slog" + "time" + + "github.com/gravitational/trace" + awsspiffe "github.com/spiffe/aws-spiffe-workload-helper" + "github.com/spiffe/aws-spiffe-workload-helper/vendoredaws" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "gopkg.in/ini.v1" + + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/reversetunnelclient" + "github.com/gravitational/teleport/lib/tbot/bot" + "github.com/gravitational/teleport/lib/tbot/config" + "github.com/gravitational/teleport/lib/tbot/identity" + "github.com/gravitational/teleport/lib/tbot/workloadidentity" +) + +// WorkloadIdentityAWSRAService is a service that retrieves X.509 certificates +// and exchanges them for AWS credentials using the AWS Roles Anywhere service. +type WorkloadIdentityAWSRAService struct { + botAuthClient *authclient.Client + botCfg *config.BotConfig + cfg *config.WorkloadIdentityAWSRAService + getBotIdentity getBotIdentityFn + log *slog.Logger + resolver reversetunnelclient.Resolver + reloadBroadcaster *channelBroadcaster +} + +// String returns a human-readable description of the service. +func (s *WorkloadIdentityAWSRAService) String() string { + return fmt.Sprintf("workload-identity-aws-roles-anywhere (%s)", s.cfg.Destination.String()) +} + +// OneShot runs the service once, generating the output and writing it to the +// destination, before exiting. +func (s *WorkloadIdentityAWSRAService) OneShot(ctx context.Context) error { + return s.generate(ctx) +} + +// Run runs the service in a loop, generating the output and writing it to the +// destination at regular intervals. +func (s *WorkloadIdentityAWSRAService) Run(ctx context.Context) error { + reloadCh, unsubscribe := s.reloadBroadcaster.subscribe() + defer unsubscribe() + + err := runOnInterval(ctx, runOnIntervalConfig{ + service: s.String(), + name: "output-renewal", + f: s.generate, + interval: s.cfg.SessionRenewalInterval, + retryLimit: renewalRetryLimit, + log: s.log, + reloadCh: reloadCh, + }) + return trace.Wrap(err) +} + +func (s *WorkloadIdentityAWSRAService) generate(ctx context.Context) error { + res, privateKey, err := s.requestSVID(ctx) + if err != nil { + return trace.Wrap(err, "requesting SVID") + } + pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return trace.Wrap(err, "marshaling private key") + } + svid, err := x509svid.ParseRaw(res.GetX509Svid().Cert, pkcs8) + if err != nil { + return trace.Wrap(err, "parsing x509 svid") + } + + s.log.InfoContext( + ctx, + "Exchanging SVID for AWS credentials", + "spiffe_id", svid.ID.String(), + "role_arn", s.cfg.RoleARN, + "profile_arn", s.cfg.ProfileARN, + "trust_anchor_arn", s.cfg.TrustAnchorARN, + ) + creds, err := s.exchangeSVID(svid) + if err != nil { + return trace.Wrap(err, "exchanging SVID via Roles Anywhere") + } + s.log.InfoContext( + ctx, + "Exchanged SVID for AWS credentials", + "aws_credentials_expiry", creds.Expiration, + ) + + return renderAWSCreds(ctx, creds, s.cfg.Destination) +} + +// exchangeSVID will exchange the X.509 SVID for AWS credentials using the +// AWS Roles Anywhere service. +func (s *WorkloadIdentityAWSRAService) exchangeSVID( + svid *x509svid.SVID, +) (*vendoredaws.CredentialProcessOutput, error) { + signer := &awsspiffe.X509SVIDSigner{ + SVID: svid, + } + algo, err := signer.SignatureAlgorithm() + if err != nil { + return nil, trace.Wrap(err, "getting signature algorithm") + } + + credentials, err := vendoredaws.GenerateCredentials(&vendoredaws.CredentialsOpts{ + RoleArn: s.cfg.RoleARN, + ProfileArnStr: s.cfg.ProfileARN, + Region: s.cfg.Region, + TrustAnchorArnStr: s.cfg.TrustAnchorARN, + SessionDuration: int(s.cfg.SessionDuration.Seconds()), + Endpoint: s.cfg.EndpointOverride, + }, signer, algo) + if err != nil { + return nil, trace.Wrap(err, "exchanging credentials") + } + + return &credentials, nil +} + +func (s *WorkloadIdentityAWSRAService) requestSVID( + ctx context.Context, +) ( + *workloadidentityv1pb.Credential, + crypto.Signer, + error, +) { + ctx, span := tracer.Start( + ctx, + "WorkloadIdentityAWSRAService/requestSVID", + ) + defer span.End() + + roles, err := fetchDefaultRoles(ctx, s.botAuthClient, s.getBotIdentity()) + if err != nil { + return nil, nil, trace.Wrap(err, "fetching roles") + } + + id, err := generateIdentity( + ctx, + s.botAuthClient, + s.getBotIdentity(), + roles, + // We only need this to issue the X509 SVID, so we don't need the full + // lifetime. + time.Minute*10, + nil, + ) + if err != nil { + return nil, nil, trace.Wrap(err, "generating identity") + } + // create a client that uses the impersonated identity, so that when we + // fetch information, we can ensure access rights are enforced. + facade := identity.NewFacade(s.botCfg.FIPS, s.botCfg.Insecure, id) + impersonatedClient, err := clientForFacade(ctx, s.log, s.botCfg, facade, s.resolver) + if err != nil { + return nil, nil, trace.Wrap(err) + } + defer impersonatedClient.Close() + + x509Credentials, privateKey, err := workloadidentity.IssueX509WorkloadIdentity( + ctx, + s.log, + impersonatedClient, + s.cfg.Selector, + // We only use this SVID to exchange for AWS credentials, so we don't + // need the full lifetime. + time.Minute*10, + nil, + ) + if err != nil { + return nil, nil, trace.Wrap(err, "generating X509 SVID") + } + var x509Credential *workloadidentityv1pb.Credential + switch len(x509Credentials) { + case 0: + return nil, nil, trace.BadParameter("no X509 SVIDs returned") + case 1: + x509Credential = x509Credentials[0] + default: + // We could eventually implement some kind of hint selection mechanism + // to pick the "right" one. + received := make([]string, 0, len(x509Credentials)) + for _, cred := range x509Credentials { + received = append(received, + fmt.Sprintf( + "%s:%s", + cred.WorkloadIdentityName, + cred.SpiffeId, + ), + ) + } + return nil, nil, trace.BadParameter( + "multiple X509 SVIDs received: %v", received, + ) + } + + return x509Credential, privateKey, nil +} + +// render will write the AWS credentials to the AWS CLI configuration file. +// See https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html +func renderAWSCreds( + ctx context.Context, creds *vendoredaws.CredentialProcessOutput, dest bot.Destination, +) error { + ctx, span := tracer.Start( + ctx, + "renderAWSCreds", + ) + defer span.End() + + // TODO(noah): At a later date, we can add a mode where we read in and + // modify an existing profile within the credentials file - without + // overwriting other profiles. + f := ini.Empty() + // "default" is the name of the section that the AWS CLI will use by + // default. + sec := f.Section("default") + sec.Key("aws_secret_access_key").SetValue(creds.SecretAccessKey) + sec.Key("aws_access_key_id").SetValue(creds.AccessKeyId) + sec.Key("aws_session_token").SetValue(creds.SessionToken) + + b := &bytes.Buffer{} + _, err := f.WriteTo(b) + if err != nil { + return trace.Wrap(err, "writing credentials to buffer") + } + + err = dest.Write(ctx, "aws_credentials", b.Bytes()) + if err != nil { + return trace.Wrap(err, "writing credentials to destination") + } + return nil +} diff --git a/lib/tbot/service_workload_identity_aws_ra_test.go b/lib/tbot/service_workload_identity_aws_ra_test.go new file mode 100644 index 0000000000000..41c509d0b6121 --- /dev/null +++ b/lib/tbot/service_workload_identity_aws_ra_test.go @@ -0,0 +1,215 @@ +// 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 . +package tbot + +import ( + "context" + "crypto/x509" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/spiffe/aws-spiffe-workload-helper/vendoredaws" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/api/types" + apiutils "github.com/gravitational/teleport/api/utils" + "github.com/gravitational/teleport/lib/tbot/config" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/lib/utils/testutils/golden" + "github.com/gravitational/teleport/tool/teleport/testenv" +) + +func Test_renderAWSCreds(t *testing.T) { + creds := &vendoredaws.CredentialProcessOutput{ + AccessKeyId: "AKIAIOSFODNN7EXAMPLEAKID", + SessionToken: "AQoDYXdzEJrtyWJ4NjK7PiEXAMPLEST", + SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLESAK", + } + ctx := context.Background() + + dest := &config.DestinationMemory{} + require.NoError(t, dest.CheckAndSetDefaults()) + require.NoError(t, dest.Init(ctx, []string{})) + + err := renderAWSCreds(ctx, creds, dest) + require.NoError(t, err) + + got, err := dest.Read(ctx, "aws_credentials") + require.NoError(t, err) + + if golden.ShouldSet() { + golden.Set(t, got) + } + require.Equal(t, golden.Get(t), got) +} + +type mockCreateSessionInputBody struct { + DurationSeconds int `json:"durationSeconds"` +} + +func TestBotWorkloadIdentityAWSRA(t *testing.T) { + t.Parallel() + ctx := context.Background() + log := utils.NewSlogLoggerForTests() + + process := testenv.MakeTestServer(t, defaultTestServerOpts(t, log)) + rootClient := testenv.MakeDefaultAuthClient(t, process) + + roleArn := "arn:aws:iam::123456789012:role/example-role" + trustAnchorArn := "arn:aws:rolesanywhere:us-east-1:123456789012:trust-anchor/0000000-0000-0000-0000-000000000000" + profileArn := "arn:aws:rolesanywhere:us-east-1:123456789012:profile/0000000-0000-0000-0000-00000000000" + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/sessions", r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + + // Check query parameter inputs + // The AWS documentation "lies" about these inputs using the JSON body + // - the rolesanywhere API client in + // `aws/rolesanywhere-credential-helper` uses query parameters for + // these. + assert.Equal(t, roleArn, r.URL.Query().Get("roleArn")) + assert.Equal(t, trustAnchorArn, r.URL.Query().Get("trustAnchorArn")) + assert.Equal(t, profileArn, r.URL.Query().Get("profileArn")) + + // Check JSON body inputs + body := &mockCreateSessionInputBody{} + assert.NoError(t, json.NewDecoder(r.Body).Decode(body)) + assert.Equal(t, int((2 * time.Hour).Seconds()), body.DurationSeconds) + + // Validate the X-Amz-X509 header contains the valid (and correct) SVID + derString := r.Header.Get("X-Amz-X509") + assert.NotEmpty(t, derString) + derBytes, err := base64.StdEncoding.DecodeString(derString) + assert.NoError(t, err) + cert, err := x509.ParseCertificate(derBytes) + assert.NoError(t, err) + assert.Len(t, cert.URIs, 1) + assert.Equal(t, "spiffe://root/ra-test", cert.URIs[0].String()) + + // Validate the authorization header exists. We rely on the AWS SDK to + // actually produce the signature, and, validating this signature would + // introduce significant complexity to this test - so this is omitted. + authz := r.Header.Get("Authorization") + assert.NotEmpty(t, authz) + + // Send mocked response + _, _ = w.Write([]byte(`{ + "credentialSet":[ + { + "assumedRoleUser": { + "arn": "arn:aws:iam::123456789012:role/example-role", + "assumedRoleId": "assumedRoleId" + }, + "credentials":{ + "accessKeyId": "accessKeyId", + "expiration": "2028-07-27T04:36:55Z", + "secretAccessKey": "secretAccessKey", + "sessionToken": "sessionToken" + }, + "packedPolicySize": 10, + "roleArn": "arn:aws:iam::123456789012:role/example-role", + "sourceIdentity": "sourceIdentity" + } + ], + "subjectArn": "arn:aws:rolesanywhere:us-east-1:000000000000:subject/41cl0bae-6783-40d4-ab20-65dc5d922e45" + }`)) + })) + t.Cleanup(srv.Close) + + role, err := types.NewRole("issue-foo", types.RoleSpecV6{ + Allow: types.RoleConditions{ + WorkloadIdentityLabels: map[string]apiutils.Strings{ + "foo": []string{"bar"}, + }, + Rules: []types.Rule{ + { + Resources: []string{types.KindWorkloadIdentity}, + Verbs: []string{types.VerbRead, types.VerbList}, + }, + }, + }, + }) + require.NoError(t, err) + role, err = rootClient.UpsertRole(ctx, role) + require.NoError(t, err) + + workloadIdentity := &workloadidentityv1pb.WorkloadIdentity{ + Kind: types.KindWorkloadIdentity, + Version: types.V1, + Metadata: &headerv1.Metadata{ + Name: "foo-bar-bizz", + Labels: map[string]string{ + "foo": "bar", + }, + }, + Spec: &workloadidentityv1pb.WorkloadIdentitySpec{ + Spiffe: &workloadidentityv1pb.WorkloadIdentitySPIFFE{ + Id: "/ra-test", + }, + }, + } + workloadIdentity, err = rootClient.WorkloadIdentityResourceServiceClient(). + CreateWorkloadIdentity(ctx, &workloadidentityv1pb.CreateWorkloadIdentityRequest{ + WorkloadIdentity: workloadIdentity, + }) + require.NoError(t, err) + + tmpDir := t.TempDir() + onboarding, _ := makeBot(t, rootClient, "ra-test", role.GetName()) + botConfig := defaultBotConfig(t, process, onboarding, config.ServiceConfigs{ + &config.WorkloadIdentityAWSRAService{ + Selector: config.WorkloadIdentitySelector{ + Name: workloadIdentity.GetMetadata().GetName(), + }, + Destination: &config.DestinationDirectory{ + Path: tmpDir, + }, + RoleARN: roleArn, + ProfileARN: profileArn, + TrustAnchorARN: trustAnchorArn, + Region: "us-east-1", + SessionDuration: 2 * time.Hour, + SessionRenewalInterval: 30 * time.Minute, + EndpointOverride: srv.URL, + }, + }, defaultBotConfigOpts{ + useAuthServer: true, + insecure: true, + }) + + botConfig.Oneshot = true + b := New(botConfig, log) + // Run Bot with 10 second timeout to catch hangs. + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + t.Cleanup(cancel) + require.NoError(t, b.Run(ctx)) + + got, err := os.ReadFile(filepath.Join(tmpDir, "aws_credentials")) + require.NoError(t, err) + if golden.ShouldSet() { + golden.Set(t, got) + } + require.Equal(t, string(golden.Get(t)), string(got)) +} diff --git a/lib/tbot/tbot.go b/lib/tbot/tbot.go index a1a133e1bafad..2a4c18b3ed6e5 100644 --- a/lib/tbot/tbot.go +++ b/lib/tbot/tbot.go @@ -608,6 +608,19 @@ func (b *Bot) Run(ctx context.Context) (err error) { teleport.ComponentKey, teleport.Component(componentTBot, "svc", svc.String()), ) services = append(services, svc) + case *config.WorkloadIdentityAWSRAService: + svc := &WorkloadIdentityAWSRAService{ + botCfg: b.cfg, + cfg: svcCfg, + resolver: resolver, + botAuthClient: b.botIdentitySvc.GetClient(), + getBotIdentity: b.botIdentitySvc.GetIdentity, + reloadBroadcaster: reloadBroadcaster, + } + svc.log = b.log.With( + teleport.ComponentKey, teleport.Component(componentTBot, "svc", svc.String()), + ) + services = append(services, svc) default: return trace.BadParameter("unknown service type: %T", svcCfg) } diff --git a/lib/tbot/testdata/TestBotWorkloadIdentityAWSRA.golden b/lib/tbot/testdata/TestBotWorkloadIdentityAWSRA.golden new file mode 100644 index 0000000000000..ae1150fb2ee6d --- /dev/null +++ b/lib/tbot/testdata/TestBotWorkloadIdentityAWSRA.golden @@ -0,0 +1,4 @@ +[default] +aws_secret_access_key=secretAccessKey +aws_access_key_id=accessKeyId +aws_session_token=sessionToken diff --git a/lib/tbot/testdata/Test_renderAWSCreds.golden b/lib/tbot/testdata/Test_renderAWSCreds.golden new file mode 100644 index 0000000000000..0a55d7dad24b5 --- /dev/null +++ b/lib/tbot/testdata/Test_renderAWSCreds.golden @@ -0,0 +1,4 @@ +[default] +aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLESAK +aws_access_key_id=AKIAIOSFODNN7EXAMPLEAKID +aws_session_token=AQoDYXdzEJrtyWJ4NjK7PiEXAMPLEST diff --git a/lib/teleterm/teleterm_test.go b/lib/teleterm/teleterm_test.go index 854273d71c683..bf7b2f6a2e548 100644 --- a/lib/teleterm/teleterm_test.go +++ b/lib/teleterm/teleterm_test.go @@ -27,6 +27,7 @@ import ( "net" "os" "path/filepath" + "slices" "testing" "time" @@ -226,5 +227,13 @@ func createValidClientTLSConfig(t *testing.T, certsDir string) *tls.Config { tlsConfig, err := createClientTLSConfig(clientCert, serverCertPath) require.NoError(t, err) + // this would be done by the grpc TransportCredential in the grpc client, + // but we're going to fake it with just a tls.Client, so we have to add the + // http2 next proto ourselves (enforced by grpc-go starting from v1.67, and + // required by the http2 spec when speaking http2 in TLS) + if !slices.Contains(tlsConfig.NextProtos, "h2") { + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") + } + return tlsConfig } diff --git a/tool/tbot/main.go b/tool/tbot/main.go index e9e22d506934e..293d7c0a5d45e 100644 --- a/tool/tbot/main.go +++ b/tool/tbot/main.go @@ -152,6 +152,9 @@ func Run(args []string, stdout io.Writer) error { cli.NewWorkloadIdentityJWTCommand(startCmd, buildConfigAndStart(ctx, globalCfg), cli.CommandModeStart), cli.NewWorkloadIdentityJWTCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout), cli.CommandModeConfigure), + + cli.NewWorkloadIdentityAWSRACommand(startCmd, buildConfigAndStart(ctx, globalCfg), cli.CommandModeStart), + cli.NewWorkloadIdentityAWSRACommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout), cli.CommandModeConfigure), ) // Initialize legacy-style commands. These are simple enough to not really From 77e29ea7e244ea4e34985d62f97786ff6964d95d Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Tue, 25 Mar 2025 18:23:13 +0000 Subject: [PATCH 2/2] Bump grpc to 1.68 --- go.mod | 6 +++--- go.sum | 12 ++++++------ integrations/event-handler/go.mod | 2 +- integrations/event-handler/go.sum | 4 ++-- integrations/terraform/go.mod | 4 ++-- integrations/terraform/go.sum | 12 ++++++------ 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 18a7fb33f5b2e..f91b2d360a910 100644 --- a/go.mod +++ b/go.mod @@ -219,7 +219,7 @@ require ( golang.zx2c4.com/wireguard/windows v0.5.3 google.golang.org/api v0.197.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 - google.golang.org/grpc v1.67.1 + google.golang.org/grpc v1.68.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 google.golang.org/protobuf v1.35.2 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c @@ -247,7 +247,7 @@ require ( ) require ( - cel.dev/expr v0.16.0 // indirect + cel.dev/expr v0.16.1 // indirect cloud.google.com/go v0.115.1 // indirect cloud.google.com/go/auth v0.9.4 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect @@ -305,7 +305,7 @@ require ( github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.0 // indirect github.com/cloudflare/cfssl v1.6.4 // indirect - github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 // indirect + github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/containerd/containerd v1.7.27 // indirect github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect diff --git a/go.sum b/go.sum index 469ca553a9e7a..9703091e6ee10 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= -cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g= +cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -1051,8 +1051,8 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 h1:fLZ97KE86ELjEYJCEUVzmbhfzDxHHGwBrDVMd4XL6Bs= -github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= @@ -3090,8 +3090,8 @@ google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= diff --git a/integrations/event-handler/go.mod b/integrations/event-handler/go.mod index 1eec3a5636636..40eaea25aca7b 100644 --- a/integrations/event-handler/go.mod +++ b/integrations/event-handler/go.mod @@ -298,7 +298,7 @@ require ( google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.67.1 // indirect + google.golang.org/grpc v1.68.0 // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/integrations/event-handler/go.sum b/integrations/event-handler/go.sum index b2393a73f2dc3..22c7e03a2b971 100644 --- a/integrations/event-handler/go.sum +++ b/integrations/event-handler/go.sum @@ -1026,8 +1026,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod index f020539faaedf..3b34d78f0b352 100644 --- a/integrations/terraform/go.mod +++ b/integrations/terraform/go.mod @@ -23,7 +23,7 @@ require ( github.com/jonboulle/clockwork v0.4.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 - google.golang.org/grpc v1.67.1 + google.golang.org/grpc v1.68.0 google.golang.org/protobuf v1.35.2 ) @@ -121,7 +121,7 @@ require ( github.com/charlievieth/strcase v0.0.5 // indirect github.com/cloudflare/cfssl v1.6.4 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 // indirect + github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect github.com/containerd/containerd v1.7.27 // indirect github.com/containerd/errdefs v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 73425ee4127de..de1a27f8460f0 100644 --- a/integrations/terraform/go.sum +++ b/integrations/terraform/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.16.0 h1:yloc84fytn4zmJX2GU3TkXGsaieaV7dQ057Qs4sIG2Y= -cel.dev/expr v0.16.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg= +cel.dev/expr v0.16.1 h1:NR0+oFYzR1CqLFhTAqg3ql59G9VfN8fKq1TCHJ6gq1g= +cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -340,8 +340,8 @@ github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtM github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59 h1:fLZ97KE86ELjEYJCEUVzmbhfzDxHHGwBrDVMd4XL6Bs= -github.com/cncf/xds/go v0.0.0-20240822171458-6449f94b4d59/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII= @@ -1649,8 +1649,8 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= -google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= +google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=