mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
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:
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -129,6 +129,7 @@ func TestSessionStartEvent(t *testing.T) {
|
||||
RemoteAddr: testDesktop.GetAddr(),
|
||||
Protocol: libevents.EventProtocolTDP,
|
||||
},
|
||||
CertMetadata: new(events.WindowsCertificateMetadata),
|
||||
Status: events.Status{
|
||||
Success: true,
|
||||
},
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user