Skip to content

Commit

Permalink
[v16] Workload Identity: workload-identity-jwt (#51027) (#51167)
Browse files Browse the repository at this point in the history
* Workload Identity: `workload-identity-jwt` (#51027)

* Add config.WorkloadIdentityJWTService

* Add tsests for WorkloadIdentityJWTService

* Add CLI setup for `workload-identity-jwt` svc

* Start work on svc implementation

* Add TestBotWorkloadIdentityJWT

* Correct log message

* Firm up TestBotWorkloadIdentityX509 test

* Fix param names in errors

* Fix jitter in backport
  • Loading branch information
strideynet authored Jan 17, 2025
1 parent d6a3dd7 commit 11134e8
Show file tree
Hide file tree
Showing 10 changed files with 730 additions and 0 deletions.
6 changes: 6 additions & 0 deletions lib/tbot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,12 @@ func (o *ServiceConfigs) UnmarshalYAML(node *yaml.Node) error {
return trace.Wrap(err)
}
out = append(out, v)
case WorkloadIdentityJWTOutputType:
v := &WorkloadIdentityJWTService{}
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
9 changes: 9 additions & 0 deletions lib/tbot/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,15 @@ func TestBotConfig_YAML(t *testing.T) {
Name: "my-workload-identity",
},
},
&WorkloadIdentityJWTService{
Destination: &DestinationDirectory{
Path: "/an/output/path",
},
Selector: WorkloadIdentitySelector{
Name: "my-workload-identity",
},
Audiences: []string{"audience1", "audience2"},
},
},
},
},
Expand Down
106 changes: 106 additions & 0 deletions lib/tbot/config/service_workload_identity_jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 WorkloadIdentityJWTOutputType = "workload-identity-jwt"

var (
_ ServiceConfig = &WorkloadIdentityJWTService{}
_ Initable = &WorkloadIdentityJWTService{}
)

// WorkloadIdentityJWTService is the configuration for the WorkloadIdentityJWTService
type WorkloadIdentityJWTService 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"`
// Audiences is the list of audiences that the JWT should be valid for.
Audiences []string
}

// Init initializes the destination.
func (o *WorkloadIdentityJWTService) Init(ctx context.Context) error {
return trace.Wrap(o.Destination.Init(ctx, []string{}))
}

// CheckAndSetDefaults checks the WorkloadIdentityJWTService values and sets any defaults.
func (o *WorkloadIdentityJWTService) 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")
}
if len(o.Audiences) == 0 {
return trace.BadParameter("audiences: must have at least one value")
}
return nil
}

// JWTSVIDPath is the name of the artifact that a JWT SVID will be written to.
const JWTSVIDPath = "jwt_svid"

// Describe returns the file descriptions for the WorkloadIdentityJWTService.
func (o *WorkloadIdentityJWTService) Describe() []FileDescription {
fds := []FileDescription{
{
Name: JWTSVIDPath,
},
}
return fds
}

func (o *WorkloadIdentityJWTService) Type() string {
return WorkloadIdentityJWTOutputType
}

// MarshalYAML marshals the WorkloadIdentityJWTService into YAML.
func (o *WorkloadIdentityJWTService) MarshalYAML() (interface{}, error) {
type raw WorkloadIdentityJWTService
return withTypeHeader((*raw)(o), WorkloadIdentityJWTOutputType)
}

// UnmarshalYAML unmarshals the WorkloadIdentityJWTService from YAML.
func (o *WorkloadIdentityJWTService) 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 WorkloadIdentityJWTService
if err := node.Decode((*raw)(o)); err != nil {
return trace.Wrap(err)
}
o.Destination = dest
return nil
}

// GetDestination returns the destination.
func (o *WorkloadIdentityJWTService) GetDestination() bot.Destination {
return o.Destination
}
148 changes: 148 additions & 0 deletions lib/tbot/config/service_workload_identity_jwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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 TestWorkloadIdentityJWTService_YAML(t *testing.T) {
t.Parallel()

dest := &DestinationMemory{}
tests := []testYAMLCase[WorkloadIdentityJWTService]{
{
name: "full",
in: WorkloadIdentityJWTService{
Destination: dest,
Selector: WorkloadIdentitySelector{
Name: "my-workload-identity",
},
Audiences: []string{"audience1", "audience2"},
},
},
}
testYAML(t, tests)
}

func TestWorkloadIdentityJWTService_CheckAndSetDefaults(t *testing.T) {
t.Parallel()

tests := []testCheckAndSetDefaultsCase[*WorkloadIdentityJWTService]{
{
name: "valid",
in: func() *WorkloadIdentityJWTService {
return &WorkloadIdentityJWTService{
Selector: WorkloadIdentitySelector{
Name: "my-workload-identity",
},
Destination: &DestinationDirectory{
Path: "/opt/machine-id",
ACLs: botfs.ACLOff,
Symlinks: botfs.SymlinksInsecure,
},
Audiences: []string{"audience1", "audience2"},
}
},
},
{
name: "valid with labels",
in: func() *WorkloadIdentityJWTService {
return &WorkloadIdentityJWTService{
Selector: WorkloadIdentitySelector{
Labels: map[string][]string{
"key": {"value"},
},
},
Destination: &DestinationDirectory{
Path: "/opt/machine-id",
ACLs: botfs.ACLOff,
Symlinks: botfs.SymlinksInsecure,
},
Audiences: []string{"audience1", "audience2"},
}
},
},
{
name: "missing audience",
in: func() *WorkloadIdentityJWTService {
return &WorkloadIdentityJWTService{
Selector: WorkloadIdentitySelector{
Name: "my-workload-identity",
},
Destination: &DestinationDirectory{
Path: "/opt/machine-id",
ACLs: botfs.ACLOff,
Symlinks: botfs.SymlinksInsecure,
},
}
},
wantErr: "audiences: must have at least one value",
},
{
name: "missing selectors",
in: func() *WorkloadIdentityJWTService {
return &WorkloadIdentityJWTService{
Selector: WorkloadIdentitySelector{},
Destination: &DestinationDirectory{
Path: "/opt/machine-id",
ACLs: botfs.ACLOff,
Symlinks: botfs.SymlinksInsecure,
},
Audiences: []string{"audience1", "audience2"},
}
},
wantErr: "one of ['name', 'labels'] must be set",
},
{
name: "too many selectors",
in: func() *WorkloadIdentityJWTService {
return &WorkloadIdentityJWTService{
Selector: WorkloadIdentitySelector{
Name: "my-workload-identity",
Labels: map[string][]string{
"key": {"value"},
},
},
Destination: &DestinationDirectory{
Path: "/opt/machine-id",
ACLs: botfs.ACLOff,
Symlinks: botfs.SymlinksInsecure,
},
Audiences: []string{"audience1", "audience2"},
}
},
wantErr: "at most one of ['name', 'labels'] can be set",
},
{
name: "missing destination",
in: func() *WorkloadIdentityJWTService {
return &WorkloadIdentityJWTService{
Destination: nil,
Selector: WorkloadIdentitySelector{
Name: "my-workload-identity",
},
Audiences: []string{"audience1", "audience2"},
}
},
wantErr: "no destination configured for output",
},
}
testCheckAndSetDefaults(t, tests)
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ services:
enabled: false
selector:
name: my-workload-identity
- type: workload-identity-jwt
selector:
name: my-workload-identity
destination:
type: directory
path: /an/output/path
audiences:
- audience1
- audience2
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,8 @@
type: workload-identity-jwt
selector:
name: my-workload-identity
destination:
type: memory
audiences:
- audience1
- audience2
Loading

0 comments on commit 11134e8

Please sign in to comment.