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 committed Jan 15, 2025
1 parent 61c5e8a commit bafdcb9
Show file tree
Hide file tree
Showing 32 changed files with 956 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 @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions lib/tbot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
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 @@ -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",
},
},
},
},
},
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
}
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
Expand Up @@ -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
Expand Down
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
Expand Up @@ -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 (
Expand All @@ -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 {
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit bafdcb9

Please sign in to comment.