Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v14] Add bot field to certificates and various usage events (#35881) #36313

Merged
merged 4 commits into from Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions api/proto/teleport/legacy/types/events/events.proto
Expand Up @@ -63,6 +63,21 @@ message SessionMetadata {
string PrivateKeyPolicy = 3 [(gogoproto.jsontag) = "private_key_policy,omitempty"];
}

// The kind of user a given username refers to. Usernames should always refer to
// a valid cluster user (even if temporary, e.g. SSO), but may be Machine ID
// bot users.
enum UserKind {
// Indicates a legacy cluster emitting events without a defined user kind.
USER_KIND_UNSPECIFIED = 0;

// Indicates the user associated with this event is human, either created
// locally or via SSO.
USER_KIND_HUMAN = 1;

// Indicates the user associated with this event is a Machine ID bot user.
USER_KIND_BOT = 2;
}

// UserMetadata is a common user event metadata
message UserMetadata {
// User is teleport user name
Expand Down Expand Up @@ -92,6 +107,10 @@ message UserMetadata {

// RequiredPrivateKeyPolicy is the private key policy enforced for this login.
string RequiredPrivateKeyPolicy = 9 [(gogoproto.jsontag) = "required_private_key_policy,omitempty"];

// UserKind indicates what type of user this is, e.g. a human or Machine ID
// bot user.
UserKind UserKind = 10 [(gogoproto.jsontag) = "user_kind,omitempty"];
}

// Server is a server metadata
Expand Down Expand Up @@ -3659,6 +3678,9 @@ message Identity {
repeated string GCPServiceAccounts = 25 [(gogoproto.jsontag) = "gcp_service_accounts,omitempty"];
// PrivateKeyPolicy is the private key policy of the user's private key.
string PrivateKeyPolicy = 26 [(gogoproto.jsontag) = "private_key_policy,omitempty"];
// BotName indicates the name of the Machine ID bot this identity was issued
// to, if any.
string BotName = 27 [(gogoproto.jsontag) = "bot_name,omitempty"];
}

// RouteToApp contains parameters for application access certificate requests.
Expand Down
1,766 changes: 942 additions & 824 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions constants.go
Expand Up @@ -493,6 +493,9 @@ const (
// CertExtensionDeviceCredentialID is the identifier for the credential used
// by the device to authenticate itself.
CertExtensionDeviceCredentialID = "teleport-device-credential-id"
// CertExtensionBotName indicates the name of the Machine ID bot this
// certificate was issued to, if any.
CertExtensionBotName = "bot-name@goteleport.com"

// CertCriticalOptionSourceAddress is a critical option that defines IP addresses (in CIDR notation)
// from which this certificate is accepted for authentication.
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/auth.go
Expand Up @@ -1774,6 +1774,8 @@ type certRequest struct {
skipAttestation bool
// deviceExtensions holds device-aware user certificate extensions.
deviceExtensions DeviceExtensions
// botName is the name of the bot requesting this cert, if any
botName string
}

// check verifies the cert request is valid.
Expand Down Expand Up @@ -2519,6 +2521,7 @@ func generateCert(a *Server, req certRequest, caType types.CertAuthType) (*proto
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
BotName: req.botName,
CertificateExtensions: req.checker.CertificateExtensions(),
AllowedResourceIDs: requestedResourcesStr,
ConnectionDiagnosticID: req.connectionDiagnosticID,
Expand Down Expand Up @@ -2615,6 +2618,7 @@ func generateCert(a *Server, req certRequest, caType types.CertAuthType) (*proto
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
BotName: req.botName,
AllowedResourceIDs: req.checker.GetAllowedResourceIDs(),
PrivateKeyPolicy: attestedKeyPolicy,
ConnectionDiagnosticID: req.connectionDiagnosticID,
Expand Down
12 changes: 12 additions & 0 deletions lib/auth/auth_with_roles.go
Expand Up @@ -2954,6 +2954,17 @@ func isRoleImpersonation(req proto.UserCertsRequest) bool {
return req.UseRoleRequests || len(req.RoleRequests) > 0
}

// getBotName returns the name of the bot embedded in the user metadata, if any.
// For non-bot users, returns "".
func getBotName(user types.User) string {
name, ok := user.GetLabel(types.BotLabel)
if ok {
return name
}

return ""
}

func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserCertsRequest, opts ...certRequestOption) (*proto.Certs, error) {
// Device trust: authorize device before issuing certificates.
authPref, err := a.authServer.GetAuthPreference(ctx)
Expand Down Expand Up @@ -3187,6 +3198,7 @@ func (a *ServerWithRoles) generateUserCerts(ctx context.Context, req proto.UserC
},
connectionDiagnosticID: req.ConnectionDiagnosticID,
attestationStatement: keys.AttestationStatementFromProto(req.AttestationStatement),
botName: getBotName(user),
}
if user.GetName() != a.context.User.GetName() {
certReq.impersonator = a.context.User.GetName()
Expand Down
3 changes: 2 additions & 1 deletion lib/auth/bot.go
Expand Up @@ -481,7 +481,7 @@ func (a *Server) validateGenerationLabel(ctx context.Context, username string, c
// care if the current identity is Nop. This function does not validate the
// current identity at all; the caller is expected to validate that the client
// is allowed to issue the (possibly renewable) certificates.
func (a *Server) generateInitialBotCerts(ctx context.Context, username, loginIP string, pubKey []byte, expires time.Time, renewable bool) (*proto.Certs, error) {
func (a *Server) generateInitialBotCerts(ctx context.Context, botName, username, loginIP string, pubKey []byte, expires time.Time, renewable bool) (*proto.Certs, error) {
var err error

// Extract the user and role set for whom the certificate will be generated.
Expand Down Expand Up @@ -535,6 +535,7 @@ func (a *Server) generateInitialBotCerts(ctx context.Context, username, loginIP
includeHostCA: true,
generation: generation,
loginIP: loginIP,
botName: botName,
}

if err := a.validateGenerationLabel(ctx, userState.GetName(), &certReq, 0); err != nil {
Expand Down
56 changes: 56 additions & 0 deletions lib/auth/bot_test.go
Expand Up @@ -320,6 +320,62 @@ func TestRegisterBotCertificateGenerationStolen(t *testing.T) {
require.NotEmpty(t, locks)
}

// TestRegisterBotCertificateExtensions ensures bot cert extensions are present.
func TestRegisterBotCertificateExtensions(t *testing.T) {
t.Parallel()
srv := newTestTLSServer(t)
ctx := context.Background()

_, err := CreateRole(ctx, srv.Auth(), "example", types.RoleSpecV6{})
require.NoError(t, err)

// Create a new bot.
bot, err := srv.Auth().createBot(ctx, &proto.CreateBotRequest{
Name: "test",
Roles: []string{"example"},
})
require.NoError(t, err)

privateKey, publicKey, err := testauthority.New().GenerateKeyPair()
require.NoError(t, err)
sshPrivateKey, err := ssh.ParseRawPrivateKey(privateKey)
require.NoError(t, err)
tlsPublicKey, err := tlsca.MarshalPublicKeyFromPrivateKeyPEM(sshPrivateKey)
require.NoError(t, err)

certs, err := Register(RegisterParams{
Token: bot.TokenID,
ID: IdentityID{
Role: types.RoleBot,
},
AuthServers: []utils.NetAddr{*utils.MustParseAddr(srv.Addr().String())},
PublicTLSKey: tlsPublicKey,
PublicSSHKey: publicKey,
})
require.NoError(t, err)
checkCertLoginIP(t, certs.TLS, "127.0.0.1")

tlsCert, err := tls.X509KeyPair(certs.TLS, privateKey)
require.NoError(t, err)

_, certs, _, err = renewBotCerts(ctx, srv, tlsCert, bot.UserName, publicKey, privateKey)
require.NoError(t, err)

// Parse the Identity
impersonatedTLSCert, err := tlsca.ParseCertificatePEM(certs.TLS)
require.NoError(t, err)
impersonatedIdent, err := tlsca.FromSubject(impersonatedTLSCert.Subject, impersonatedTLSCert.NotAfter)
require.NoError(t, err)

// Check for proper cert extensions
require.True(t, impersonatedIdent.Renewable)
require.False(t, impersonatedIdent.DisallowReissue)
require.Equal(t, "test", impersonatedIdent.BotName)

// Initial certs have generation=1 and we start with a renewal, so add 2
require.Equal(t, uint64(2), impersonatedIdent.Generation)
}

// TestRegisterBot_RemoteAddr checks that certs returned for bot registration contain specified in the request remote addr.
func TestRegisterBot_RemoteAddr(t *testing.T) {
t.Parallel()
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/join.go
Expand Up @@ -236,7 +236,7 @@ func (a *Server) generateCertsBot(
}

certs, err := a.generateInitialBotCerts(
ctx, BotResourceName(botName), req.RemoteAddr, req.PublicSSHKey, expires, renewable,
ctx, botName, BotResourceName(botName), req.RemoteAddr, req.PublicSSHKey, expires, renewable,
)
if err != nil {
return nil, trace.Wrap(err)
Expand Down
3 changes: 3 additions & 0 deletions lib/auth/keygen/keygen.go
Expand Up @@ -205,6 +205,9 @@ func (k *Keygen) GenerateUserCertWithoutValidation(c services.UserCertParams) ([
if c.Generation > 0 {
cert.Permissions.Extensions[teleport.CertExtensionGeneration] = fmt.Sprint(c.Generation)
}
if c.BotName != "" {
cert.Permissions.Extensions[teleport.CertExtensionBotName] = c.BotName
}
if c.AllowedResourceIDs != "" {
cert.Permissions.Extensions[teleport.CertExtensionAllowedResources] = c.AllowedResourceIDs
}
Expand Down
21 changes: 14 additions & 7 deletions lib/auth/tls_test.go
Expand Up @@ -4048,7 +4048,8 @@ func TestGRPCServer_CreateTokenV2(t *testing.T) {
Code: events.ProvisionTokenCreateCode,
},
UserMetadata: eventtypes.UserMetadata{
User: "token-creator",
User: "token-creator",
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
},
Roles: types.SystemRoles{types.RoleNode, types.RoleKube},
JoinMethod: types.JoinMethodToken,
Expand Down Expand Up @@ -4076,7 +4077,8 @@ func TestGRPCServer_CreateTokenV2(t *testing.T) {
Code: events.ProvisionTokenCreateCode,
},
UserMetadata: eventtypes.UserMetadata{
User: "token-creator",
User: "token-creator",
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
},
Roles: types.SystemRoles{types.RoleTrustedCluster},
JoinMethod: types.JoinMethodToken,
Expand All @@ -4088,7 +4090,8 @@ func TestGRPCServer_CreateTokenV2(t *testing.T) {
Code: events.TrustedClusterTokenCreateCode,
},
UserMetadata: eventtypes.UserMetadata{
User: "token-creator",
User: "token-creator",
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
},
},
},
Expand Down Expand Up @@ -4207,7 +4210,8 @@ func TestGRPCServer_UpsertTokenV2(t *testing.T) {
Code: events.ProvisionTokenCreateCode,
},
UserMetadata: eventtypes.UserMetadata{
User: "token-upserter",
User: "token-upserter",
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
},
Roles: types.SystemRoles{types.RoleNode, types.RoleKube},
JoinMethod: types.JoinMethodToken,
Expand Down Expand Up @@ -4235,7 +4239,8 @@ func TestGRPCServer_UpsertTokenV2(t *testing.T) {
Code: events.ProvisionTokenCreateCode,
},
UserMetadata: eventtypes.UserMetadata{
User: "token-upserter",
User: "token-upserter",
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
},
Roles: types.SystemRoles{types.RoleTrustedCluster},
JoinMethod: types.JoinMethodToken,
Expand All @@ -4247,7 +4252,8 @@ func TestGRPCServer_UpsertTokenV2(t *testing.T) {
Code: events.TrustedClusterTokenCreateCode,
},
UserMetadata: eventtypes.UserMetadata{
User: "token-upserter",
User: "token-upserter",
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
},
},
},
Expand Down Expand Up @@ -4275,7 +4281,8 @@ func TestGRPCServer_UpsertTokenV2(t *testing.T) {
Code: events.ProvisionTokenCreateCode,
},
UserMetadata: eventtypes.UserMetadata{
User: "token-upserter",
User: "token-upserter",
UserKind: eventtypes.UserKind_USER_KIND_HUMAN,
},
Roles: types.SystemRoles{types.RoleNode},
JoinMethod: types.JoinMethodToken,
Expand Down
5 changes: 4 additions & 1 deletion lib/services/authority.go
Expand Up @@ -357,10 +357,13 @@ type UserCertParams struct {
DisallowReissue bool
// CertificateExtensions are user configured ssh key extensions
CertificateExtensions []*types.CertExtension
// Renewable indicates this certificate is renewable
// Renewable indicates this certificate is renewable.
Renewable bool
// Generation counts the number of times a certificate has been renewed.
Generation uint64
// BotName is set to the name of the bot, if the user is a Machine ID bot user.
// Empty for human users.
BotName string
// AllowedResourceIDs lists the resources the user should be able to access.
AllowedResourceIDs string
// ConnectionDiagnosticID references the ConnectionDiagnostic that we should use to append traces when testing a Connection.
Expand Down
3 changes: 3 additions & 0 deletions lib/srv/authhandlers.go
Expand Up @@ -204,6 +204,9 @@ func (h *AuthHandlers) CreateIdentityContext(sconn *ssh.ServerConn) (IdentityCon
if _, ok := certificate.Extensions[teleport.CertExtensionRenewable]; ok {
identity.Renewable = true
}
if botName, ok := certificate.Extensions[teleport.CertExtensionBotName]; ok {
identity.BotName = botName
}
if generationStr, ok := certificate.Extensions[teleport.CertExtensionGeneration]; ok {
generation, err := strconv.ParseUint(generationStr, 10, 64)
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions lib/srv/ctx.go
Expand Up @@ -277,6 +277,10 @@ type IdentityContext struct {
// been renewed.
Generation uint64

// BotName is the name of the Machine ID bot this identity is associated
// with, if any.
BotName string

// AllowedResourceIDs lists the resources this identity should be allowed to
// access
AllowedResourceIDs []types.ResourceID
Expand Down Expand Up @@ -1194,12 +1198,18 @@ func eventDeviceMetadataFromCert(cert *ssh.Certificate) *apievents.DeviceMetadat
}

func (id *IdentityContext) GetUserMetadata() apievents.UserMetadata {
userKind := apievents.UserKind_USER_KIND_HUMAN
if id.BotName != "" {
userKind = apievents.UserKind_USER_KIND_BOT
}

return apievents.UserMetadata{
Login: id.Login,
User: id.TeleportUser,
Impersonator: id.Impersonator,
AccessRequests: id.ActiveRequests,
TrustedDevice: eventDeviceMetadataFromCert(id.Certificate),
UserKind: userKind,
}
}

Expand Down
2 changes: 2 additions & 0 deletions lib/srv/ctx_test.go
Expand Up @@ -198,6 +198,7 @@ func TestIdentityContext_GetUserMetadata(t *testing.T) {
Login: "alpaca1",
Impersonator: "llama",
AccessRequests: []string{"access-req1", "access-req2"},
UserKind: apievents.UserKind_USER_KIND_HUMAN,
},
},
{
Expand All @@ -223,6 +224,7 @@ func TestIdentityContext_GetUserMetadata(t *testing.T) {
AssetTag: "assettag1",
CredentialId: "credentialid1",
},
UserKind: apievents.UserKind_USER_KIND_HUMAN,
},
},
}
Expand Down