fix: keep RDP creds out of tunnel URL/logs via single-use encrypted stash

This commit is contained in:
2026-07-17 11:28:00 +01:00
parent 138f708a87
commit 332c7760ca
4 changed files with 72 additions and 5 deletions
+16 -4
View File
@@ -44,6 +44,13 @@ func consoleConnect(c *gin.Context) {
return
}
if body.Protocol == "rdp" && (body.RDPUsername != "" || body.RDPPassword != "") {
if err := services.StashConsoleRDPCreds(sess.SessionID, body.RDPUsername, body.RDPPassword); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
services.LogEvent("console.opened", actorFromCtx(c), srv.ServerID, "",
"console session opened ("+body.Protocol+")")
@@ -83,10 +90,15 @@ func consoleTunnel(c *gin.Context) {
}
}
// RDP creds are single-use, passed via the connect step into the session
// document is avoided; instead they are re-supplied here as query params
// over the already-authenticated WS token. For ssh they are empty.
gp, err := services.BuildGuacParams(srv, sess.Protocol, privKey, c.Query("u"), c.Query("p"))
var rdpUser, rdpPass string
if sess.Protocol == "rdp" {
rdpUser, rdpPass, err = services.ConsumeConsoleRDPCreds(sessionID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not load credentials"})
return
}
}
gp, err := services.BuildGuacParams(srv, sess.Protocol, privKey, rdpUser, rdpPass)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
@@ -16,4 +16,7 @@ type ConsoleSession struct {
StartedAt time.Time `bson:"started_at" json:"started_at"`
EndedAt *time.Time `bson:"ended_at,omitempty" json:"ended_at,omitempty"`
ClientIP string `bson:"client_ip,omitempty" json:"client_ip,omitempty"`
RDPUserEnc string `bson:"rdp_user_enc,omitempty" json:"-"`
RDPPassEnc string `bson:"rdp_pass_enc,omitempty" json:"-"`
}
+50
View File
@@ -150,6 +150,56 @@ func GetConsoleSession(sessionID string) (*models.ConsoleSession, error) {
return &s, nil
}
// StashConsoleRDPCreds encrypts and stores single-use RDP credentials on the
// session document. They are consumed (and cleared) when the tunnel opens.
func StashConsoleRDPCreds(sessionID, username, password string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
u, err := encryptString(username)
if err != nil {
return err
}
p, err := encryptString(password)
if err != nil {
return err
}
_, err = db.Col("console_sessions").UpdateOne(ctx,
bson.M{"session_id": sessionID},
bson.M{"$set": bson.M{"rdp_user_enc": u, "rdp_pass_enc": p}},
)
return err
}
// ConsumeConsoleRDPCreds decrypts and returns the stored RDP credentials, then
// clears them from the session document (single-use). Returns empty strings if
// none were stored.
func ConsumeConsoleRDPCreds(sessionID string) (username, password string, err error) {
s, err := GetConsoleSession(sessionID)
if err != nil {
return "", "", err
}
if s.RDPUserEnc == "" && s.RDPPassEnc == "" {
return "", "", nil
}
if s.RDPUserEnc != "" {
if username, err = decryptString(s.RDPUserEnc); err != nil {
return "", "", err
}
}
if s.RDPPassEnc != "" {
if password, err = decryptString(s.RDPPassEnc); err != nil {
return "", "", err
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _ = db.Col("console_sessions").UpdateOne(ctx,
bson.M{"session_id": sessionID},
bson.M{"$unset": bson.M{"rdp_user_enc": "", "rdp_pass_enc": ""}},
)
return username, password, nil
}
func EndConsoleSession(sessionID string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()