Enhance the Windows desktop session start audit event (#57608)

Include more details about the certificate, which will be helpful when
troubleshooting authentication or validation errors on the Windows side.
This commit is contained in:
Zac Bergquist
2025-08-07 22:10:12 +00:00
committed by GitHub
parent c32627c1a2
commit c73e7538ce
11 changed files with 2091 additions and 1488 deletions
@@ -3570,6 +3570,18 @@ message PostgresFunctionCall {
repeated string FunctionArgs = 6 [(gogoproto.jsontag) = "function_args,omitempty"];
}
// WindowsCertificateMetadata contains metadata about certificates
// issued for Windows environments.
message WindowsCertificateMetadata {
string Subject = 1 [(gogoproto.jsontag) = "subject"];
string SerialNumber = 2 [(gogoproto.jsontag) = "serial_number"];
string UPN = 3 [(gogoproto.jsontag) = "upn"];
repeated string CRLDistributionPoints = 4 [(gogoproto.jsontag) = "crl_distribution_points"];
int32 KeyUsage = 5 [(gogoproto.jsontag) = "key_usage"];
repeated int32 ExtendedKeyUsage = 6 [(gogoproto.jsontag) = "extended_key_usage"];
repeated string EnhancedKeyUsage = 7 [(gogoproto.jsontag) = "enhanced_key_usage"];
}
// WindowsDesktopSessionStart is emitted when a user connects to a desktop.
message WindowsDesktopSessionStart {
// Metadata is common event metadata.
@@ -3620,6 +3632,8 @@ message WindowsDesktopSessionStart {
// NLA indicates whether Teleport performed Network Level Authentication (NLA)
// when initiating this session.
bool NLA = 13 [(gogoproto.jsontag) = "nla"];
WindowsCertificateMetadata CertMetadata = 14 [(gogoproto.jsontag) = "certificate"];
}
// DatabaseSessionEnd is emitted when a user ends the database session.
+1996 -1439
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -734,7 +734,8 @@ func TestJSON(t *testing.T) {
},
{
name: "desktop session start",
json: `{"uid":"cd06365f-3cef-4b21-809a-4af9502c11a1","user":"foo","impersonator":"bar","login":"Administrator","success":true,"proto":"tdp","sid":"test-session","addr.local":"192.168.1.100:39887","addr.remote":"[::1]:34902","with_mfa":"mfa-device","code":"TDP00I","event":"windows.desktop.session.start","time":"2020-04-23T18:22:35.35Z","ei":4,"cluster_name":"test-cluster","windows_user":"Administrator","windows_domain":"test.example.com","desktop_name":"test-desktop","desktop_addr":"[::1]:34902","windows_desktop_service":"00baaef5-ff1e-4222-85a5-c7cb0cd8e7b8","allow_user_creation":false,"nla":true,"desktop_labels":{"env":"production"}}`,
json: `{"uid":"cd06365f-3cef-4b21-809a-4af9502c11a1","user":"foo","impersonator":"bar","login":"Administrator","success":true,"proto":"tdp","sid":"test-session","addr.local":"192.168.1.100:39887","addr.remote":"[::1]:34902","with_mfa":"mfa-device","code":"TDP00I","event":"windows.desktop.session.start","time":"2020-04-23T18:22:35.35Z","ei":4,"cluster_name":"test-cluster","windows_user":"Administrator","windows_domain":"test.example.com","desktop_name":"test-desktop","desktop_addr":"[::1]:34902","windows_desktop_service":"00baaef5-ff1e-4222-85a5-c7cb0cd8e7b8","allow_user_creation":false,"nla":true,"desktop_labels":{"env":"production"},
"certificate": { "crl_distribution_points": [ "ldap:///CN=ABC123_exampple,CN=Teleport,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration,DC=example,DC=com?certificateRevocationList?base?objectClass=cRLDistributionPoint" ], "enhanced_key_usage": [ "1.3.6.1.5.5.7.3.2", "1.3.6.1.4.1.311.20.2.2" ], "extended_key_usage": [ 2 ], "key_usage": 1, "serial_number": "325419247945947193356212349496095884841", "subject": "CN=Administrator", "upn": "[email protected]" } }`,
event: apievents.WindowsDesktopSessionStart{
Metadata: apievents.Metadata{
Index: 4,
@@ -758,6 +759,15 @@ func TestJSON(t *testing.T) {
RemoteAddr: "[::1]:34902",
Protocol: EventProtocolTDP,
},
CertMetadata: &apievents.WindowsCertificateMetadata{
Subject: "CN=Administrator",
UPN: "[email protected]",
CRLDistributionPoints: []string{"ldap:///CN=ABC123_exampple,CN=Teleport,CN=CDP,CN=Public Key Services,CN=Services,CN=Configuration,DC=example,DC=com?certificateRevocationList?base?objectClass=cRLDistributionPoint"},
KeyUsage: 1,
EnhancedKeyUsage: []string{"1.3.6.1.5.5.7.3.2", "1.3.6.1.4.1.311.20.2.2"},
ExtendedKeyUsage: []int32{2},
SerialNumber: "325419247945947193356212349496095884841",
},
Status: apievents.Status{
Success: true,
},
+1
View File
@@ -100,6 +100,7 @@ func (d *desktopSessionAuditor) makeSessionStart(err error) *events.WindowsDeskt
ClusterName: d.clusterName,
Time: d.startTime,
},
CertMetadata: new(events.WindowsCertificateMetadata),
UserMetadata: userMetadata,
SessionMetadata: d.getSessionMetadata(),
ConnectionMetadata: d.getConnectionMetadata(),
+1
View File
@@ -129,6 +129,7 @@ func TestSessionStartEvent(t *testing.T) {
RemoteAddr: testDesktop.GetAddr(),
Protocol: libevents.EventProtocolTDP,
},
CertMetadata: new(events.WindowsCertificateMetadata),
Status: events.Status{
Success: true,
},
+9 -14
View File
@@ -190,9 +190,9 @@ func New(cfg Config) (*Client, error) {
return c, nil
}
// Run starts the rdp client and blocks until the client disconnects,
// then ensures the cleanup is run.
func (c *Client) Run(ctx context.Context) error {
// Run starts the RDP client, using the provided user certificate and private key.
// It blocks until the client disconnects, then ensures the cleanup is run.
func (c *Client) Run(ctx context.Context, certDER, keyDER []byte) error {
// Create a handle to the client to pass to Rust.
// The handle is used to call back into this Client from Rust.
// Since the handle is created and deleted here, methods which
@@ -213,7 +213,7 @@ func (c *Client) Run(ctx context.Context) error {
rustRDPReturnCh := make(chan error, 1)
// Kick off rust RDP loop goroutine
go func() {
rustRDPReturnCh <- c.startRustRDP(ctx)
rustRDPReturnCh <- c.startRustRDP(ctx, certDER, keyDER)
}()
select {
@@ -316,15 +316,10 @@ func (c *Client) sendTDPAlert(message string, severity tdp.Severity) error {
return c.cfg.Conn.WriteMessage(tdp.Alert{Message: message, Severity: severity})
}
func (c *Client) startRustRDP(ctx context.Context) error {
func (c *Client) startRustRDP(ctx context.Context, certDER, keyDER []byte) error {
c.cfg.Logger.InfoContext(ctx, "Rust RDP loop starting")
defer c.cfg.Logger.InfoContext(ctx, "Rust RDP loop finished")
userCertDER, userKeyDER, err := c.cfg.GenerateUserCert(ctx, c.username, c.cfg.CertTTL)
if err != nil {
return trace.Wrap(err)
}
// [username] need only be valid for the duration of
// C.client_run. It is copied on the Rust side and
// thus can be freed here.
@@ -349,14 +344,14 @@ func (c *Client) startRustRDP(ctx context.Context) error {
computerName := C.CString(c.cfg.ComputerName)
defer C.free(unsafe.Pointer(computerName))
cert_der, err := utils.UnsafeSliceData(userCertDER)
cert_der, err := utils.UnsafeSliceData(certDER)
if err != nil {
return trace.Wrap(err)
} else if cert_der == nil {
return trace.BadParameter("user cert was nil")
}
key_der, err := utils.UnsafeSliceData(userKeyDER)
key_der, err := utils.UnsafeSliceData(keyDER)
if err != nil {
return trace.Wrap(err)
} else if key_der == nil {
@@ -386,10 +381,10 @@ func (c *Client) startRustRDP(ctx context.Context) error {
go_computer_name: computerName,
go_kdc_addr: kdcAddr,
// cert length and bytes.
cert_der_len: C.uint32_t(len(userCertDER)),
cert_der_len: C.uint32_t(len(certDER)),
cert_der: (*C.uint8_t)(cert_der),
// key length and bytes.
key_der_len: C.uint32_t(len(userKeyDER)),
key_der_len: C.uint32_t(len(keyDER)),
key_der: (*C.uint8_t)(key_der),
screen_width: C.uint16_t(c.requestedWidth),
screen_height: C.uint16_t(c.requestedHeight),
@@ -23,7 +23,6 @@ import (
"context"
"image/png"
"log/slog"
"time"
"github.com/gravitational/trace"
@@ -46,10 +45,6 @@ type Config struct {
LicenseStore LicenseStore
HostID string
// UserCertGenerator generates user certificates for RDP authentication.
GenerateUserCert GenerateUserCertFn
CertTTL time.Duration
// AuthorizeFn is called to authorize a user connecting to a Windows desktop.
AuthorizeFn func(login string) error
@@ -97,17 +92,11 @@ type Config struct {
AD bool
}
// GenerateUserCertFn generates user certificates for RDP authentication.
type GenerateUserCertFn func(ctx context.Context, username string, ttl time.Duration) (certDER, keyDER []byte, err error)
//nolint:unused // used in client.go that is behind desktop_access_rdp build flag
func (c *Config) checkAndSetDefaults() error {
if c.Addr == "" {
return trace.BadParameter("missing Addr in rdpclient.Config")
}
if c.GenerateUserCert == nil {
return trace.BadParameter("missing GenerateUserCert in rdpclient.Config")
}
if c.Conn == nil {
return trace.BadParameter("missing Conn in rdpclient.Config")
}
+3 -3
View File
@@ -41,9 +41,9 @@ func New(cfg Config) (*Client, error) {
return nil, errors.New("the real rdpclient.Client implementation was not included in this build")
}
// Run starts the rdp client and blocks until the client disconnects,
// then runs the cleanup.
func (c *Client) Run(ctx context.Context) error {
// Run starts the RDP client, using the provided user certificate and private key.
// It blocks until the client disconnects, then ensures the cleanup is run.
func (c *Client) Run(ctx context.Context, certDER, keyDER []byte) error {
return errors.New("the real rdpclient.Client implementation was not included in this build")
}
+4 -11
View File
@@ -21,10 +21,8 @@ package rdpclient
import (
"bytes"
"context"
"log/slog"
"testing"
"time"
"github.com/stretchr/testify/require"
@@ -109,14 +107,9 @@ func TestClientNew_KeyboardLayout(t *testing.T) {
func createConfig(conn *tdp.Conn) Config {
return Config{
Addr: "example.com",
GenerateUserCert: func(_ context.Context, _ string, _ time.Duration) ([]byte, []byte, error) {
return nil, nil, nil
},
AuthorizeFn: func(login string) error {
return nil
},
Conn: conn,
Logger: slog.Default(),
Addr: "example.com",
AuthorizeFn: func(login string) error { return nil },
Conn: conn,
Logger: slog.Default(),
}
}
+51 -8
View File
@@ -23,6 +23,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"errors"
"fmt"
"log/slog"
@@ -56,6 +57,7 @@ import (
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
logutils "github.com/gravitational/teleport/lib/utils/log"
"github.com/gravitational/teleport/lib/utils/slices"
"github.com/gravitational/teleport/lib/winpki"
)
@@ -784,13 +786,9 @@ func (s *WindowsService) connectRDP(ctx context.Context, log *slog.Logger, tdpCo
//nolint:staticcheck // SA4023. False positive, depends on build tags.
rdpc, err := rdpclient.New(rdpclient.Config{
LicenseStore: s.cfg.LicenseStore,
HostID: s.cfg.Heartbeat.HostUUID,
Logger: log,
GenerateUserCert: func(ctx context.Context, username string, ttl time.Duration) (certDER, keyDER []byte, err error) {
return s.generateUserCert(ctx, username, ttl, desktop, createUsers, groups)
},
CertTTL: windowsUserCertTTL,
LicenseStore: s.cfg.LicenseStore,
HostID: s.cfg.Heartbeat.HostUUID,
Logger: log,
Addr: addr.String(),
ComputerName: computerName,
KDCAddr: kdcAddr,
@@ -811,6 +809,7 @@ func (s *WindowsService) connectRDP(ctx context.Context, log *slog.Logger, tdpCo
windowsUser = rdpc.GetClientUsername()
audit.windowsUser = windowsUser
}
//nolint:staticcheck // SA4023. False positive, depends on build tags.
if err != nil {
startEvent := audit.makeSessionStart(err)
@@ -819,6 +818,12 @@ func (s *WindowsService) connectRDP(ctx context.Context, log *slog.Logger, tdpCo
return trace.Wrap(err)
}
// Generate client certificates to be used for the RDP connection.
certDER, keyDER, err := s.generateUserCert(ctx, windowsUser, windowsUserCertTTL, desktop, createUsers, groups)
if err != nil {
return trace.Wrap(err, "could not generate client certificates for RDP")
}
if err := s.trackSession(ctx, &identity, windowsUser, string(sessionID), desktop); err != nil {
return trace.Wrap(err)
}
@@ -860,10 +865,18 @@ func (s *WindowsService) connectRDP(ctx context.Context, log *slog.Logger, tdpCo
startEvent := audit.makeSessionStart(nil)
startEvent.AllowUserCreation = createUsers
// Parse some information about the cert, which we'll use in order to enhance
// the session start event.
userCert, err := x509.ParseCertificate(certDER)
if err != nil {
return trace.Wrap(err, "the user certificate for RDP is invalid")
}
populateCertMetadata(startEvent.CertMetadata, userCert)
s.record(ctx, recorder, startEvent)
s.emit(ctx, startEvent)
err = rdpc.Run(ctx)
err = rdpc.Run(ctx, certDER, keyDER)
// ctx may have been canceled, so emit with a separate context
endEvent := audit.makeSessionEnd(recordSession)
@@ -873,6 +886,36 @@ func (s *WindowsService) connectRDP(ctx context.Context, log *slog.Logger, tdpCo
return trace.Wrap(err)
}
func populateCertMetadata(metadata *events.WindowsCertificateMetadata, cert *x509.Certificate) {
var enhancedKeyUsages []string
var upn string
for _, extension := range cert.Extensions {
if extension.Id.Equal(winpki.EnhancedKeyUsageExtensionOID) {
var oids []asn1.ObjectIdentifier
if _, err := asn1.Unmarshal(extension.Value, &oids); err == nil {
enhancedKeyUsages = make([]string, 0, len(oids))
for _, oid := range oids {
enhancedKeyUsages = append(enhancedKeyUsages, oid.String())
}
}
} else if extension.Id.Equal(winpki.SubjectAltNameExtensionOID) {
var san winpki.SubjectAltName[winpki.UPN]
if _, err := asn1.Unmarshal(extension.Value, &san); err == nil {
upn = san.OtherName.Value.Value
}
}
}
metadata.Subject = cert.Subject.String()
metadata.SerialNumber = cert.SerialNumber.String()
metadata.UPN = upn
metadata.CRLDistributionPoints = cert.CRLDistributionPoints
metadata.KeyUsage = int32(cert.KeyUsage)
metadata.ExtendedKeyUsage = slices.Map(cert.ExtKeyUsage, func(eku x509.ExtKeyUsage) int32 { return int32(eku) })
metadata.EnhancedKeyUsage = enhancedKeyUsages
}
func (s *WindowsService) makeTDPSendHandler(
ctx context.Context,
recorder libevents.SessionPreparerRecorder,
+1 -1
View File
@@ -1343,7 +1343,7 @@ func (c *CertificateRequest) CheckAndSetDefaults() error {
return trace.BadParameter("missing parameter PublicKey")
}
if c.Subject.CommonName == "" {
return trace.BadParameter("missing parameter Subject.Common name")
return trace.BadParameter("missing parameter Subject.CommonName")
}
if c.NotAfter.IsZero() {
return trace.BadParameter("missing parameter NotAfter")