mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Add support for delegation to tbot's relevant services (#64900)
* Add support for delegation to `tbot`'s relevant services * Explicitly prevent use of delegation with leaf clusters * Make `WithRoles` and `WithDelegation` mutually-exclusive * Make `delegation_session_id` and `roles` mutually-exclusive in tbot config * Use nil-tolerant proto getter functions * Move delegation cert generation into separate method * Stop delegation service from returning CA certificates Per @strideynet's request, to avoid replicating existing bad patterns around conflating the user and host CAs. * Fix tbot OpenSSH output in delegation sessions
This commit is contained in:
@@ -27,9 +27,11 @@ import (
|
||||
"github.com/gravitational/trace"
|
||||
"go.opentelemetry.io/otel"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
apiclient "github.com/gravitational/teleport/api/client"
|
||||
"github.com/gravitational/teleport/api/client/proto"
|
||||
delegationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/delegation/v1"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/utils/keys"
|
||||
"github.com/gravitational/teleport/lib/cryptosuites"
|
||||
@@ -99,6 +101,7 @@ type Generator struct {
|
||||
}
|
||||
|
||||
type generateOpts struct {
|
||||
delegationSessionID string
|
||||
roles []string
|
||||
ttl, renewalInterval time.Duration
|
||||
currentIdentity *Identity
|
||||
@@ -109,6 +112,16 @@ type generateOpts struct {
|
||||
// GenerateOption allows you to customize aspects of the generated identity.
|
||||
type GenerateOption func(*generateOpts)
|
||||
|
||||
// WithDelegation uses the given delegation session ID to generate certificates
|
||||
// associated with a *human* user and delegation session.
|
||||
//
|
||||
// Note: this option is mutually-exclusive with WithRoles.
|
||||
func WithDelegation(sessionID string) GenerateOption {
|
||||
return func(opts *generateOpts) {
|
||||
opts.delegationSessionID = sessionID
|
||||
}
|
||||
}
|
||||
|
||||
// WithRoles sets the roles the generated identity should include.
|
||||
//
|
||||
// Generally, if the user did not specify any roles, it's best to leave this
|
||||
@@ -227,6 +240,10 @@ func (g *Generator) Generate(ctx context.Context, opts ...GenerateOption) (*Iden
|
||||
|
||||
log := cmp.Or(o.logger, g.logger)
|
||||
|
||||
if len(o.roles) != 0 && o.delegationSessionID != "" {
|
||||
return nil, trace.BadParameter("delegation sessions and explicit roles are mutually-exclusive")
|
||||
}
|
||||
|
||||
if len(o.roles) == 0 {
|
||||
if o.currentIdentity != nil {
|
||||
// If the caller provided an impersonated identity, take its roles.
|
||||
@@ -289,11 +306,19 @@ func (g *Generator) Generate(ctx context.Context, opts ...GenerateOption) (*Iden
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
// First, ask the auth server to generate a new set of certs with a new
|
||||
// expiration date.
|
||||
certs, err := g.client.GenerateUserCerts(ctx, req)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
var certs *proto.Certs
|
||||
if o.delegationSessionID == "" {
|
||||
// Traditional bot certificates.
|
||||
certs, err = g.client.GenerateUserCerts(ctx, req)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
} else {
|
||||
// Delegation certificates, tied to a human user identity.
|
||||
certs, err = g.generateDelegationCertificates(ctx, req, o)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
// The root CA included with the returned user certs will only contain the
|
||||
@@ -357,6 +382,63 @@ func (g *Generator) botDefaultRoles(ctx context.Context) ([]string, error) {
|
||||
return conditions.Roles, nil
|
||||
}
|
||||
|
||||
func (g *Generator) generateDelegationCertificates(ctx context.Context, req proto.UserCertsRequest, o *generateOpts) (*proto.Certs, error) {
|
||||
certReq := &delegationv1.GenerateCertsRequest{
|
||||
DelegationSessionId: o.delegationSessionID,
|
||||
SshPublicKey: req.SSHPublicKey,
|
||||
TlsPublicKey: req.TLSPublicKey,
|
||||
Ttl: durationpb.New(o.ttl),
|
||||
}
|
||||
if req.GetRouteToCluster() != o.currentIdentity.ClusterName {
|
||||
return nil, trace.BadParameter("delegation sessions cannot be used with leaf clusters")
|
||||
}
|
||||
switch {
|
||||
case req.GetRouteToApp().Name != "":
|
||||
route := req.GetRouteToApp()
|
||||
certReq.Routing = &delegationv1.GenerateCertsRequest_RouteToApp{
|
||||
RouteToApp: &delegationv1.RouteToApp{
|
||||
Name: route.GetName(),
|
||||
PublicAddr: route.GetPublicAddr(),
|
||||
ClusterName: route.GetClusterName(),
|
||||
Uri: route.GetURI(),
|
||||
TargetPort: route.GetTargetPort(),
|
||||
AwsRoleArn: route.GetAWSRoleARN(),
|
||||
AzureIdentity: route.GetAzureIdentity(),
|
||||
GcpServiceAccount: route.GetGCPServiceAccount(),
|
||||
},
|
||||
}
|
||||
case req.GetRouteToDatabase().ServiceName != "":
|
||||
route := req.GetRouteToDatabase()
|
||||
certReq.Routing = &delegationv1.GenerateCertsRequest_RouteToDatabase{
|
||||
RouteToDatabase: &delegationv1.RouteToDatabase{
|
||||
ServiceName: route.GetServiceName(),
|
||||
Protocol: route.GetProtocol(),
|
||||
Username: route.GetUsername(),
|
||||
Database: route.GetDatabase(),
|
||||
Roles: route.GetRoles(),
|
||||
},
|
||||
}
|
||||
case req.GetKubernetesCluster() != "":
|
||||
certReq.Routing = &delegationv1.GenerateCertsRequest_RouteToKubernetes{
|
||||
RouteToKubernetes: &delegationv1.RouteToKubernetes{
|
||||
ClusterName: req.GetKubernetesCluster(),
|
||||
},
|
||||
}
|
||||
}
|
||||
certsRsp, err := g.client.DelegationSessionServiceClient().
|
||||
GenerateCerts(ctx, certReq)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
return &proto.Certs{
|
||||
SSH: certsRsp.GetSsh(),
|
||||
TLS: certsRsp.GetTls(),
|
||||
SSHCACerts: o.currentIdentity.SSHCACertBytes,
|
||||
TLSCACerts: o.currentIdentity.TLSCACertsBytes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// warnOnEarlyExpiration logs a warning if the given identity is likely to
|
||||
// expire problematically early. This can happen if either the configured TTL is
|
||||
// less than the renewal interval, or if the server returns certs valid for a
|
||||
|
||||
@@ -285,11 +285,28 @@ func MakeNameOrDiscoveredNamePredicate(name string) string {
|
||||
}
|
||||
|
||||
func GetClusterNames(
|
||||
ctx context.Context, client *apiclient.Client, connectedClusterName string,
|
||||
ctx context.Context,
|
||||
client *apiclient.Client,
|
||||
connectedClusterName string,
|
||||
inDelegationSession bool,
|
||||
) ([]string, error) {
|
||||
allClusterNames := []string{connectedClusterName}
|
||||
|
||||
leafClusters, err := client.GetRemoteClusters(ctx)
|
||||
if trace.IsAccessDenied(err) && inDelegationSession {
|
||||
// Currently, delegations sessions scoped to a specific node do not have
|
||||
// permission to list remote clusters, and so the overall resource/verb
|
||||
// check will fail.
|
||||
//
|
||||
// When we add support for delegating access to resources in remote
|
||||
// clusters, we'll probably grant the session read access to the cluster
|
||||
// automatically.
|
||||
//
|
||||
// In the mean time, we'll treat the GetRemoteClusters call as optional
|
||||
// but only in a delegation session (to avoid a backward-incompatible
|
||||
// change in behavior).
|
||||
return allClusterNames, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,11 @@ type OutputConfig struct {
|
||||
// be configured with specific paths to use, but exists for compatibility.
|
||||
SpecificTLSExtensions bool `yaml:"specific_tls_naming"`
|
||||
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
|
||||
// CredentialLifetime contains configuration for how long credentials will
|
||||
// last and the frequency at which they'll be renewed.
|
||||
CredentialLifetime bot.CredentialLifetime `yaml:",inline"`
|
||||
@@ -67,6 +72,9 @@ func (o *OutputConfig) CheckAndSetDefaults() error {
|
||||
if o.AppName == "" {
|
||||
return trace.BadParameter("app_name must not be empty")
|
||||
}
|
||||
if o.DelegationSessionID != "" && len(o.Roles) > 0 {
|
||||
return trace.BadParameter("delegation_session_id: is mutually-exclusive with roles")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,9 +32,10 @@ func TestApplicationOutput_YAML(t *testing.T) {
|
||||
{
|
||||
name: "full",
|
||||
in: OutputConfig{
|
||||
Destination: dest,
|
||||
Roles: []string{"access"},
|
||||
AppName: "my-app",
|
||||
Destination: dest,
|
||||
Roles: []string{"access"},
|
||||
AppName: "my-app",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
CredentialLifetime: bot.CredentialLifetime{
|
||||
TTL: 1 * time.Minute,
|
||||
RenewalInterval: 30 * time.Second,
|
||||
@@ -83,6 +84,18 @@ func TestApplicationOutput_CheckAndSetDefaults(t *testing.T) {
|
||||
},
|
||||
wantErr: "app_name must not be empty",
|
||||
},
|
||||
{
|
||||
name: "delegation session id conflicts with roles",
|
||||
in: func() *OutputConfig {
|
||||
return &OutputConfig{
|
||||
Destination: destination.NewMemory(),
|
||||
Roles: []string{"access"},
|
||||
AppName: "app",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
}
|
||||
},
|
||||
wantErr: "delegation_session_id: is mutually-exclusive with roles",
|
||||
},
|
||||
}
|
||||
testCheckAndSetDefaults(t, tests)
|
||||
}
|
||||
|
||||
@@ -122,10 +122,14 @@ func (s *OutputService) generate(ctx context.Context) error {
|
||||
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
identityOpts := []identity.GenerateOption{
|
||||
identity.WithRoles(s.cfg.Roles),
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
}
|
||||
if s.cfg.DelegationSessionID == "" {
|
||||
identityOpts = append(identityOpts, identity.WithRoles(s.cfg.Roles))
|
||||
} else {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
id, err := s.identityGenerator.GenerateFacade(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
||||
@@ -46,6 +46,10 @@ type ProxyServiceConfig struct {
|
||||
// last and the frequency at which they'll be renewed. For the application
|
||||
// proxy, this is primarily an internal detail.
|
||||
CredentialLifetime bot.CredentialLifetime `yaml:"credential_lifetime,omitempty"`
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
// Listener overrides "listen" and directly provides an opened listener to
|
||||
// use. Primarily used for testing.
|
||||
Listener net.Listener `yaml:"-"`
|
||||
|
||||
@@ -32,8 +32,9 @@ func TestProxyServiceConfig_YAML(t *testing.T) {
|
||||
{
|
||||
name: "full",
|
||||
in: ProxyServiceConfig{
|
||||
Name: "foo",
|
||||
Listen: "tcp://0.0.0.0:3621",
|
||||
Name: "foo",
|
||||
Listen: "tcp://0.0.0.0:3621",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
CredentialLifetime: bot.CredentialLifetime{
|
||||
TTL: 1 * time.Minute,
|
||||
RenewalInterval: 30 * time.Second,
|
||||
|
||||
@@ -227,6 +227,9 @@ func (s *ProxyService) issueCert(
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
}
|
||||
if s.cfg.DelegationSessionID != "" {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
impersonatedIdentity, err := s.identityGenerator.GenerateFacade(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
|
||||
@@ -5,5 +5,6 @@ roles:
|
||||
- access
|
||||
app_name: my-app
|
||||
specific_tls_naming: false
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
|
||||
+1
@@ -3,3 +3,4 @@ listen: tcp://0.0.0.0:3621
|
||||
app_name: my-app
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
|
||||
+1
@@ -4,3 +4,4 @@ listen: tcp://0.0.0.0:3621
|
||||
credential_lifetime:
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
|
||||
@@ -52,6 +52,11 @@ type TunnelConfig struct {
|
||||
// last and the frequency at which they'll be renewed.
|
||||
CredentialLifetime bot.CredentialLifetime `yaml:",inline"`
|
||||
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
|
||||
// Listener overrides "listen" and directly provides an opened listener to
|
||||
// use.
|
||||
Listener net.Listener `yaml:"-"`
|
||||
@@ -106,6 +111,9 @@ func (s *TunnelConfig) CheckAndSetDefaults() error {
|
||||
if s.clock == nil {
|
||||
s.clock = clockwork.NewRealClock()
|
||||
}
|
||||
if s.DelegationSessionID != "" && len(s.Roles) > 0 {
|
||||
return trace.BadParameter("delegation_session_id: is mutually-exclusive with roles")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -34,8 +34,9 @@ func TestApplicationTunnelService_YAML(t *testing.T) {
|
||||
{
|
||||
name: "full",
|
||||
in: TunnelConfig{
|
||||
Listen: "tcp://0.0.0.0:3621",
|
||||
AppName: "my-app",
|
||||
Listen: "tcp://0.0.0.0:3621",
|
||||
AppName: "my-app",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
CredentialLifetime: bot.CredentialLifetime{
|
||||
TTL: 1 * time.Minute,
|
||||
RenewalInterval: 30 * time.Second,
|
||||
@@ -101,6 +102,18 @@ func TestApplicationTunnelService_CheckAndSetDefaults(t *testing.T) {
|
||||
},
|
||||
wantErr: "app_name: should not be empty",
|
||||
},
|
||||
{
|
||||
name: "delegation session id conflicts with roles",
|
||||
in: func() *TunnelConfig {
|
||||
return &TunnelConfig{
|
||||
Listen: "tcp://0.0.0.0:3621",
|
||||
Roles: []string{"role1", "role2"},
|
||||
AppName: "my-app",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
}
|
||||
},
|
||||
wantErr: "delegation_session_id: is mutually-exclusive with roles",
|
||||
},
|
||||
}
|
||||
testCheckAndSetDefaults(t, tests)
|
||||
}
|
||||
|
||||
@@ -267,10 +267,14 @@ func (s *TunnelService) issueCert(
|
||||
// routeToApp once.
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
identityOpts := []identity.GenerateOption{
|
||||
identity.WithRoles(s.cfg.Roles),
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
}
|
||||
if s.cfg.DelegationSessionID == "" {
|
||||
identityOpts = append(identityOpts, identity.WithRoles(s.cfg.Roles))
|
||||
} else {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
impersonatedIdentity, err := s.identityGenerator.GenerateFacade(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return nil, nil, trace.Wrap(err)
|
||||
|
||||
@@ -94,6 +94,11 @@ type OutputConfig struct {
|
||||
// Username is the database username to request access as.
|
||||
Username string `yaml:"username,omitempty"`
|
||||
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
|
||||
// CredentialLifetime contains configuration for how long credentials will
|
||||
// last and the frequency at which they'll be renewed.
|
||||
CredentialLifetime bot.CredentialLifetime `yaml:",inline"`
|
||||
@@ -132,6 +137,9 @@ func (o *OutputConfig) CheckAndSetDefaults() error {
|
||||
if !slices.Contains(databaseFormats, o.Format) {
|
||||
return trace.BadParameter("unrecognized format (%s)", o.Format)
|
||||
}
|
||||
if o.DelegationSessionID != "" && len(o.Roles) > 0 {
|
||||
return trace.BadParameter("delegation_session_id: is mutually-exclusive with roles")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,12 +32,13 @@ func TestDatabaseOutput_YAML(t *testing.T) {
|
||||
{
|
||||
name: "full",
|
||||
in: OutputConfig{
|
||||
Destination: dest,
|
||||
Roles: []string{"access"},
|
||||
Format: TLSDatabaseFormat,
|
||||
Service: "my-database-service",
|
||||
Database: "my-database",
|
||||
Username: "my-username",
|
||||
Destination: dest,
|
||||
Roles: []string{"access"},
|
||||
Format: TLSDatabaseFormat,
|
||||
Service: "my-database-service",
|
||||
Database: "my-database",
|
||||
Username: "my-username",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
CredentialLifetime: bot.CredentialLifetime{
|
||||
TTL: 1 * time.Minute,
|
||||
RenewalInterval: 30 * time.Second,
|
||||
@@ -99,6 +100,18 @@ func TestDatabaseOutput_CheckAndSetDefaults(t *testing.T) {
|
||||
},
|
||||
wantErr: "unrecognized format (no-such-format)",
|
||||
},
|
||||
{
|
||||
name: "delegation session id conflicts with roles",
|
||||
in: func() *OutputConfig {
|
||||
return &OutputConfig{
|
||||
Destination: destination.NewMemory(),
|
||||
Roles: []string{"access"},
|
||||
Service: "service",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
}
|
||||
},
|
||||
wantErr: "delegation_session_id: is mutually-exclusive with roles",
|
||||
},
|
||||
}
|
||||
testCheckAndSetDefaults(t, tests)
|
||||
}
|
||||
|
||||
@@ -120,10 +120,14 @@ func (s *OutputService) generate(ctx context.Context) error {
|
||||
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
identityOpts := []identity.GenerateOption{
|
||||
identity.WithRoles(s.cfg.Roles),
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
}
|
||||
if s.cfg.DelegationSessionID == "" {
|
||||
identityOpts = append(identityOpts, identity.WithRoles(s.cfg.Roles))
|
||||
} else {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
id, err := s.identityGenerator.GenerateFacade(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "generating identity")
|
||||
|
||||
@@ -7,5 +7,6 @@ format: tls
|
||||
service: my-database-service
|
||||
database: my-database
|
||||
username: my-username
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
|
||||
+1
@@ -6,5 +6,6 @@ roles:
|
||||
service: service
|
||||
database: database
|
||||
username: username
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
|
||||
@@ -51,6 +51,11 @@ type TunnelConfig struct {
|
||||
// Username is the database username to proxy as.
|
||||
Username string `yaml:"username"`
|
||||
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
|
||||
// CredentialLifetime contains configuration for how long credentials will
|
||||
// last and the frequency at which they'll be renewed.
|
||||
CredentialLifetime bot.CredentialLifetime `yaml:",inline"`
|
||||
@@ -102,6 +107,9 @@ func (s *TunnelConfig) CheckAndSetDefaults() error {
|
||||
if _, err := url.Parse(s.Listen); err != nil {
|
||||
return trace.Wrap(err, "parsing listen")
|
||||
}
|
||||
if s.DelegationSessionID != "" && len(s.Roles) > 0 {
|
||||
return trace.BadParameter("delegation_session_id: is mutually-exclusive with roles")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,12 @@ func TestDatabaseTunnelService_YAML(t *testing.T) {
|
||||
{
|
||||
name: "full",
|
||||
in: TunnelConfig{
|
||||
Listen: "tcp://0.0.0.0:3621",
|
||||
Roles: []string{"role1", "role2"},
|
||||
Service: "service",
|
||||
Database: "database",
|
||||
Username: "username",
|
||||
Listen: "tcp://0.0.0.0:3621",
|
||||
Roles: []string{"role1", "role2"},
|
||||
Service: "service",
|
||||
Database: "database",
|
||||
Username: "username",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
CredentialLifetime: bot.CredentialLifetime{
|
||||
TTL: 1 * time.Minute,
|
||||
RenewalInterval: 30 * time.Second,
|
||||
@@ -112,6 +113,20 @@ func TestDatabaseTunnelService_CheckAndSetDefaults(t *testing.T) {
|
||||
},
|
||||
wantErr: "username: should not be empty",
|
||||
},
|
||||
{
|
||||
name: "delegation session id conflicts with roles",
|
||||
in: func() *TunnelConfig {
|
||||
return &TunnelConfig{
|
||||
Listen: "tcp://0.0.0.0:3621",
|
||||
Roles: []string{"role1", "role2"},
|
||||
Service: "service",
|
||||
Database: "database",
|
||||
Username: "username",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
}
|
||||
},
|
||||
wantErr: "delegation_session_id: is mutually-exclusive with roles",
|
||||
},
|
||||
}
|
||||
testCheckAndSetDefaults(t, tests)
|
||||
}
|
||||
|
||||
@@ -282,11 +282,16 @@ func (s *TunnelService) getRouteToDatabaseWithImpersonation(ctx context.Context)
|
||||
defer span.End()
|
||||
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
impersonatedIdentity, err := s.identityGenerator.GenerateFacade(ctx,
|
||||
identity.WithRoles(s.cfg.Roles),
|
||||
identityOpts := []identity.GenerateOption{
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
)
|
||||
}
|
||||
if s.cfg.DelegationSessionID == "" {
|
||||
identityOpts = append(identityOpts, identity.WithRoles(s.cfg.Roles))
|
||||
} else {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
impersonatedIdentity, err := s.identityGenerator.GenerateFacade(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return proto.RouteToDatabase{}, trace.Wrap(err)
|
||||
}
|
||||
@@ -313,12 +318,17 @@ func (s *TunnelService) issueCert(
|
||||
|
||||
s.log.DebugContext(ctx, "Requesting issuance of certificate for tunnel proxy.")
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
ident, err := s.identityGenerator.Generate(ctx,
|
||||
identity.WithRoles(s.cfg.Roles),
|
||||
identityOpts := []identity.GenerateOption{
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
identity.WithRouteToDatabase(route),
|
||||
)
|
||||
}
|
||||
if s.cfg.DelegationSessionID == "" {
|
||||
identityOpts = append(identityOpts, identity.WithRoles(s.cfg.Roles))
|
||||
} else {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
ident, err := s.identityGenerator.Generate(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -146,11 +146,15 @@ func (s *OutputService) generate(ctx context.Context) error {
|
||||
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
identityOpts := []identity.GenerateOption{
|
||||
identity.WithRoles(s.cfg.Roles),
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithReissuableRoleImpersonation(s.cfg.AllowReissue),
|
||||
identity.WithLogger(s.log),
|
||||
}
|
||||
if s.cfg.DelegationSessionID == "" {
|
||||
identityOpts = append(identityOpts, identity.WithRoles(s.cfg.Roles))
|
||||
} else {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
id, err := s.identityGenerator.GenerateFacade(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "generating identity")
|
||||
@@ -191,7 +195,7 @@ func (s *OutputService) generate(ctx context.Context) error {
|
||||
}
|
||||
|
||||
if s.cfg.SSHConfigMode == SSHConfigModeOn {
|
||||
clusterNames, err := internal.GetClusterNames(ctx, impersonatedClient, id.Get().ClusterName)
|
||||
clusterNames, err := internal.GetClusterNames(ctx, impersonatedClient, id.Get().ClusterName, s.cfg.DelegationSessionID != "")
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,11 @@ type OutputConfig struct {
|
||||
// Defaults to false.
|
||||
AllowReissue bool `yaml:"allow_reissue,omitempty"`
|
||||
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
|
||||
// CredentialLifetime contains configuration for how long credentials will
|
||||
// last and the frequency at which they'll be renewed.
|
||||
CredentialLifetime bot.CredentialLifetime `yaml:",inline"`
|
||||
@@ -142,6 +147,9 @@ func (o *OutputConfig) CheckAndSetDefaults() error {
|
||||
default:
|
||||
return trace.BadParameter("ssh_config: unrecognized value %q", o.SSHConfigMode)
|
||||
}
|
||||
if o.DelegationSessionID != "" && len(o.Roles) > 0 {
|
||||
return trace.BadParameter("delegation_session_id: is mutually-exclusive with roles")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,11 +32,12 @@ func TestIdentityOutput_YAML(t *testing.T) {
|
||||
{
|
||||
name: "full",
|
||||
in: OutputConfig{
|
||||
Destination: dest,
|
||||
Roles: []string{"access"},
|
||||
Cluster: "leaf.example.com",
|
||||
SSHConfigMode: SSHConfigModeOff,
|
||||
AllowReissue: true,
|
||||
Destination: dest,
|
||||
Roles: []string{"access"},
|
||||
Cluster: "leaf.example.com",
|
||||
SSHConfigMode: SSHConfigModeOff,
|
||||
AllowReissue: true,
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
CredentialLifetime: bot.CredentialLifetime{
|
||||
TTL: 1 * time.Minute,
|
||||
RenewalInterval: 30 * time.Second,
|
||||
@@ -96,6 +97,18 @@ func TestIdentityOutput_CheckAndSetDefaults(t *testing.T) {
|
||||
},
|
||||
wantErr: "ssh_config: unrecognized value \"invalid\"",
|
||||
},
|
||||
{
|
||||
name: "delegation session id conflicts with roles",
|
||||
in: func() *OutputConfig {
|
||||
return &OutputConfig{
|
||||
Destination: destination.NewMemory(),
|
||||
Roles: []string{"access"},
|
||||
SSHConfigMode: SSHConfigModeOn,
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
}
|
||||
},
|
||||
wantErr: "delegation_session_id: is mutually-exclusive with roles",
|
||||
},
|
||||
}
|
||||
testCheckAndSetDefaults(t, tests)
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ roles:
|
||||
cluster: leaf.example.com
|
||||
ssh_config: "off"
|
||||
allow_reissue: true
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
|
||||
@@ -154,10 +154,14 @@ func (s *OutputV1Service) generate(ctx context.Context) error {
|
||||
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
identityOpts := []identity.GenerateOption{
|
||||
identity.WithRoles(s.cfg.Roles),
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
}
|
||||
if s.cfg.DelegationSessionID == "" {
|
||||
identityOpts = append(identityOpts, identity.WithRoles(s.cfg.Roles))
|
||||
} else {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
id, err := s.identityGenerator.GenerateFacade(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "generating identity")
|
||||
|
||||
@@ -55,6 +55,11 @@ type OutputV1Config struct {
|
||||
// refresh the credentials within an individual invocation.
|
||||
DisableExecPlugin bool `yaml:"disable_exec_plugin"`
|
||||
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
|
||||
// CredentialLifetime contains configuration for how long credentials will
|
||||
// last and the frequency at which they'll be renewed.
|
||||
CredentialLifetime bot.CredentialLifetime `yaml:",inline"`
|
||||
@@ -80,6 +85,9 @@ func (o *OutputV1Config) CheckAndSetDefaults() error {
|
||||
if o.KubernetesCluster == "" {
|
||||
return trace.BadParameter("kubernetes_cluster must not be empty")
|
||||
}
|
||||
if o.DelegationSessionID != "" && len(o.Roles) > 0 {
|
||||
return trace.BadParameter("delegation_session_id: is mutually-exclusive with roles")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,10 @@ func TestKubernetesOutput_YAML(t *testing.T) {
|
||||
{
|
||||
name: "full",
|
||||
in: OutputV1Config{
|
||||
Destination: dest,
|
||||
Roles: []string{"access"},
|
||||
KubernetesCluster: "k8s.example.com",
|
||||
Destination: dest,
|
||||
Roles: []string{"access"},
|
||||
KubernetesCluster: "k8s.example.com",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
CredentialLifetime: bot.CredentialLifetime{
|
||||
TTL: 1 * time.Minute,
|
||||
RenewalInterval: 30 * time.Second,
|
||||
@@ -83,6 +84,18 @@ func TestKubernetesOutput_CheckAndSetDefaults(t *testing.T) {
|
||||
},
|
||||
wantErr: "kubernetes_cluster must not be empty",
|
||||
},
|
||||
{
|
||||
name: "delegation session id conflicts with roles",
|
||||
in: func() *OutputV1Config {
|
||||
return &OutputV1Config{
|
||||
Destination: destination.NewMemory(),
|
||||
Roles: []string{"access"},
|
||||
KubernetesCluster: "my-cluster",
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
}
|
||||
},
|
||||
wantErr: "delegation_session_id: is mutually-exclusive with roles",
|
||||
},
|
||||
}
|
||||
testCheckAndSetDefaults(t, tests)
|
||||
}
|
||||
|
||||
@@ -153,10 +153,16 @@ func (s *OutputV2Service) generate(ctx context.Context) error {
|
||||
}
|
||||
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
id, err := s.identityGenerator.GenerateFacade(ctx,
|
||||
opts := []identity.GenerateOption{
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
)
|
||||
}
|
||||
|
||||
if s.cfg.DelegationSessionID != "" {
|
||||
opts = append(opts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
|
||||
id, err := s.identityGenerator.GenerateFacade(ctx, opts...)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "generating identity")
|
||||
}
|
||||
|
||||
@@ -72,6 +72,11 @@ type OutputV2Config struct {
|
||||
// RelayAddress specifies the address of a relay transport server to use in
|
||||
// the generated Kubernetes config file.
|
||||
RelayAddress string `yaml:"relay_server,omitempty"`
|
||||
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
}
|
||||
|
||||
// GetName returns the user-given name of the service, used for validation purposes.
|
||||
|
||||
@@ -32,8 +32,9 @@ func TestKubernetesV2Output_YAML(t *testing.T) {
|
||||
{
|
||||
name: "full",
|
||||
in: OutputV2Config{
|
||||
Destination: dest,
|
||||
DisableExecPlugin: true,
|
||||
Destination: dest,
|
||||
DisableExecPlugin: true,
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
Selectors: []*KubernetesSelector{
|
||||
{
|
||||
Name: "foo",
|
||||
|
||||
@@ -5,5 +5,6 @@ roles:
|
||||
- access
|
||||
kubernetes_cluster: k8s.example.com
|
||||
disable_exec_plugin: false
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
|
||||
@@ -10,3 +10,4 @@ selectors:
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
context_name_template: '{{.KubeName}}'
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
|
||||
@@ -194,7 +194,7 @@ func (s *MultiplexerService) writeArtifacts(
|
||||
) error {
|
||||
dest := s.cfg.Destination.(*destination.Directory)
|
||||
|
||||
clusterNames, err := internal.GetClusterNames(ctx, authClient, s.identity.Get().ClusterName)
|
||||
clusterNames, err := internal.GetClusterNames(ctx, authClient, s.identity.Get().ClusterName, s.cfg.DelegationSessionID != "")
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "fetching cluster names")
|
||||
}
|
||||
@@ -395,10 +395,14 @@ func (s *MultiplexerService) setup(ctx context.Context) (
|
||||
// the destination.
|
||||
func (s *MultiplexerService) generateIdentity(ctx context.Context) (*identity.Facade, error) {
|
||||
effectiveLifetime := cmp.Or(s.cfg.CredentialLifetime, s.defaultCredentialLifetime)
|
||||
facade, err := s.identityGenerator.GenerateFacade(ctx,
|
||||
identityOpts := []identity.GenerateOption{
|
||||
identity.WithLifetime(effectiveLifetime.TTL, effectiveLifetime.RenewalInterval),
|
||||
identity.WithLogger(s.log),
|
||||
)
|
||||
}
|
||||
if s.cfg.DelegationSessionID != "" {
|
||||
identityOpts = append(identityOpts, identity.WithDelegation(s.cfg.DelegationSessionID))
|
||||
}
|
||||
facade, err := s.identityGenerator.GenerateFacade(ctx, identityOpts...)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "generating identity")
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ type MultiplexerConfig struct {
|
||||
// all the SSH connections going through this mux.
|
||||
RelayAddress string `yaml:"relay_server,omitempty"`
|
||||
|
||||
// DelegationSessionID optionally identifies the delegation session the
|
||||
// generated credentials will be associated with, enabling the bot to act
|
||||
// on a (human) user's behalf.
|
||||
DelegationSessionID string `yaml:"delegation_session_id,omitempty"`
|
||||
|
||||
// CredentialLifetime contains configuration for how long credentials will
|
||||
// last and the frequency at which they'll be renewed.
|
||||
CredentialLifetime bot.CredentialLifetime `yaml:",inline"`
|
||||
|
||||
@@ -37,9 +37,10 @@ func TestSSHMultiplexerService_YAML(t *testing.T) {
|
||||
Destination: &destination.Directory{
|
||||
Path: "/opt/machine-id",
|
||||
},
|
||||
EnableResumption: ptr(true),
|
||||
ProxyTemplatesPath: "/etc/teleport/templates",
|
||||
ProxyCommand: []string{"rusty-boi"},
|
||||
EnableResumption: ptr(true),
|
||||
ProxyTemplatesPath: "/etc/teleport/templates",
|
||||
ProxyCommand: []string{"rusty-boi"},
|
||||
DelegationSessionID: "8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee",
|
||||
CredentialLifetime: bot.CredentialLifetime{
|
||||
TTL: 1 * time.Minute,
|
||||
RenewalInterval: 30 * time.Second,
|
||||
|
||||
@@ -6,5 +6,6 @@ enable_resumption: true
|
||||
proxy_templates_path: /etc/teleport/templates
|
||||
proxy_command:
|
||||
- rusty-boi
|
||||
delegation_session_id: 8a50ba48-2fad-4c2c-a8ce-f48bc18db9ee
|
||||
credential_ttl: 1m0s
|
||||
renewal_interval: 30s
|
||||
|
||||
Reference in New Issue
Block a user