Skip to content

Commit

Permalink
Workload Identity: Add workload-identity-x509 service to tbot (#5…
Browse files Browse the repository at this point in the history
…0812)

* 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
strideynet authored Jan 15, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 00aa81b commit 5bd6c59
Showing 35 changed files with 1,157 additions and 28 deletions.
6 changes: 6 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
@@ -889,6 +889,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)
111 changes: 111 additions & 0 deletions lib/tbot/cli/start_workload_identity_x509.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package cli

import (
"fmt"
"log/slog"

"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"

"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/tbot/config"
)

// WorkloadIdentityX509Command implements `tbot start workload-identity-x509` and
// `tbot configure spiffe-svid`.
type WorkloadIdentityX509Command struct {
*sharedStartArgs
*sharedDestinationArgs
*genericMutatorHandler

IncludeFederatedTrustBundles bool
// 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
}

// NewWorkloadIdentityX509Command initializes the command and flags for the
// `workload-identity-x509` output and returns a struct that will contain the parse
// result.
func NewWorkloadIdentityX509Command(parentCmd *kingpin.CmdClause, action MutatorAction, mode CommandMode) *WorkloadIdentityX509Command {
// TODO(noah): Unhide this command when feature flag removed
cmd := parentCmd.Command("workload-identity-x509", fmt.Sprintf("%s tbot with a SPIFFE-compatible SVID output.", mode)).Hidden()

c := &WorkloadIdentityX509Command{}
c.sharedStartArgs = newSharedStartArgs(cmd)
c.sharedDestinationArgs = newSharedDestinationArgs(cmd)
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)

cmd.Flag(
"include-federated-trust-bundles",
"If set, include federated trust bundles in the output",
).BoolVar(&c.IncludeFederatedTrustBundles)
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)

return c
}

// ApplyConfig applies the parsed flags to the bot configuration.
func (c *WorkloadIdentityX509Command) 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.WorkloadIdentityX509Service{
Destination: dest,
IncludeFederatedTrustBundles: c.IncludeFederatedTrustBundles,
}

switch {
case c.NameSelector != "" && c.LabelSelector != "":
return trace.BadParameter("workload-identity-name and workload-identity-labels 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 --workload-identity-labels")
}
svc.Selector.Labels = map[string][]string{}
for k, v := range labels {
svc.Selector.Labels[k] = []string{v}
}
default:
return trace.BadParameter("workload-identity-name or workload-identity-labels must be specified")
}

cfg.Services = append(cfg.Services, svc)

return nil
}
87 changes: 87 additions & 0 deletions lib/tbot/cli/start_workload_identity_x509_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Teleport
// Copyright (C) 2025 Gravitational, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package cli

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/tbot/config"
)

func TestWorkloadIdentityX509Command(t *testing.T) {
testStartConfigureCommand(t, NewWorkloadIdentityX509Command, []startConfigureTestCase{
{
name: "success",
args: []string{
"start",
"workload-identity-x509",
"--destination=/bar",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--include-federated-trust-bundles",
"--label-selector=*=*,foo=bar",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
require.Len(t, cfg.Services, 1)

svc := cfg.Services[0]
wis, ok := svc.(*config.WorkloadIdentityX509Service)
require.True(t, ok)
require.True(t, wis.IncludeFederatedTrustBundles)

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)
},
},
{
name: "success name selector",
args: []string{
"start",
"workload-identity-x509",
"--destination=/bar",
"--token=foo",
"--join-method=github",
"--proxy-server=example.com:443",
"--include-federated-trust-bundles",
"--name-selector=jim",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
require.Len(t, cfg.Services, 1)

svc := cfg.Services[0]
wis, ok := svc.(*config.WorkloadIdentityX509Service)
require.True(t, ok)
require.True(t, wis.IncludeFederatedTrustBundles)

dir, ok := wis.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/bar", dir.Path)

require.Equal(t, "jim", wis.Selector.Name)
},
},
})
}
6 changes: 6 additions & 0 deletions lib/tbot/config/config.go
Original file line number Diff line number Diff line change
@@ -399,6 +399,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)
}
8 changes: 8 additions & 0 deletions lib/tbot/config/config_test.go
Original file line number Diff line number Diff line change
@@ -264,6 +264,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",
},
},
},
},
},
2 changes: 1 addition & 1 deletion lib/tbot/config/service_spiffe_workload_api.go
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion lib/tbot/config/service_spiffe_workload_api_test.go
Original file line number Diff line number Diff line change
@@ -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 {
136 changes: 136 additions & 0 deletions lib/tbot/config/service_workload_identity_x509.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
133 changes: 133 additions & 0 deletions lib/tbot/config/service_workload_identity_x509_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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)
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type: workload-identity-x509
selector:
name: my-workload-identity
destination:
type: memory
include_federated_trust_bundles: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: workload-identity-x509
selector:
name: my-workload-identity
destination:
type: memory
8 changes: 4 additions & 4 deletions lib/tbot/service_spiffe_svid_output.go
Original file line number Diff line number Diff line change
@@ -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 crypto.Signer,
jwtSVIDs map[string]string,
8 changes: 4 additions & 4 deletions lib/tbot/service_spiffe_workload_api.go
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 4 additions & 4 deletions lib/tbot/service_spiffe_workload_api_sds.go
Original file line number Diff line number Diff line change
@@ -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) {
14 changes: 7 additions & 7 deletions lib/tbot/service_spiffe_workload_api_sds_test.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion lib/tbot/service_spiffe_workload_api_test.go
Original file line number Diff line number Diff line change
@@ -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"
)
305 changes: 305 additions & 0 deletions lib/tbot/service_workload_identity_x509.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
149 changes: 149 additions & 0 deletions lib/tbot/service_workload_identity_x509_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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())
})
}
27 changes: 23 additions & 4 deletions lib/tbot/tbot.go
Original file line number Diff line number Diff line change
@@ -48,7 +48,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"
)

@@ -281,14 +281,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(),
@@ -486,6 +486,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)
}
149 changes: 149 additions & 0 deletions lib/tbot/workloadidentity/issue.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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")
}
}
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package spiffe
package workloadidentity

import (
"context"
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package spiffe
package workloadidentity

import (
"context"
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 3 additions & 0 deletions tool/tbot/main.go
Original file line number Diff line number Diff line change
@@ -140,6 +140,9 @@ func Run(args []string, stdout io.Writer) error {

cli.NewSPIFFESVIDCommand(startCmd, buildConfigAndStart(ctx, globalCfg), cli.CommandModeStart),
cli.NewSPIFFESVIDCommand(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout), cli.CommandModeConfigure),

cli.NewWorkloadIdentityX509Command(startCmd, buildConfigAndStart(ctx, globalCfg), cli.CommandModeStart),
cli.NewWorkloadIdentityX509Command(configureCmd, buildConfigAndConfigure(ctx, globalCfg, &configureOutPath, stdout), cli.CommandModeConfigure),
)

// Initialize legacy-style commands. These are simple enough to not really

0 comments on commit 5bd6c59

Please sign in to comment.