From bafdcb94ce398349a43a1604397c564279bf6447 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Wed, 15 Jan 2025 10:05:34 +0000 Subject: [PATCH] Workload Identity: Add `workload-identity-x509` service to `tbot` (#50812) * Add config for new output * Add tests * rename * rename * Add simple impl for WorkloadIdentityX509Service * Add support for label based issuance * Add support for specifying selectors via cli * Add `TestBotWorkloadIdentityX509` * Add note on removing hidden flag * Add more thorough logging * Remove unnecessary slice copy * Update terminology * Reshuffle and rename * Fix broken build * Fix more building * Rename name/label selector * Rename selector * Add godocs * Nicer error messge --- api/client/client.go | 6 + lib/tbot/config/config.go | 6 + lib/tbot/config/config_test.go | 8 + .../config/service_spiffe_workload_api.go | 2 +- .../service_spiffe_workload_api_test.go | 2 +- .../config/service_workload_identity_x509.go | 136 ++++++++ .../service_workload_identity_x509_test.go | 133 ++++++++ .../TestBotConfig_YAML/standard_config.golden | 6 + .../full.golden | 6 + .../minimal.golden | 5 + lib/tbot/service_spiffe_svid_output.go | 8 +- lib/tbot/service_spiffe_workload_api.go | 8 +- lib/tbot/service_spiffe_workload_api_sds.go | 8 +- .../service_spiffe_workload_api_sds_test.go | 14 +- lib/tbot/service_spiffe_workload_api_test.go | 2 +- lib/tbot/service_workload_identity_x509.go | 305 ++++++++++++++++++ .../service_workload_identity_x509_test.go | 149 +++++++++ lib/tbot/tbot.go | 27 +- lib/tbot/workloadidentity/issue.go | 149 +++++++++ .../trust_bundle_cache.go | 2 +- .../trust_bundle_cache_test.go | 2 +- .../workloadattest/attest.go | 0 .../workloadattest/kubernetes.go | 0 .../workloadattest/kubernetes_unix.go | 0 .../workloadattest/kubernetes_unix_test.go | 0 .../workloadattest/kubernetes_windows.go | 0 .../mountfile/k8s-real-docker-desktop | 0 .../k8s-real-gcp-v1.29.5-gke.1091002 | 0 .../k8s-real-k3s-ubuntu-v1.28.6+k3s2 | 0 .../testdata/mountfile/k8s-real-orbstack | 0 .../workloadattest/unix.go | 0 .../workloadattest/unix_test.go | 0 32 files changed, 956 insertions(+), 28 deletions(-) create mode 100644 lib/tbot/config/service_workload_identity_x509.go create mode 100644 lib/tbot/config/service_workload_identity_x509_test.go create mode 100644 lib/tbot/config/testdata/TestWorkloadIdentityX509Service_YAML/full.golden create mode 100644 lib/tbot/config/testdata/TestWorkloadIdentityX509Service_YAML/minimal.golden create mode 100644 lib/tbot/service_workload_identity_x509.go create mode 100644 lib/tbot/service_workload_identity_x509_test.go create mode 100644 lib/tbot/workloadidentity/issue.go rename lib/tbot/{spiffe => workloadidentity}/trust_bundle_cache.go (99%) rename lib/tbot/{spiffe => workloadidentity}/trust_bundle_cache_test.go (99%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/attest.go (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/kubernetes.go (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/kubernetes_unix.go (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/kubernetes_unix_test.go (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/kubernetes_windows.go (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/testdata/mountfile/k8s-real-docker-desktop (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/testdata/mountfile/k8s-real-gcp-v1.29.5-gke.1091002 (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/testdata/mountfile/k8s-real-k3s-ubuntu-v1.28.6+k3s2 (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/testdata/mountfile/k8s-real-orbstack (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/unix.go (100%) rename lib/tbot/{spiffe => workloadidentity}/workloadattest/unix_test.go (100%) diff --git a/api/client/client.go b/api/client/client.go index f8cb9c54f01d4..5eff6fb40fa0b 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -872,6 +872,12 @@ func (c *Client) WorkloadIdentityResourceServiceClient() workloadidentityv1pb.Wo return workloadidentityv1pb.NewWorkloadIdentityResourceServiceClient(c.conn) } +// WorkloadIdentityIssuanceClient returns an unadorned client for the workload +// identity service. +func (c *Client) WorkloadIdentityIssuanceClient() workloadidentityv1pb.WorkloadIdentityIssuanceServiceClient { + return workloadidentityv1pb.NewWorkloadIdentityIssuanceServiceClient(c.conn) +} + // PresenceServiceClient returns an unadorned client for the presence service. func (c *Client) PresenceServiceClient() presencepb.PresenceServiceClient { return presencepb.NewPresenceServiceClient(c.conn) diff --git a/lib/tbot/config/config.go b/lib/tbot/config/config.go index 91263638db0eb..a3834b0126540 100644 --- a/lib/tbot/config/config.go +++ b/lib/tbot/config/config.go @@ -551,6 +551,12 @@ func (o *ServiceConfigs) UnmarshalYAML(node *yaml.Node) error { return trace.Wrap(err) } out = append(out, v) + case WorkloadIdentityX509OutputType: + v := &WorkloadIdentityX509Service{} + 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/config_test.go b/lib/tbot/config/config_test.go index 97f33460f2c0d..b03cf172459a4 100644 --- a/lib/tbot/config/config_test.go +++ b/lib/tbot/config/config_test.go @@ -309,6 +309,14 @@ func TestBotConfig_YAML(t *testing.T) { Roles: []string{"access"}, AppName: "my-app", }, + &WorkloadIdentityX509Service{ + Destination: &DestinationDirectory{ + Path: "/an/output/path", + }, + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + }, }, }, }, diff --git a/lib/tbot/config/service_spiffe_workload_api.go b/lib/tbot/config/service_spiffe_workload_api.go index 0a080c669a7a1..799da41ebb9ef 100644 --- a/lib/tbot/config/service_spiffe_workload_api.go +++ b/lib/tbot/config/service_spiffe_workload_api.go @@ -25,7 +25,7 @@ import ( "github.com/gravitational/trace" "gopkg.in/yaml.v3" - "github.com/gravitational/teleport/lib/tbot/spiffe/workloadattest" + "github.com/gravitational/teleport/lib/tbot/workloadidentity/workloadattest" ) const SPIFFEWorkloadAPIServiceType = "spiffe-workload-api" diff --git a/lib/tbot/config/service_spiffe_workload_api_test.go b/lib/tbot/config/service_spiffe_workload_api_test.go index 5ebdb04db13c0..46de57dd58d13 100644 --- a/lib/tbot/config/service_spiffe_workload_api_test.go +++ b/lib/tbot/config/service_spiffe_workload_api_test.go @@ -22,7 +22,7 @@ import ( "testing" "time" - "github.com/gravitational/teleport/lib/tbot/spiffe/workloadattest" + "github.com/gravitational/teleport/lib/tbot/workloadidentity/workloadattest" ) func ptr[T any](v T) *T { diff --git a/lib/tbot/config/service_workload_identity_x509.go b/lib/tbot/config/service_workload_identity_x509.go new file mode 100644 index 0000000000000..adff17f991eee --- /dev/null +++ b/lib/tbot/config/service_workload_identity_x509.go @@ -0,0 +1,136 @@ +// 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" + + "github.com/gravitational/trace" + "gopkg.in/yaml.v3" + + "github.com/gravitational/teleport/lib/tbot/bot" +) + +const WorkloadIdentityX509OutputType = "workload-identity-x509" + +var ( + _ ServiceConfig = &WorkloadIdentityX509Service{} + _ Initable = &WorkloadIdentityX509Service{} +) + +// WorkloadIdentitySelector allows the user to select which WorkloadIdentity +// resource should be used. +// +// Only one of Name or Labels can be set. +type WorkloadIdentitySelector struct { + // Name is the name of a specific WorkloadIdentity resource. + Name string `yaml:"name"` + // Labels is a set of labels that the WorkloadIdentity resource must have. + Labels map[string][]string `yaml:"labels,omitempty"` +} + +// CheckAndSetDefaults checks the WorkloadIdentitySelector values and sets any +// defaults. +func (s *WorkloadIdentitySelector) CheckAndSetDefaults() error { + switch { + case s.Name == "" && len(s.Labels) == 0: + return trace.BadParameter("one of ['name', 'labels'] must be set") + case s.Name != "" && len(s.Labels) > 0: + return trace.BadParameter("at most one of ['name', 'labels'] can be set") + } + for k, v := range s.Labels { + if len(v) == 0 { + return trace.BadParameter("labels[%s]: must have at least one value", k) + } + } + return nil +} + +// WorkloadIdentityX509Service is the configuration for the WorkloadIdentityX509Service +// Emulates the output of https://github.com/spiffe/spiffe-helper +type WorkloadIdentityX509Service 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"` + // IncludeFederatedTrustBundles controls whether to include federated trust + // bundles in the output. + IncludeFederatedTrustBundles bool `yaml:"include_federated_trust_bundles,omitempty"` +} + +// Init initializes the destination. +func (o *WorkloadIdentityX509Service) Init(ctx context.Context) error { + return trace.Wrap(o.Destination.Init(ctx, []string{})) +} + +// GetDestination returns the destination. +func (o *WorkloadIdentityX509Service) GetDestination() bot.Destination { + return o.Destination +} + +// CheckAndSetDefaults checks the SPIFFESVIDOutput values and sets any defaults. +func (o *WorkloadIdentityX509Service) 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") + } + return nil +} + +// Describe returns the file descriptions for the WorkloadIdentityX509Service. +func (o *WorkloadIdentityX509Service) Describe() []FileDescription { + fds := []FileDescription{ + { + Name: SVIDPEMPath, + }, + { + Name: SVIDKeyPEMPath, + }, + { + Name: SVIDTrustBundlePEMPath, + }, + } + return fds +} + +func (o *WorkloadIdentityX509Service) Type() string { + return WorkloadIdentityX509OutputType +} + +// MarshalYAML marshals the WorkloadIdentityX509Service into YAML. +func (o *WorkloadIdentityX509Service) MarshalYAML() (interface{}, error) { + type raw WorkloadIdentityX509Service + return withTypeHeader((*raw)(o), WorkloadIdentityX509OutputType) +} + +// UnmarshalYAML unmarshals the WorkloadIdentityX509Service from YAML. +func (o *WorkloadIdentityX509Service) 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 WorkloadIdentityX509Service + if err := node.Decode((*raw)(o)); err != nil { + return trace.Wrap(err) + } + o.Destination = dest + return nil +} diff --git a/lib/tbot/config/service_workload_identity_x509_test.go b/lib/tbot/config/service_workload_identity_x509_test.go new file mode 100644 index 0000000000000..584aaacf2ca83 --- /dev/null +++ b/lib/tbot/config/service_workload_identity_x509_test.go @@ -0,0 +1,133 @@ +// 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" + + "github.com/gravitational/teleport/lib/tbot/botfs" +) + +func TestWorkloadIdentityX509Service_YAML(t *testing.T) { + t.Parallel() + + dest := &DestinationMemory{} + tests := []testYAMLCase[WorkloadIdentityX509Service]{ + { + name: "full", + in: WorkloadIdentityX509Service{ + Destination: dest, + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + IncludeFederatedTrustBundles: true, + }, + }, + { + name: "minimal", + in: WorkloadIdentityX509Service{ + Destination: dest, + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + }, + }, + } + testYAML(t, tests) +} + +func TestWorkloadIdentityX509Service_CheckAndSetDefaults(t *testing.T) { + t.Parallel() + + tests := []testCheckAndSetDefaultsCase[*WorkloadIdentityX509Service]{ + { + name: "valid", + in: func() *WorkloadIdentityX509Service { + return &WorkloadIdentityX509Service{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + } + }, + }, + { + name: "valid with labels", + in: func() *WorkloadIdentityX509Service { + return &WorkloadIdentityX509Service{ + Selector: WorkloadIdentitySelector{ + Labels: map[string][]string{ + "key": {"value"}, + }, + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + } + }, + }, + { + name: "missing selectors", + in: func() *WorkloadIdentityX509Service { + return &WorkloadIdentityX509Service{ + Selector: WorkloadIdentitySelector{}, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + } + }, + wantErr: "one of ['name', 'labels'] must be set", + }, + { + name: "too many selectors", + in: func() *WorkloadIdentityX509Service { + return &WorkloadIdentityX509Service{ + Selector: WorkloadIdentitySelector{ + Name: "my-workload-identity", + Labels: map[string][]string{ + "key": {"value"}, + }, + }, + Destination: &DestinationDirectory{ + Path: "/opt/machine-id", + ACLs: botfs.ACLOff, + Symlinks: botfs.SymlinksInsecure, + }, + } + }, + wantErr: "at most one of ['name', 'labels'] can be set", + }, + { + name: "missing destination", + in: func() *WorkloadIdentityX509Service { + return &WorkloadIdentityX509Service{ + Destination: nil, + } + }, + wantErr: "no destination configured for output", + }, + } + testCheckAndSetDefaults(t, tests) +} diff --git a/lib/tbot/config/testdata/TestBotConfig_YAML/standard_config.golden b/lib/tbot/config/testdata/TestBotConfig_YAML/standard_config.golden index 53a59aceacc8e..45af51d235ede 100644 --- a/lib/tbot/config/testdata/TestBotConfig_YAML/standard_config.golden +++ b/lib/tbot/config/testdata/TestBotConfig_YAML/standard_config.golden @@ -55,6 +55,12 @@ services: roles: - access app_name: my-app + - type: workload-identity-x509 + selector: + name: my-workload-identity + destination: + type: directory + path: /an/output/path debug: true auth_server: example.teleport.sh:443 certificate_ttl: 1m0s diff --git a/lib/tbot/config/testdata/TestWorkloadIdentityX509Service_YAML/full.golden b/lib/tbot/config/testdata/TestWorkloadIdentityX509Service_YAML/full.golden new file mode 100644 index 0000000000000..ecb401da9de18 --- /dev/null +++ b/lib/tbot/config/testdata/TestWorkloadIdentityX509Service_YAML/full.golden @@ -0,0 +1,6 @@ +type: workload-identity-x509 +selector: + name: my-workload-identity +destination: + type: memory +include_federated_trust_bundles: true diff --git a/lib/tbot/config/testdata/TestWorkloadIdentityX509Service_YAML/minimal.golden b/lib/tbot/config/testdata/TestWorkloadIdentityX509Service_YAML/minimal.golden new file mode 100644 index 0000000000000..41317fa36cd86 --- /dev/null +++ b/lib/tbot/config/testdata/TestWorkloadIdentityX509Service_YAML/minimal.golden @@ -0,0 +1,5 @@ +type: workload-identity-x509 +selector: + name: my-workload-identity +destination: + type: memory diff --git a/lib/tbot/service_spiffe_svid_output.go b/lib/tbot/service_spiffe_svid_output.go index 783ce148d6834..558752160a1a6 100644 --- a/lib/tbot/service_spiffe_svid_output.go +++ b/lib/tbot/service_spiffe_svid_output.go @@ -38,7 +38,7 @@ import ( "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/tbot/config" "github.com/gravitational/teleport/lib/tbot/identity" - "github.com/gravitational/teleport/lib/tbot/spiffe" + "github.com/gravitational/teleport/lib/tbot/workloadidentity" ) const ( @@ -60,7 +60,7 @@ type SPIFFESVIDOutputService struct { resolver reversetunnelclient.Resolver // trustBundleCache is the cache of trust bundles. It only needs to be // provided when running in daemon mode. - trustBundleCache *spiffe.TrustBundleCache + trustBundleCache *workloadidentity.TrustBundleCache } func (s *SPIFFESVIDOutputService) String() string { @@ -72,7 +72,7 @@ func (s *SPIFFESVIDOutputService) OneShot(ctx context.Context) error { if err != nil { return trace.Wrap(err, "requesting SVID") } - bundleSet, err := spiffe.FetchInitialBundleSet( + bundleSet, err := workloadidentity.FetchInitialBundleSet( ctx, s.log, s.botAuthClient.SPIFFEFederationServiceClient(), @@ -223,7 +223,7 @@ func (s *SPIFFESVIDOutputService) requestSVID( func (s *SPIFFESVIDOutputService) render( ctx context.Context, - bundleSet *spiffe.BundleSet, + bundleSet *workloadidentity.BundleSet, res *machineidv1pb.SignX509SVIDsResponse, privateKey *rsa.PrivateKey, jwtSVIDs map[string]string, diff --git a/lib/tbot/service_spiffe_workload_api.go b/lib/tbot/service_spiffe_workload_api.go index 752994d959fbd..748e2e3cbd13f 100644 --- a/lib/tbot/service_spiffe_workload_api.go +++ b/lib/tbot/service_spiffe_workload_api.go @@ -57,8 +57,8 @@ import ( "github.com/gravitational/teleport/lib/observability/metrics" "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/tbot/config" - "github.com/gravitational/teleport/lib/tbot/spiffe" - "github.com/gravitational/teleport/lib/tbot/spiffe/workloadattest" + "github.com/gravitational/teleport/lib/tbot/workloadidentity" + "github.com/gravitational/teleport/lib/tbot/workloadidentity/workloadattest" "github.com/gravitational/teleport/lib/uds" ) @@ -79,7 +79,7 @@ type SPIFFEWorkloadAPIService struct { cfg *config.SPIFFEWorkloadAPIService log *slog.Logger resolver reversetunnelclient.Resolver - trustBundleCache *spiffe.TrustBundleCache + trustBundleCache *workloadidentity.TrustBundleCache // client holds the impersonated client for the service client *authclient.Client @@ -320,7 +320,7 @@ func (s *SPIFFEWorkloadAPIService) fetchX509SVIDs( return nil, trace.Wrap(err) } - marshaledBundle := spiffe.MarshalX509Bundle(localBundle.X509Bundle()) + marshaledBundle := workloadidentity.MarshalX509Bundle(localBundle.X509Bundle()) // Convert responses from the Teleport API to the SPIFFE Workload API // format. diff --git a/lib/tbot/service_spiffe_workload_api_sds.go b/lib/tbot/service_spiffe_workload_api_sds.go index cfb035b69d1ff..b67632e6a0cd9 100644 --- a/lib/tbot/service_spiffe_workload_api_sds.go +++ b/lib/tbot/service_spiffe_workload_api_sds.go @@ -39,8 +39,8 @@ import ( "google.golang.org/protobuf/types/known/anypb" "github.com/gravitational/teleport/lib/tbot/config" - "github.com/gravitational/teleport/lib/tbot/spiffe" - "github.com/gravitational/teleport/lib/tbot/spiffe/workloadattest" + "github.com/gravitational/teleport/lib/tbot/workloadidentity" + "github.com/gravitational/teleport/lib/tbot/workloadidentity/workloadattest" "github.com/gravitational/teleport/lib/utils" ) @@ -60,7 +60,7 @@ const ( ) type bundleSetGetter interface { - GetBundleSet(ctx context.Context) (*spiffe.BundleSet, error) + GetBundleSet(ctx context.Context) (*workloadidentity.BundleSet, error) } // spiffeSDSHandler implements an Envoy SDS API. @@ -369,7 +369,7 @@ func elementsMatch(a, b []string) bool { } func (s *spiffeSDSHandler) generateResponse( - bundleSet *spiffe.BundleSet, + bundleSet *workloadidentity.BundleSet, svids []*workloadpb.X509SVID, req *discoveryv3pb.DiscoveryRequest, ) (*discoveryv3pb.DiscoveryResponse, error) { diff --git a/lib/tbot/service_spiffe_workload_api_sds_test.go b/lib/tbot/service_spiffe_workload_api_sds_test.go index f4e850465306f..2db10bc7742ef 100644 --- a/lib/tbot/service_spiffe_workload_api_sds_test.go +++ b/lib/tbot/service_spiffe_workload_api_sds_test.go @@ -50,18 +50,18 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/fixtures" "github.com/gravitational/teleport/lib/tbot/config" - "github.com/gravitational/teleport/lib/tbot/spiffe" - "github.com/gravitational/teleport/lib/tbot/spiffe/workloadattest" + "github.com/gravitational/teleport/lib/tbot/workloadidentity" + "github.com/gravitational/teleport/lib/tbot/workloadidentity/workloadattest" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/utils/testutils/golden" "github.com/gravitational/teleport/tool/teleport/testenv" ) type mockTrustBundleCache struct { - currentBundle *spiffe.BundleSet + currentBundle *workloadidentity.BundleSet } -func (m *mockTrustBundleCache) GetBundleSet(ctx context.Context) (*spiffe.BundleSet, error) { +func (m *mockTrustBundleCache) GetBundleSet(ctx context.Context) (*workloadidentity.BundleSet, error) { return m.currentBundle, nil } @@ -98,7 +98,7 @@ func TestSDS_FetchSecrets(t *testing.T) { federatedBundle.AddX509Authority(ca) mockBundleCache := &mockTrustBundleCache{ - currentBundle: &spiffe.BundleSet{ + currentBundle: &workloadidentity.BundleSet{ Local: bundle, Federated: map[string]*spiffebundle.Bundle{ "federated.example.com": federatedBundle, @@ -118,13 +118,13 @@ func TestSDS_FetchSecrets(t *testing.T) { SpiffeId: "spiffe://example.com/default", X509Svid: []byte("CERT-spiffe://example.com/default"), X509SvidKey: []byte("KEY-spiffe://example.com/default"), - Bundle: spiffe.MarshalX509Bundle(localBundle.X509Bundle()), + Bundle: workloadidentity.MarshalX509Bundle(localBundle.X509Bundle()), }, { SpiffeId: "spiffe://example.com/second", X509Svid: []byte("CERT-spiffe://example.com/second"), X509SvidKey: []byte("KEY-spiffe://example.com/second"), - Bundle: spiffe.MarshalX509Bundle(localBundle.X509Bundle()), + Bundle: workloadidentity.MarshalX509Bundle(localBundle.X509Bundle()), }, }, nil } diff --git a/lib/tbot/service_spiffe_workload_api_test.go b/lib/tbot/service_spiffe_workload_api_test.go index f0117cd181b14..3c4c10927b994 100644 --- a/lib/tbot/service_spiffe_workload_api_test.go +++ b/lib/tbot/service_spiffe_workload_api_test.go @@ -36,7 +36,7 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/tbot/config" - "github.com/gravitational/teleport/lib/tbot/spiffe/workloadattest" + "github.com/gravitational/teleport/lib/tbot/workloadidentity/workloadattest" "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/tool/teleport/testenv" ) diff --git a/lib/tbot/service_workload_identity_x509.go b/lib/tbot/service_workload_identity_x509.go new file mode 100644 index 0000000000000..0ca146dc0b3d4 --- /dev/null +++ b/lib/tbot/service_workload_identity_x509.go @@ -0,0 +1,305 @@ +// 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" + "crypto/x509" + "encoding/pem" + "fmt" + "log/slog" + "math" + "time" + + "github.com/gravitational/trace" + + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/api/utils/retryutils" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/reversetunnelclient" + "github.com/gravitational/teleport/lib/tbot/config" + "github.com/gravitational/teleport/lib/tbot/identity" + "github.com/gravitational/teleport/lib/tbot/workloadidentity" +) + +// WorkloadIdentityX509Service is a service that retrieves X.509 certificates +// for WorkloadIdentity resources. +type WorkloadIdentityX509Service struct { + botAuthClient *authclient.Client + botCfg *config.BotConfig + cfg *config.WorkloadIdentityX509Service + getBotIdentity getBotIdentityFn + log *slog.Logger + resolver reversetunnelclient.Resolver + // trustBundleCache is the cache of trust bundles. It only needs to be + // provided when running in daemon mode. + trustBundleCache *workloadidentity.TrustBundleCache +} + +// String returns a human-readable description of the service. +func (s *WorkloadIdentityX509Service) String() string { + return fmt.Sprintf("workload-identity-x509 (%s)", s.cfg.Destination.String()) +} + +// OneShot runs the service once, generating the output and writing it to the +// destination, before exiting. +func (s *WorkloadIdentityX509Service) OneShot(ctx context.Context) error { + res, privateKey, err := s.requestSVID(ctx) + if err != nil { + return trace.Wrap(err, "requesting SVID") + } + bundleSet, err := workloadidentity.FetchInitialBundleSet( + ctx, + s.log, + s.botAuthClient.SPIFFEFederationServiceClient(), + s.botAuthClient.TrustClient(), + s.cfg.IncludeFederatedTrustBundles, + s.getBotIdentity().ClusterName, + ) + if err != nil { + return trace.Wrap(err, "fetching trust bundle set") + + } + return s.render(ctx, bundleSet, res, privateKey) +} + +// Run runs the service in daemon mode, periodically generating the output and +// writing it to the destination. +func (s *WorkloadIdentityX509Service) Run(ctx context.Context) error { + bundleSet, err := s.trustBundleCache.GetBundleSet(ctx) + if err != nil { + return trace.Wrap(err, "getting trust bundle set") + } + + jitter := retryutils.DefaultJitter + var x509Cred *workloadidentityv1pb.Credential + var privateKey crypto.Signer + var failures int + firstRun := make(chan struct{}, 1) + firstRun <- struct{}{} + for { + var retryAfter <-chan time.Time + if failures > 0 { + backoffTime := time.Second * time.Duration(math.Pow(2, float64(failures-1))) + if backoffTime > time.Minute { + backoffTime = time.Minute + } + backoffTime = jitter(backoffTime) + s.log.WarnContext( + ctx, + "Last attempt to generate output failed, will retry", + "retry_after", backoffTime, + "failures", failures, + ) + retryAfter = time.After(time.Duration(failures) * time.Second) + } + select { + case <-ctx.Done(): + return nil + case <-retryAfter: + s.log.InfoContext(ctx, "Retrying") + case <-bundleSet.Stale(): + newBundleSet, err := s.trustBundleCache.GetBundleSet(ctx) + if err != nil { + return trace.Wrap(err, "getting trust bundle set") + } + s.log.InfoContext(ctx, "Trust bundle set has been updated") + if !newBundleSet.Local.Equal(bundleSet.Local) { + // If the local trust domain CA has changed, we need to reissue + // the SVID. + x509Cred = nil + privateKey = nil + } + bundleSet = newBundleSet + case <-time.After(s.botCfg.RenewalInterval): + s.log.InfoContext(ctx, "Renewal interval reached, renewing SVIDs") + x509Cred = nil + privateKey = nil + case <-firstRun: + } + + if x509Cred == nil || privateKey == nil { + var err error + x509Cred, privateKey, err = s.requestSVID(ctx) + if err != nil { + s.log.ErrorContext(ctx, "Failed to request SVID", "error", err) + failures++ + continue + } + } + if err := s.render(ctx, bundleSet, x509Cred, privateKey); err != nil { + s.log.ErrorContext(ctx, "Failed to render output", "error", err) + failures++ + continue + } + failures = 0 + } +} + +func (s *WorkloadIdentityX509Service) requestSVID( + ctx context.Context, +) ( + *workloadidentityv1pb.Credential, + crypto.Signer, + error, +) { + ctx, span := tracer.Start( + ctx, + "WorkloadIdentityX509Service/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, + s.botCfg.CertificateTTL, + 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, + s.botCfg.CertificateTTL, + 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 +} + +func (s *WorkloadIdentityX509Service) render( + ctx context.Context, + bundleSet *workloadidentity.BundleSet, + x509Cred *workloadidentityv1pb.Credential, + privateKey crypto.Signer, +) error { + ctx, span := tracer.Start( + ctx, + "WorkloadIdentityX509Service/render", + ) + defer span.End() + s.log.InfoContext(ctx, "Rendering output") + + // Check the ACLs. We can't fix them, but we can warn if they're + // misconfigured. We'll need to precompute a list of keys to check. + // Note: This may only log a warning, depending on configuration. + if err := s.cfg.Destination.Verify(identity.ListKeys(identity.DestinationKinds()...)); err != nil { + return trace.Wrap(err) + } + // Ensure this destination is also writable. This is a hard fail if + // ACLs are misconfigured, regardless of configuration. + if err := identity.VerifyWrite(ctx, s.cfg.Destination); err != nil { + return trace.Wrap(err, "verifying destination") + } + + privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return trace.Wrap(err) + } + + privPEM := pem.EncodeToMemory(&pem.Block{ + Type: pemPrivateKey, + Bytes: privBytes, + }) + + if err := s.cfg.Destination.Write(ctx, config.SVIDKeyPEMPath, privPEM); err != nil { + return trace.Wrap(err, "writing svid key") + } + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: pemCertificate, + Bytes: x509Cred.GetX509Svid().GetCert(), + }) + if err := s.cfg.Destination.Write(ctx, config.SVIDPEMPath, certPEM); err != nil { + return trace.Wrap(err, "writing svid certificate") + } + + trustBundleBytes, err := bundleSet.Local.X509Bundle().Marshal() + if err != nil { + return trace.Wrap(err, "marshaling local trust bundle") + } + + if s.cfg.IncludeFederatedTrustBundles { + for _, federatedBundle := range bundleSet.Federated { + federatedBundleBytes, err := federatedBundle.X509Bundle().Marshal() + if err != nil { + return trace.Wrap(err, "marshaling federated trust bundle (%s)", federatedBundle.TrustDomain().Name()) + } + trustBundleBytes = append(trustBundleBytes, federatedBundleBytes...) + } + } + + if err := s.cfg.Destination.Write( + ctx, config.SVIDTrustBundlePEMPath, trustBundleBytes, + ); err != nil { + return trace.Wrap(err, "writing svid trust bundle") + } + + s.log.InfoContext( + ctx, + "Successfully wrote X509 workload identity credential to destination", + "workload_identity", workloadidentity.WorkloadIdentityLogValue(x509Cred), + "destination", s.cfg.Destination.String(), + ) + return nil +} diff --git a/lib/tbot/service_workload_identity_x509_test.go b/lib/tbot/service_workload_identity_x509_test.go new file mode 100644 index 0000000000000..392670f5ce674 --- /dev/null +++ b/lib/tbot/service_workload_identity_x509_test.go @@ -0,0 +1,149 @@ +// 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" + "path" + "testing" + "time" + + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "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/auth/machineid/workloadidentityv1/experiment" + "github.com/gravitational/teleport/lib/tbot/config" + "github.com/gravitational/teleport/lib/utils" + "github.com/gravitational/teleport/tool/teleport/testenv" +) + +func TestBotWorkloadIdentityX509(t *testing.T) { + experimentStatus := experiment.Enabled() + defer experiment.SetEnabled(experimentStatus) + experiment.SetEnabled(true) + + ctx := context.Background() + log := utils.NewSlogLoggerForTests() + + process := testenv.MakeTestServer(t, defaultTestServerOpts(t, log)) + rootClient := testenv.MakeDefaultAuthClient(t, process) + + 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: "/valid/{{ user.bot_name }}", + }, + }, + } + workloadIdentity, err = rootClient.WorkloadIdentityResourceServiceClient(). + CreateWorkloadIdentity(ctx, &workloadidentityv1pb.CreateWorkloadIdentityRequest{ + WorkloadIdentity: workloadIdentity, + }) + require.NoError(t, err) + + t.Run("By Name", func(t *testing.T) { + tmpDir := t.TempDir() + onboarding, _ := makeBot(t, rootClient, "by-name", role.GetName()) + botConfig := defaultBotConfig(t, process, onboarding, config.ServiceConfigs{ + &config.WorkloadIdentityX509Service{ + Selector: config.WorkloadIdentitySelector{ + Name: workloadIdentity.GetMetadata().GetName(), + }, + Destination: &config.DestinationDirectory{ + Path: tmpDir, + }, + }, + }, 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) + defer cancel() + require.NoError(t, b.Run(ctx)) + + svid, err := x509svid.Load( + path.Join(tmpDir, config.SVIDPEMPath), + path.Join(tmpDir, config.SVIDKeyPEMPath), + ) + require.NoError(t, err) + require.Equal(t, "spiffe://root/valid/by-name", svid.ID.String()) + }) + t.Run("By Labels", func(t *testing.T) { + tmpDir := t.TempDir() + onboarding, _ := makeBot(t, rootClient, "by-labels", role.GetName()) + botConfig := defaultBotConfig(t, process, onboarding, config.ServiceConfigs{ + &config.WorkloadIdentityX509Service{ + Selector: config.WorkloadIdentitySelector{ + Labels: map[string][]string{ + "foo": {"bar"}, + }, + }, + Destination: &config.DestinationDirectory{ + Path: tmpDir, + }, + }, + }, 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) + defer cancel() + require.NoError(t, b.Run(ctx)) + + svid, err := x509svid.Load( + path.Join(tmpDir, config.SVIDPEMPath), + path.Join(tmpDir, config.SVIDKeyPEMPath), + ) + require.NoError(t, err) + require.Equal(t, "spiffe://root/valid/by-labels", svid.ID.String()) + }) +} diff --git a/lib/tbot/tbot.go b/lib/tbot/tbot.go index e289731a28c50..e702db20393c9 100644 --- a/lib/tbot/tbot.go +++ b/lib/tbot/tbot.go @@ -49,7 +49,7 @@ import ( "github.com/gravitational/teleport/lib/reversetunnelclient" "github.com/gravitational/teleport/lib/tbot/config" "github.com/gravitational/teleport/lib/tbot/identity" - "github.com/gravitational/teleport/lib/tbot/spiffe" + "github.com/gravitational/teleport/lib/tbot/workloadidentity" "github.com/gravitational/teleport/lib/utils" ) @@ -282,14 +282,14 @@ func (b *Bot) Run(ctx context.Context) (err error) { // We only want to create this service if it's needed by a dependent // service. - var trustBundleCache *spiffe.TrustBundleCache - setupTrustBundleCache := func() (*spiffe.TrustBundleCache, error) { + var trustBundleCache *workloadidentity.TrustBundleCache + setupTrustBundleCache := func() (*workloadidentity.TrustBundleCache, error) { if trustBundleCache != nil { return trustBundleCache, nil } var err error - trustBundleCache, err = spiffe.NewTrustBundleCache(spiffe.TrustBundleCacheConfig{ + trustBundleCache, err = workloadidentity.NewTrustBundleCache(workloadidentity.TrustBundleCacheConfig{ FederationClient: b.botIdentitySvc.GetClient().SPIFFEFederationServiceClient(), TrustClient: b.botIdentitySvc.GetClient().TrustClient(), EventsClient: b.botIdentitySvc.GetClient(), @@ -488,6 +488,25 @@ func (b *Bot) Run(ctx context.Context) (err error) { teleport.ComponentKey, teleport.Component(componentTBot, "svc", svc.String()), ) services = append(services, svc) + case *config.WorkloadIdentityX509Service: + svc := &WorkloadIdentityX509Service{ + botAuthClient: b.botIdentitySvc.GetClient(), + botCfg: b.cfg, + cfg: svcCfg, + getBotIdentity: b.botIdentitySvc.GetIdentity, + resolver: resolver, + } + svc.log = b.log.With( + teleport.ComponentKey, teleport.Component(componentTBot, "svc", svc.String()), + ) + if !b.cfg.Oneshot { + tbCache, err := setupTrustBundleCache() + if err != nil { + return trace.Wrap(err) + } + svc.trustBundleCache = tbCache + } + services = append(services, svc) default: return trace.BadParameter("unknown service type: %T", svcCfg) } diff --git a/lib/tbot/workloadidentity/issue.go b/lib/tbot/workloadidentity/issue.go new file mode 100644 index 0000000000000..921eda590e1f3 --- /dev/null +++ b/lib/tbot/workloadidentity/issue.go @@ -0,0 +1,149 @@ +// 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 workloadidentity + +import ( + "context" + "crypto" + "crypto/x509" + "log/slog" + "time" + + "github.com/gravitational/trace" + "google.golang.org/protobuf/types/known/durationpb" + + workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1" + "github.com/gravitational/teleport/lib/auth/authclient" + "github.com/gravitational/teleport/lib/cryptosuites" + "github.com/gravitational/teleport/lib/tbot/config" +) + +// WorkloadIdentityLogValue returns a slog.Value for a given +// *workloadidentityv1pb.Credential +func WorkloadIdentityLogValue(credential *workloadidentityv1pb.Credential) slog.Value { + return slog.GroupValue( + slog.String("name", credential.GetWorkloadIdentityName()), + slog.String("revision", credential.GetWorkloadIdentityRevision()), + slog.String("spiffe_id", credential.GetSpiffeId()), + slog.String("serial_number", credential.GetX509Svid().GetSerialNumber()), + ) +} + +// WorkloadIdentitiesLogValue returns []slog.Value for a slice of +// *workloadidentityv1.Credential +func WorkloadIdentitiesLogValue(credentials []*workloadidentityv1pb.Credential) []slog.Value { + values := make([]slog.Value, 0, len(credentials)) + for _, credential := range credentials { + values = append(values, WorkloadIdentityLogValue(credential)) + } + return values +} + +// IssueX509WorkloadIdentity uses a given client and selector to issue a single +// or multiple X509 workload identity credentials. +func IssueX509WorkloadIdentity( + ctx context.Context, + log *slog.Logger, + clt *authclient.Client, + workloadIdentity config.WorkloadIdentitySelector, + ttl time.Duration, + attest *workloadidentityv1pb.WorkloadAttrs, +) ([]*workloadidentityv1pb.Credential, crypto.Signer, error) { + ctx, span := tracer.Start( + ctx, + "issueX509WorkloadIdentity", + ) + defer span.End() + privateKey, err := cryptosuites.GenerateKey(ctx, + cryptosuites.GetCurrentSuiteFromAuthPreference(clt), + cryptosuites.BotSVID) + if err != nil { + return nil, nil, trace.Wrap(err) + } + pubBytes, err := x509.MarshalPKIXPublicKey(privateKey.Public()) + if err != nil { + return nil, nil, trace.Wrap(err) + } + + switch { + case workloadIdentity.Name != "": + log.DebugContext( + ctx, + "Requesting issuance of X509 workload identity credential using name of WorkloadIdentity resource", + "name", workloadIdentity.Name, + ) + // When using the "name" based selector, we either get a single WIC back, + // or an error. We don't need to worry about selecting the right one. + res, err := clt.WorkloadIdentityIssuanceClient().IssueWorkloadIdentity(ctx, + &workloadidentityv1pb.IssueWorkloadIdentityRequest{ + Name: workloadIdentity.Name, + Credential: &workloadidentityv1pb.IssueWorkloadIdentityRequest_X509SvidParams{ + X509SvidParams: &workloadidentityv1pb.X509SVIDParams{ + PublicKey: pubBytes, + }, + }, + RequestedTtl: durationpb.New(ttl), + WorkloadAttrs: attest, + }, + ) + if err != nil { + return nil, nil, trace.Wrap(err) + } + log.DebugContext( + ctx, + "Received X509 workload identity credential", + "credential", WorkloadIdentityLogValue(res.Credential), + ) + return []*workloadidentityv1pb.Credential{res.Credential}, privateKey, nil + case len(workloadIdentity.Labels) > 0: + labelSelectors := make([]*workloadidentityv1pb.LabelSelector, 0, len(workloadIdentity.Labels)) + for k, v := range workloadIdentity.Labels { + labelSelectors = append(labelSelectors, &workloadidentityv1pb.LabelSelector{ + Key: k, + Values: v, + }) + } + log.DebugContext( + ctx, + "Requesting issuance of X509 workload identity credentials using labels", + "labels", labelSelectors, + ) + res, err := clt.WorkloadIdentityIssuanceClient().IssueWorkloadIdentities(ctx, + &workloadidentityv1pb.IssueWorkloadIdentitiesRequest{ + LabelSelectors: labelSelectors, + Credential: &workloadidentityv1pb.IssueWorkloadIdentitiesRequest_X509SvidParams{ + X509SvidParams: &workloadidentityv1pb.X509SVIDParams{ + PublicKey: pubBytes, + }, + }, + RequestedTtl: durationpb.New(ttl), + WorkloadAttrs: attest, + }, + ) + if err != nil { + return nil, nil, trace.Wrap(err) + } + log.DebugContext( + ctx, + "Received X509 workload identity credentials", + "credentials", WorkloadIdentitiesLogValue(res.Credentials), + ) + return res.Credentials, privateKey, nil + default: + return nil, nil, trace.BadParameter("no valid selector configured") + } +} diff --git a/lib/tbot/spiffe/trust_bundle_cache.go b/lib/tbot/workloadidentity/trust_bundle_cache.go similarity index 99% rename from lib/tbot/spiffe/trust_bundle_cache.go rename to lib/tbot/workloadidentity/trust_bundle_cache.go index 7ccfdb31044a2..8ab76a04c693f 100644 --- a/lib/tbot/spiffe/trust_bundle_cache.go +++ b/lib/tbot/workloadidentity/trust_bundle_cache.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package spiffe +package workloadidentity import ( "context" diff --git a/lib/tbot/spiffe/trust_bundle_cache_test.go b/lib/tbot/workloadidentity/trust_bundle_cache_test.go similarity index 99% rename from lib/tbot/spiffe/trust_bundle_cache_test.go rename to lib/tbot/workloadidentity/trust_bundle_cache_test.go index f862f79f166f5..e8667a84088f7 100644 --- a/lib/tbot/spiffe/trust_bundle_cache_test.go +++ b/lib/tbot/workloadidentity/trust_bundle_cache_test.go @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package spiffe +package workloadidentity import ( "context" diff --git a/lib/tbot/spiffe/workloadattest/attest.go b/lib/tbot/workloadidentity/workloadattest/attest.go similarity index 100% rename from lib/tbot/spiffe/workloadattest/attest.go rename to lib/tbot/workloadidentity/workloadattest/attest.go diff --git a/lib/tbot/spiffe/workloadattest/kubernetes.go b/lib/tbot/workloadidentity/workloadattest/kubernetes.go similarity index 100% rename from lib/tbot/spiffe/workloadattest/kubernetes.go rename to lib/tbot/workloadidentity/workloadattest/kubernetes.go diff --git a/lib/tbot/spiffe/workloadattest/kubernetes_unix.go b/lib/tbot/workloadidentity/workloadattest/kubernetes_unix.go similarity index 100% rename from lib/tbot/spiffe/workloadattest/kubernetes_unix.go rename to lib/tbot/workloadidentity/workloadattest/kubernetes_unix.go diff --git a/lib/tbot/spiffe/workloadattest/kubernetes_unix_test.go b/lib/tbot/workloadidentity/workloadattest/kubernetes_unix_test.go similarity index 100% rename from lib/tbot/spiffe/workloadattest/kubernetes_unix_test.go rename to lib/tbot/workloadidentity/workloadattest/kubernetes_unix_test.go diff --git a/lib/tbot/spiffe/workloadattest/kubernetes_windows.go b/lib/tbot/workloadidentity/workloadattest/kubernetes_windows.go similarity index 100% rename from lib/tbot/spiffe/workloadattest/kubernetes_windows.go rename to lib/tbot/workloadidentity/workloadattest/kubernetes_windows.go diff --git a/lib/tbot/spiffe/workloadattest/testdata/mountfile/k8s-real-docker-desktop b/lib/tbot/workloadidentity/workloadattest/testdata/mountfile/k8s-real-docker-desktop similarity index 100% rename from lib/tbot/spiffe/workloadattest/testdata/mountfile/k8s-real-docker-desktop rename to lib/tbot/workloadidentity/workloadattest/testdata/mountfile/k8s-real-docker-desktop diff --git a/lib/tbot/spiffe/workloadattest/testdata/mountfile/k8s-real-gcp-v1.29.5-gke.1091002 b/lib/tbot/workloadidentity/workloadattest/testdata/mountfile/k8s-real-gcp-v1.29.5-gke.1091002 similarity index 100% rename from lib/tbot/spiffe/workloadattest/testdata/mountfile/k8s-real-gcp-v1.29.5-gke.1091002 rename to lib/tbot/workloadidentity/workloadattest/testdata/mountfile/k8s-real-gcp-v1.29.5-gke.1091002 diff --git a/lib/tbot/spiffe/workloadattest/testdata/mountfile/k8s-real-k3s-ubuntu-v1.28.6+k3s2 b/lib/tbot/workloadidentity/workloadattest/testdata/mountfile/k8s-real-k3s-ubuntu-v1.28.6+k3s2 similarity index 100% rename from lib/tbot/spiffe/workloadattest/testdata/mountfile/k8s-real-k3s-ubuntu-v1.28.6+k3s2 rename to lib/tbot/workloadidentity/workloadattest/testdata/mountfile/k8s-real-k3s-ubuntu-v1.28.6+k3s2 diff --git a/lib/tbot/spiffe/workloadattest/testdata/mountfile/k8s-real-orbstack b/lib/tbot/workloadidentity/workloadattest/testdata/mountfile/k8s-real-orbstack similarity index 100% rename from lib/tbot/spiffe/workloadattest/testdata/mountfile/k8s-real-orbstack rename to lib/tbot/workloadidentity/workloadattest/testdata/mountfile/k8s-real-orbstack diff --git a/lib/tbot/spiffe/workloadattest/unix.go b/lib/tbot/workloadidentity/workloadattest/unix.go similarity index 100% rename from lib/tbot/spiffe/workloadattest/unix.go rename to lib/tbot/workloadidentity/workloadattest/unix.go diff --git a/lib/tbot/spiffe/workloadattest/unix_test.go b/lib/tbot/workloadidentity/workloadattest/unix_test.go similarity index 100% rename from lib/tbot/spiffe/workloadattest/unix_test.go rename to lib/tbot/workloadidentity/workloadattest/unix_test.go