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
  • Loading branch information
strideynet authored Jan 15, 2025
1 parent 00aa81b commit 5bd6c59
Show file tree
Hide file tree
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
Expand Up @@ -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)
Expand Down
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
Expand Up @@ -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)
}
Expand Down
8 changes: 8 additions & 0 deletions lib/tbot/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion lib/tbot/config/service_spiffe_workload_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
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
Expand Up @@ -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 {
Expand Down
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
}
Loading

0 comments on commit 5bd6c59

Please sign in to comment.