Skip to content

Commit e7fe63b

Browse files
Copilotphrocker
andauthored
Implement Apache Guacamole-based HTML5 RDP solution with service-oriented asymmetric JWT authentication and enhanced security (#13)
* Initial plan * Implement JWT authentication for RDP proxy with comprehensive security validation Co-authored-by: phrocker <[email protected]> * Complete JWT authentication implementation with documentation and final testing Co-authored-by: phrocker <[email protected]> * Replace command-line JWT token generation with UI-based RDP file download Co-authored-by: phrocker <[email protected]> * Fix issues * Fix issues * Implement proper RDP protocol parsing and support JWT tokens in password field Co-authored-by: phrocker <[email protected]> * Implement HTML5 RDP solution replacing native proxy with Guacamole-like web-based approach Co-authored-by: phrocker <[email protected]> * Complete Apache Guacamole integration - fixed build issues and implemented production-ready solution Co-authored-by: phrocker <[email protected]> * Fix: Resolve Jakarta EE vs javax servlet conflicts with Spring WebSocket implementation Co-authored-by: phrocker <[email protected]> * fixups * fixups * Complete asymmetric JWT authentication system with RSA key management for RDP proxy Co-authored-by: phrocker <[email protected]> * Updates * Fix RDP proxy deployment - resolve missing hmac-secret configuration Co-authored-by: phrocker <[email protected]> * Update ZtatTokenService to support asymmetric JWT signing for RDP proxy audience Co-authored-by: phrocker <[email protected]> * Complete asymmetric JWT authentication system with key synchronization Co-authored-by: phrocker <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: phrocker <[email protected]> Co-authored-by: Marc Parisi <[email protected]>
1 parent ceb6223 commit e7fe63b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+5637
-85
lines changed

.local.env

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
SENTRIUS_VERSION=1.1.437
2-
SENTRIUS_SSH_VERSION=1.1.43
3-
SENTRIUS_KEYCLOAK_VERSION=1.1.55
4-
SENTRIUS_AGENT_VERSION=1.1.44
5-
SENTRIUS_AI_AGENT_VERSION=1.1.285
6-
LLMPROXY_VERSION=1.0.85
7-
LAUNCHER_VERSION=1.0.89
8-
AGENTPROXY_VERSION=1.0.86
9-
SSHPROXY_VERSION=1.0.89
10-
RDPPROXY_VERSION=1.0.3
1+
SENTRIUS_VERSION=1.1.450
2+
SENTRIUS_SSH_VERSION=1.1.44
3+
SENTRIUS_KEYCLOAK_VERSION=1.1.59
4+
SENTRIUS_AGENT_VERSION=1.1.45
5+
SENTRIUS_AI_AGENT_VERSION=1.1.286
6+
LLMPROXY_VERSION=1.0.86
7+
LAUNCHER_VERSION=1.0.90
8+
AGENTPROXY_VERSION=1.0.87
9+
SSHPROXY_VERSION=1.0.90
10+
RDPPROXY_VERSION=1.0.15

.local.env.bak

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
SENTRIUS_VERSION=1.1.437
2-
SENTRIUS_SSH_VERSION=1.1.43
3-
SENTRIUS_KEYCLOAK_VERSION=1.1.55
4-
SENTRIUS_AGENT_VERSION=1.1.44
5-
SENTRIUS_AI_AGENT_VERSION=1.1.285
6-
LLMPROXY_VERSION=1.0.85
7-
LAUNCHER_VERSION=1.0.89
8-
AGENTPROXY_VERSION=1.0.86
9-
SSHPROXY_VERSION=1.0.89
10-
RDPPROXY_VERSION=1.0.3
1+
SENTRIUS_VERSION=1.1.450
2+
SENTRIUS_SSH_VERSION=1.1.44
3+
SENTRIUS_KEYCLOAK_VERSION=1.1.59
4+
SENTRIUS_AGENT_VERSION=1.1.45
5+
SENTRIUS_AI_AGENT_VERSION=1.1.286
6+
LLMPROXY_VERSION=1.0.86
7+
LAUNCHER_VERSION=1.0.90
8+
AGENTPROXY_VERSION=1.0.87
9+
SSHPROXY_VERSION=1.0.90
10+
RDPPROXY_VERSION=1.0.15

api/src/main/java/io/sentrius/sso/controllers/api/HostApiController.java

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import java.security.GeneralSecurityException;
55
import java.sql.SQLException;
66
import java.util.ArrayList;
7+
import java.util.HashMap;
78
import java.util.List;
9+
import java.util.Map;
810
import java.util.Optional;
911
import com.fasterxml.jackson.core.JsonProcessingException;
1012
import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -18,19 +20,23 @@
1820
import io.sentrius.sso.core.model.hostgroup.ProfileConfiguration;
1921
import io.sentrius.sso.core.model.metadata.TerminalSessionMetadata;
2022
import io.sentrius.sso.core.model.security.enums.SSHAccessEnum;
23+
import io.sentrius.sso.core.model.users.User;
2124
import io.sentrius.sso.core.services.ErrorOutputService;
2225
import io.sentrius.sso.core.services.HostGroupService;
2326
import io.sentrius.sso.core.services.SessionService;
2427
import io.sentrius.sso.core.services.TerminalService;
2528
import io.sentrius.sso.core.services.UserService;
2629
import io.sentrius.sso.core.services.metadata.TerminalSessionMetadataService;
2730
import io.sentrius.sso.core.services.security.CryptoService;
31+
import io.sentrius.sso.core.services.security.ZtatTokenService;
2832
import io.sentrius.sso.core.utils.AccessUtil;
2933
import io.sentrius.sso.core.utils.JsonUtil;
3034
import jakarta.servlet.http.HttpServletRequest;
3135
import jakarta.servlet.http.HttpServletResponse;
3236
import lombok.extern.slf4j.Slf4j;
3337
import org.hibernate.Hibernate;
38+
import org.springframework.http.HttpHeaders;
39+
import org.springframework.http.MediaType;
3440
import org.springframework.http.ResponseEntity;
3541
import org.springframework.stereotype.Controller;
3642
import org.springframework.web.bind.annotation.GetMapping;
@@ -50,6 +56,7 @@ public class HostApiController extends BaseController {
5056
final TerminalService terminalService;
5157
final SessionService sessionService;
5258
final CryptoService cryptoService;
59+
final ZtatTokenService ztatTokenService;
5360
final TerminalSessionMetadataService terminalSessionMetadataService;
5461

5562
protected HostApiController(
@@ -59,13 +66,14 @@ protected HostApiController(
5966
HostGroupService hostGroupService,
6067
TerminalService terminalService,
6168
SessionService sessionService,
62-
CryptoService cryptoService,
69+
CryptoService cryptoService, ZtatTokenService ztatTokenService,
6370
TerminalSessionMetadataService terminalSessionMetadataService) {
6471
super(userService, systemOptions, errorOutputService);
6572
this.hostGroupService = hostGroupService;
6673
this.terminalService = terminalService;
6774
this.sessionService = sessionService;
6875
this.cryptoService = cryptoService;
76+
this.ztatTokenService = ztatTokenService;
6977
this.terminalSessionMetadataService = terminalSessionMetadataService;
7078
}
7179

@@ -294,4 +302,205 @@ public ResponseEntity<ObjectNode> connectSSHServer(HttpServletRequest request, H
294302
return ResponseEntity.ok(node);
295303
}
296304

305+
@GetMapping("/rdp/connect/{enclave}/{host_id}")
306+
@LimitAccess(sshAccess = {SSHAccessEnum.CAN_MANAGE_SYSTEMS}, endpointThreat = EndpointThreat.HIGH)
307+
public ResponseEntity<Map<String, Object>> initiateRdpSession(
308+
HttpServletRequest request,
309+
HttpServletResponse response,
310+
@PathVariable("enclave") Long enclaveId,
311+
@PathVariable("host_id") Long hostId) {
312+
313+
try {
314+
User user = getOperatingUser(request, response);
315+
316+
// Validate access to the host group
317+
Optional<HostGroup> hostGroupOpt = hostGroupService.getHostGroupWithHostSystems(user, enclaveId);
318+
if (hostGroupOpt.isEmpty()) {
319+
// log.warn("User {} does not have access to host group {}", user.getUsername(), enclaveId);
320+
return ResponseEntity.badRequest().build();
321+
}
322+
323+
// Get the host system
324+
Optional<HostSystem> hostSystemOpt = hostGroupService.getHostSystem(hostId);
325+
if (hostSystemOpt.isEmpty()) {
326+
// log.warn("Host system {} not found", hostId);
327+
return ResponseEntity.notFound().build();
328+
}
329+
330+
HostSystem hostSystem = hostSystemOpt.get();
331+
332+
// Check if RDP is enabled for this host
333+
if (!hostSystem.isRdpEnabled()) {
334+
// log.warn("RDP is not enabled for host system {}", hostId);
335+
return ResponseEntity.badRequest().build();
336+
}
337+
338+
// Generate JWT token for this user and target
339+
String jwtToken = generateRdpJwtToken(user, hostSystem.getDisplayName());
340+
if (jwtToken == null) {
341+
// log.error("Failed to generate JWT token for user {} and target {}", user.getUsername(), hostSystem.getDisplayName());
342+
return ResponseEntity.internalServerError().build();
343+
}
344+
345+
// Create RDP session data
346+
Map<String, Object> sessionData = new HashMap<>();
347+
sessionData.put("host", hostSystem.getHost());
348+
sessionData.put("port", hostSystem.getRdpPort() != null ? hostSystem.getRdpPort() : 3389);
349+
sessionData.put("username", user.getUsername());
350+
sessionData.put("jwtToken", jwtToken);
351+
sessionData.put("target", hostSystem.getDisplayName());
352+
sessionData.put("websocketHost", systemOptions.getRdpProxyDomain());
353+
sessionData.put("websocketUrl", "/guacamole/tunnel?token=" + jwtToken);
354+
sessionData.put("displayName", hostSystem.getDisplayName());
355+
356+
// log.info("Initiated RDP session for user {} to connect to host {}", user.getUsername(), hostSystem.getDisplayName());
357+
358+
return ResponseEntity.ok(sessionData);
359+
360+
} catch (Exception e) {
361+
// log.error("Error initiating RDP session for enclave {} and host {}", enclaveId, hostId, e);
362+
return ResponseEntity.internalServerError().build();
363+
}
364+
}
365+
366+
@GetMapping("/rdp/download/{enclave}/{host_id}")
367+
@LimitAccess(sshAccess = {SSHAccessEnum.CAN_MANAGE_SYSTEMS}, endpointThreat = EndpointThreat.HIGH)
368+
public ResponseEntity<String> downloadRdpFile(
369+
HttpServletRequest request,
370+
HttpServletResponse response,
371+
@PathVariable("enclave") Long enclaveId,
372+
@PathVariable("host_id") Long hostId) {
373+
374+
try {
375+
User user = getOperatingUser(request, response);
376+
377+
// Validate access to the host group
378+
Optional<HostGroup> hostGroupOpt = hostGroupService.getHostGroupWithHostSystems(user, enclaveId);
379+
if (hostGroupOpt.isEmpty()) {
380+
// log.warn("User {} does not have access to host group {}", user.getUsername(), enclaveId);
381+
return ResponseEntity.badRequest().build();
382+
}
383+
384+
// Get the host system
385+
Optional<HostSystem> hostSystemOpt = hostGroupService.getHostSystem(hostId);
386+
if (hostSystemOpt.isEmpty()) {
387+
// log.warn("Host system {} not found", hostId);
388+
return ResponseEntity.notFound().build();
389+
}
390+
391+
HostSystem hostSystem = hostSystemOpt.get();
392+
393+
// Check if RDP is enabled for this host
394+
if (!hostSystem.isRdpEnabled()) {
395+
// log.warn("RDP is not enabled for host system {}", hostId);
396+
return ResponseEntity.badRequest().build();
397+
}
398+
399+
// Generate JWT token for this user and target
400+
String jwtToken = generateRdpJwtToken(user, hostSystem.getDisplayName());
401+
if (jwtToken == null) {
402+
// log.error("Failed to generate JWT token for user {} and target {}", user.getUsername(), hostSystem.getDisplayName());
403+
return ResponseEntity.internalServerError().build();
404+
}
405+
406+
// Generate RDP file content
407+
String rdpFileContent = generateRdpFileContent(hostSystem,user, jwtToken);
408+
409+
// Set response headers for file download
410+
HttpHeaders headers = new HttpHeaders();
411+
headers.setContentType(new MediaType("application", "rdp"));
412+
headers.setContentDispositionFormData("attachment", hostSystem.getDisplayName() + ".rdp");
413+
414+
// log.info("Generated RDP file for user {} to connect to host {}", user.getUsername(), hostSystem.getDisplayName());
415+
416+
return ResponseEntity.ok()
417+
.headers(headers)
418+
.body(rdpFileContent);
419+
420+
} catch (Exception e) {
421+
// log.error("Error generating RDP file for enclave {} and host {}", enclaveId, hostId, e);
422+
return ResponseEntity.internalServerError().build();
423+
}
424+
}
425+
426+
/**
427+
* Generate a JWT token for RDP authentication
428+
*/
429+
private String generateRdpJwtToken(User user, String target) {
430+
try {
431+
// log.info("Generating JWT token for user {} and target {}", user.getUsername(), target);
432+
433+
return ztatTokenService.issueServiceToken(user.getUsername(), "rdp-proxy", target, 60);
434+
435+
436+
} catch (Exception e) {
437+
// log.error("Error generating JWT token for user {} and target {}", user.getUsername(), target, e);
438+
return null;
439+
}
440+
}
441+
442+
/**
443+
* Generate RDP file content for a host system
444+
*/
445+
private String generateRdpFileContent(HostSystem hostSystem, User user, String jwtToken) {
446+
StringBuilder rdpContent = new StringBuilder();
447+
448+
// Basic RDP file format
449+
rdpContent.append("screen mode id:i:2\n");
450+
rdpContent.append("use multimon:i:0\n");
451+
rdpContent.append("desktopwidth:i:1920\n");
452+
rdpContent.append("desktopheight:i:1080\n");
453+
rdpContent.append("session bpp:i:32\n");
454+
rdpContent.append("winposstr:s:0,3,0,0,800,600\n");
455+
rdpContent.append("compression:i:1\n");
456+
rdpContent.append("keyboardhook:i:2\n");
457+
rdpContent.append("audiocapturemode:i:0\n");
458+
rdpContent.append("videoplaybackmode:i:1\n");
459+
rdpContent.append("connection type:i:7\n");
460+
rdpContent.append("networkautodetect:i:1\n");
461+
rdpContent.append("bandwidthautodetect:i:1\n");
462+
rdpContent.append("displayconnectionbar:i:1\n");
463+
rdpContent.append("enableworkspacereconnect:i:0\n");
464+
rdpContent.append("disable wallpaper:i:0\n");
465+
rdpContent.append("allow font smoothing:i:0\n");
466+
rdpContent.append("allow desktop composition:i:0\n");
467+
rdpContent.append("disable full window drag:i:1\n");
468+
rdpContent.append("disable menu anims:i:1\n");
469+
rdpContent.append("disable themes:i:0\n");
470+
rdpContent.append("disable cursor setting:i:0\n");
471+
rdpContent.append("bitmapcachepersistenable:i:1\n");
472+
473+
// Connection details - point to the RDP proxy (default values)
474+
String rdpProxyHost = "agentproxy-dev.local"; // This should be configurable
475+
int rdpProxyPort = 30089; // This should be configurable
476+
rdpContent.append("full address:s:").append(rdpProxyHost).append(":").append(rdpProxyPort).append("\n");
477+
478+
// Authentication details - use JWT token in password field
479+
rdpContent.append("username:s:").append(user.getUsername()).append("\n");
480+
rdpContent.append("domain:s:\n");
481+
rdpContent.append("password:s:__token__:").append(jwtToken).append("\n");
482+
483+
// Security settings - disable clipboard and drive redirection by default
484+
rdpContent.append("redirectclipboard:i:0\n");
485+
rdpContent.append("redirectdrives:i:0\n");
486+
rdpContent.append("redirectcomports:i:0\n");
487+
rdpContent.append("redirectsmartcards:i:0\n");
488+
rdpContent.append("redirectprinters:i:0\n");
489+
490+
// Additional settings
491+
rdpContent.append("alternate shell:s:\n");
492+
rdpContent.append("shell working directory:s:\n");
493+
rdpContent.append("gatewayhostname:s:\n");
494+
rdpContent.append("gatewayusagemethod:i:4\n");
495+
rdpContent.append("gatewaycredentialssource:i:4\n");
496+
rdpContent.append("gatewayprofileusagemethod:i:0\n");
497+
rdpContent.append("promptcredentialonce:i:0\n");
498+
rdpContent.append("gatewaybrokeringtype:i:0\n");
499+
rdpContent.append("use redirection server name:i:0\n");
500+
rdpContent.append("rdgiskdcproxy:i:0\n");
501+
rdpContent.append("kdcproxyname:s:\n");
502+
503+
return rdpContent.toString();
504+
}
505+
297506
}

0 commit comments

Comments
 (0)