Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,998 changes: 1,024 additions & 974 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ message UserCertsRequest {
teleport.attestation.v1.AttestationStatement SSHPublicKeyAttestationStatement = 25 [(gogoproto.jsontag) = "ssh_public_key_attestation_statement,omitempty"];
// TLSPublicKeyAttestationStatement is an attestation statement for TLSPublicKey.
teleport.attestation.v1.AttestationStatement TLSPublicKeyAttestationStatement = 26 [(gogoproto.jsontag) = "tls_public_key_attestation_statement,omitempty"];

// ReissuableRoleImpersonation is a flag that indicates whether or not a
// role impersonation certificate can be reissued. If set to true, the
// generated certificate will be re-issuable, by default, it will not be
// re-issuable. This flag is only applicable when UseRoleRequests is set to
// true.
bool ReissuableRoleImpersonation = 27 [(gogoproto.jsontag) = "reissuable_role_impersonation"];
}

// RouteToDatabase combines parameters for database service routing information.
Expand Down
14 changes: 11 additions & 3 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3165,8 +3165,8 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
return nil, trace.AccessDenied("access denied: impersonated user can not request new roles")
}
if isRoleImpersonation(req) {
// Note: technically this should never be needed as all role
// impersonated certs should have the DisallowReissue set.
// Disallow role impersonated certs from performing further role
// impersonation, to reduce the risk of privilege escalation.
return nil, trace.AccessDenied("access denied: impersonated roles can not request other roles")
}
if req.Username != a.context.User.GetName() {
Expand Down Expand Up @@ -3435,8 +3435,16 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
// Role impersonation uses the user's own name as the impersonator value.
certReq.impersonator = a.context.User.GetName()

// Deny reissuing certs to prevent privilege re-escalation.
// By default, deny reissuing certs to prevent privilege re-escalation.
// (E.g a cert generated intended for use for Kubernetes Access against
// a specific cluster could be reissued with a different cluster name.)
// This can be overridden by the user if they acknowledge the risk and
// require a certificate which can be reissued (e.g dynamic app access
// use-case).
certReq.disallowReissue = true
if req.ReissuableRoleImpersonation {
certReq.disallowReissue = false
}
} else if a.context.Identity != nil && a.context.Identity.GetIdentity().Impersonator != "" {
// impersonating users can receive new certs
certReq.impersonator = a.context.Identity.GetIdentity().Impersonator
Expand Down
149 changes: 149 additions & 0 deletions lib/auth/auth_with_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,155 @@ func TestGenerateUserCertsWithRoleRequest(t *testing.T) {
}
}

// TestRolesRequestsExplicitAllowReissue ensures that the client can explicitly
// request role impersonation with reissuance allowed. It then checks that the
// impersonated client can reissue certificates but that role impersonation
// cannot be escaped.
func TestRolesRequestsExplicitAllowReissue(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)

app, err := types.NewAppServerV3(types.Metadata{Name: "my-app"}, types.AppServerSpecV3{
HostID: "my-app",
Hostname: "example.com",
App: &types.AppV3{
Metadata: types.Metadata{
Name: "my-app",
Labels: map[string]string{
"foo": "bar",
},
},
Spec: types.AppSpecV3{
URI: "http://127.0.0.1",
PublicAddr: "my-app.example.com",
},
},
})
require.NoError(t, err)

_, err = srv.Auth().UpsertApplicationServer(ctx, app)
require.NoError(t, err)

accessFooRole, err := CreateRole(ctx, srv.Auth(), "test-access-foo", types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{"foo"},
AppLabels: types.Labels{
"foo": []string{"bar"},
},
},
})
require.NoError(t, err)

accessBarRole, err := CreateRole(ctx, srv.Auth(), "test-access-bar", types.RoleSpecV6{
Allow: types.RoleConditions{
Logins: []string{"bar"},
},
})
require.NoError(t, err)

impersonatorRole, err := CreateRole(ctx, srv.Auth(), "test-impersonator", types.RoleSpecV6{
Allow: types.RoleConditions{
Impersonate: &types.ImpersonateConditions{
Roles: []string{accessFooRole.GetName(), accessBarRole.GetName()},
},
},
})
require.NoError(t, err)

// Create a testing user.
user, err := CreateUser(ctx, srv.Auth(), "alice")
require.NoError(t, err)
user.AddRole(impersonatorRole.GetName())
user, err = srv.Auth().UpsertUser(ctx, user)
require.NoError(t, err)

// Generate cert with a role request.
client, err := srv.NewClient(TestUser(user.GetName()))
require.NoError(t, err)

_, sshPubKey, tlsPrivKey, tlsPubKey := newSSHAndTLSKeyPairs(t)

// Request certs for only the `foo` role.
certs, err := client.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: sshPubKey,
TLSPublicKey: tlsPubKey,
Username: user.GetName(),
Expires: time.Now().Add(time.Hour),
RoleRequests: []string{accessFooRole.GetName()},
ReissuableRoleImpersonation: true,
})
require.NoError(t, err)

// Make an impersonated client.
impersonatedTLSCert, err := tls.X509KeyPair(certs.TLS, tlsPrivKey)
require.NoError(t, err)
impersonatedClient := srv.NewClientWithCert(impersonatedTLSCert)

ident, err := tlsca.FromSubject(
impersonatedTLSCert.Leaf.Subject,
impersonatedTLSCert.Leaf.NotAfter,
)
require.NoError(t, err)
require.False(t, ident.DisallowReissue)
require.Equal(t, []string{"test-access-foo"}, ident.Groups)

// Attempt to switch to a different role. This should be disallowed.
_, err = impersonatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: sshPubKey,
TLSPublicKey: tlsPubKey,
Username: user.GetName(),
Expires: time.Now().Add(time.Hour),
RoleRequests: []string{accessBarRole.GetName()},
})
require.Error(t, err)
require.True(t, trace.IsAccessDenied(err))

// Try generation without requesting roles. This should be allowed but
// we should ensure we still have our impersonated role,.
certs, err = impersonatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: sshPubKey,
TLSPublicKey: tlsPubKey,
Username: user.GetName(),
Expires: time.Now().Add(time.Hour),
})
require.NoError(t, err)
parsedCert, err := tlsca.ParseCertificatePEM(certs.TLS)
require.NoError(t, err)
ident, err = tlsca.FromSubject(
parsedCert.Subject,
parsedCert.NotAfter,
)
require.NoError(t, err)
require.False(t, ident.DisallowReissue)
require.Equal(t, []string{"test-access-foo"}, ident.Groups)

// Attempt to reissue for an app access cert. This should work, we must
// also keep our impersonated role.
certs, err = impersonatedClient.GenerateUserCerts(ctx, proto.UserCertsRequest{
SSHPublicKey: sshPubKey,
TLSPublicKey: tlsPubKey,
Username: user.GetName(),
Expires: time.Now().Add(time.Hour),
RouteToApp: proto.RouteToApp{
Name: app.GetApp().GetName(),
PublicAddr: app.GetApp().GetPublicAddr(),
},
})
require.NoError(t, err)
parsedCert, err = tlsca.ParseCertificatePEM(certs.TLS)
require.NoError(t, err)
ident, err = tlsca.FromSubject(
parsedCert.Subject,
parsedCert.NotAfter,
)
require.NoError(t, err)
require.False(t, ident.DisallowReissue)
require.Equal(t, []string{"test-access-foo"}, ident.Groups)
require.Equal(t, "my-app", ident.RouteToApp.Name)
require.Equal(t, "my-app.example.com", ident.RouteToApp.PublicAddr)
require.NotEmpty(t, ident.RouteToApp.SessionID)
}

// TestRoleRequestDenyReimpersonation make sure role requests can't be used to
// re-escalate privileges using a (perhaps compromised) set of role
// impersonated certs.
Expand Down
10 changes: 6 additions & 4 deletions lib/tbot/cli/start_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ type IdentityCommand struct {
*sharedDestinationArgs
*genericMutatorHandler

Cluster string
Cluster string
AllowReissue bool
}

// NewIdentityCommand initializes the command and flags for identity outputs
Expand All @@ -50,7 +51,7 @@ func NewIdentityCommand(parentCmd *kingpin.CmdClause, action MutatorAction, mode
c.genericMutatorHandler = newGenericMutatorHandler(cmd, c, action)

cmd.Flag("cluster", "The name of a specific cluster for which to issue an identity if using a leaf cluster").StringVar(&c.Cluster)

cmd.Flag("allow-reissue", "Allow the credentials output by this command to be reissued.").BoolVar(&c.AllowReissue)
// Note: roles and ssh_config mode are excluded for now.

return c
Expand All @@ -67,8 +68,9 @@ func (c *IdentityCommand) ApplyConfig(cfg *config.BotConfig, l *slog.Logger) err
}

cfg.Services = append(cfg.Services, &config.IdentityOutput{
Destination: dest,
Cluster: c.Cluster,
Destination: dest,
Cluster: c.Cluster,
AllowReissue: c.AllowReissue,
})

return nil
Expand Down
2 changes: 2 additions & 0 deletions lib/tbot/cli/start_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func TestIdentityCommand(t *testing.T) {
"--storage=/foo",
"--destination=file:///bar",
"--proxy-server=example.com:443",
"--allow-reissue",
},
assertConfig: func(t *testing.T, cfg *config.BotConfig) {
token, err := cfg.Onboarding.Token()
Expand Down Expand Up @@ -75,6 +76,7 @@ func TestIdentityCommand(t *testing.T) {
dir, ok = ident.Destination.(*config.DestinationDirectory)
require.True(t, ok)
require.Equal(t, "/bar", dir.Path)
require.True(t, ident.AllowReissue)
},
},
})
Expand Down
9 changes: 9 additions & 0 deletions lib/tbot/config/service_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ type IdentityOutput struct {
// SSHConfigMode controls whether to write an ssh_config file to the
// destination directory. Defaults to SSHConfigModeOn.
SSHConfigMode SSHConfigMode `yaml:"ssh_config,omitempty"`

// AllowReissue controls whether the generated credentials can be used to
// reissue further credentials (e.g to produce a certificate for application
// access). It is recommended to leave this disabled to prevent the scope of
// issued credentials from being increased, however, it can be useful in
// scenarios where credentials are desired to be reissued in a dynamic way.
//
// Defaults to false.
AllowReissue bool `yaml:"allow_reissue,omitempty"`
}

func (o *IdentityOutput) Init(ctx context.Context) error {
Expand Down
1 change: 1 addition & 0 deletions lib/tbot/config/service_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func TestIdentityOutput_YAML(t *testing.T) {
Roles: []string{"access"},
Cluster: "leaf.example.com",
SSHConfigMode: SSHConfigModeOff,
AllowReissue: true,
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ roles:
- access
cluster: leaf.example.com
ssh_config: "off"
allow_reissue: true
5 changes: 4 additions & 1 deletion lib/tbot/service_identity_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ func (s *IdentityOutputService) generate(ctx context.Context) error {
s.getBotIdentity(),
roles,
s.botCfg.CertificateTTL,
nil,
func(req *proto.UserCertsRequest) {
req.ReissuableRoleImpersonation = s.cfg.AllowReissue
},
)
if err != nil {
return trace.Wrap(err, "generating identity")
Expand All @@ -140,6 +142,7 @@ func (s *IdentityOutputService) generate(ctx context.Context) error {
s.botCfg.CertificateTTL,
func(req *proto.UserCertsRequest) {
req.RouteToCluster = s.cfg.Cluster
req.ReissuableRoleImpersonation = s.cfg.AllowReissue
},
)
if err != nil {
Expand Down
Loading