Skip to content

Commit

Permalink
Start work on svc implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
strideynet committed Jan 15, 2025
1 parent ca7a284 commit 2e13610
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 0 deletions.
250 changes: 250 additions & 0 deletions lib/tbot/service_workload_identity_jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// 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"
"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"
)

// WorkloadIdentityJWTService is a service that retrieves JWT workload identity
// credentials for WorkloadIdentity resources.
type WorkloadIdentityJWTService struct {
botAuthClient *authclient.Client
botCfg *config.BotConfig
cfg *config.WorkloadIdentityJWTService
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 *WorkloadIdentityJWTService) String() string {
return fmt.Sprintf("workload-identity-jwt (%s)", s.cfg.Destination.String())
}

// OneShot runs the service once, generating the output and writing it to the
// destination, before exiting.
func (s *WorkloadIdentityJWTService) OneShot(ctx context.Context) error {
res, err := s.requestJWTSVID(ctx)
if err != nil {
return trace.Wrap(err, "requesting JWT SVID")
}
return s.render(ctx, res)
}

// Run runs the service in daemon mode, periodically generating the output and
// writing it to the destination.
func (s *WorkloadIdentityJWTService) 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 cred *workloadidentityv1pb.Credential
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():
// We don't actually write this bundle out at the moment, but, we
// still track it so we know when to reissue the JWT SVID.
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.
cred = nil
}
bundleSet = newBundleSet
case <-time.After(s.botCfg.RenewalInterval):
s.log.InfoContext(ctx, "Renewal interval reached, renewing SVIDs")
cred = nil
case <-firstRun:
}

if cred == nil {
var err error
cred, err = s.requestJWTSVID(ctx)
if err != nil {
s.log.ErrorContext(ctx, "Failed to request JWT SVID", "error", err)
failures++
continue
}
}
if err := s.render(ctx, cred); err != nil {
s.log.ErrorContext(ctx, "Failed to render output", "error", err)
failures++
continue
}
failures = 0
}
}

func (s *WorkloadIdentityJWTService) requestJWTSVID(
ctx context.Context,
) (
*workloadidentityv1pb.Credential,
error,
) {
ctx, span := tracer.Start(
ctx,
"WorkloadIdentityJWTService/requestJWTSVID",
)
defer span.End()

roles, err := fetchDefaultRoles(ctx, s.botAuthClient, s.getBotIdentity())
if err != nil {
return nil, trace.Wrap(err, "fetching roles")
}

id, err := generateIdentity(
ctx,
s.botAuthClient,
s.getBotIdentity(),
roles,
s.botCfg.CertificateTTL,
nil,
)
if err != nil {
return 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, trace.Wrap(err)
}
defer impersonatedClient.Close()

credentials, err := workloadidentity.IssueJWTWorkloadIdentity(
ctx,
s.log,
impersonatedClient,
s.cfg.Selector,
s.cfg.Audiences,
s.botCfg.CertificateTTL,
nil,
)
if err != nil {
return nil, trace.Wrap(err, "generating JWT SVID")
}
var credential *workloadidentityv1pb.Credential
switch len(credentials) {
case 0:
return nil, trace.BadParameter("no JWT SVIDs returned")
case 1:
credential = credentials[0]
default:
// We could eventually implement some kind of hint selection mechanism
// to pick the "right" one.
received := make([]string, 0, len(credentials))
for _, cred := range credentials {
received = append(received,
fmt.Sprintf(
"%s:%s",
cred.WorkloadIdentityName,
cred.SpiffeId,
),
)
}
return nil, trace.BadParameter(
"multiple JWT SVIDs received: %v", received,
)
}

return credential, nil
}

func (s *WorkloadIdentityJWTService) render(
ctx context.Context,
cred *workloadidentityv1pb.Credential,
) error {
ctx, span := tracer.Start(
ctx,
"WorkloadIdentityJWTService/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")
}

if err := s.cfg.Destination.Write(
ctx, config.JWTSVIDPath, []byte(cred.GetJwtSvid().GetJwt()),
); err != nil {
return trace.Wrap(err, "writing jwt svid")
}

s.log.InfoContext(
ctx,
"Successfully wrote X509 workload identity credential to destination",
"workload_identity", workloadidentity.WorkloadIdentityLogValue(cred),
"destination", s.cfg.Destination.String(),
)
return nil
}
23 changes: 23 additions & 0 deletions lib/tbot/service_workload_identity_jwt_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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 "testing"

func TestBotWorkloadIdentityJWT(t *testing.T) {
t.Fatalf("not implemented")
}
19 changes: 19 additions & 0 deletions lib/tbot/tbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,25 @@ func (b *Bot) Run(ctx context.Context) (err error) {
svc.trustBundleCache = tbCache
}
services = append(services, svc)
case *config.WorkloadIdentityJWTService:
svc := &WorkloadIdentityJWTService{
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)
case *config.WorkloadIdentityAPIService:
clientCredential := &config.UnstableClientCredentialOutput{}
svcIdentity := &ClientCredentialOutputService{
Expand Down

0 comments on commit 2e13610

Please sign in to comment.