Compare commits
12
Commits
v1.0.2
..
881587c42a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
881587c42a | ||
|
|
1bb7d1c680 | ||
|
|
fd5f651f2c | ||
|
|
b4317c9589 | ||
|
|
07f3e4cef9 | ||
|
|
7592bda57a | ||
|
|
47f496efcf | ||
|
|
4a2c3a8eb4 | ||
|
|
e9e7451a8e | ||
|
|
06c0cfbb78 | ||
|
|
37b0369361 | ||
|
|
81a974d715 |
@@ -0,0 +1,97 @@
|
|||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
RDP_NIX = Path(__file__).resolve().parents[2] / "modules" / "rdp.nix"
|
||||||
|
USERNAME_READ = "USERNAME=\"$(tr -d '\\n' < \"$USERNAME_FILE\")\""
|
||||||
|
USERNAME_LENGTH_GUARD = "if [ \"''${#USERNAME}\" -gt 32 ]; then"
|
||||||
|
SHORT_PASSWORD_GUARD = 'if [ "\'\'${#PASSWORD}" -lt 8 ]; then'
|
||||||
|
|
||||||
|
|
||||||
|
def _section(source: str, start: str, end: str) -> str:
|
||||||
|
start_idx = source.find(start)
|
||||||
|
if start_idx == -1:
|
||||||
|
raise AssertionError(f"Expected section start not found: {start!r}")
|
||||||
|
end_idx = source.find(end, start_idx)
|
||||||
|
if end_idx == -1:
|
||||||
|
raise AssertionError(f"Expected section end not found: {end!r}")
|
||||||
|
return source[start_idx:end_idx]
|
||||||
|
|
||||||
|
|
||||||
|
class RdpModuleBootSetupTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.source = RDP_NIX.read_text()
|
||||||
|
self.gnome_service = _section(
|
||||||
|
self.source,
|
||||||
|
"systemd.services.gnome-remote-desktop = {",
|
||||||
|
"systemd.tmpfiles.rules = [",
|
||||||
|
)
|
||||||
|
self.setup_service = _section(
|
||||||
|
self.source,
|
||||||
|
"systemd.services.gnome-remote-desktop-setup = {",
|
||||||
|
"};\n}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_does_not_redeclare_gnome_remote_desktop_user(self):
|
||||||
|
self.assertNotIn("users.users.gnome-remote-desktop", self.source)
|
||||||
|
self.assertNotIn("createHome = true;", self.source)
|
||||||
|
|
||||||
|
def test_main_service_requires_setup_before_starting(self):
|
||||||
|
self.assertIn('wantedBy = [ "graphical.target" ];', self.gnome_service)
|
||||||
|
self.assertIn('after = [ "gnome-remote-desktop-setup.service" ];', self.gnome_service)
|
||||||
|
self.assertIn('requires = [ "gnome-remote-desktop-setup.service" ];', self.gnome_service)
|
||||||
|
|
||||||
|
def test_setup_waits_for_configuration_service_and_bounded_timeout(self):
|
||||||
|
self.assertIn('wantedBy = [ "graphical.target" ];', self.setup_service)
|
||||||
|
self.assertIn('before = [ "gnome-remote-desktop.service" ];', self.setup_service)
|
||||||
|
self.assertIn('"dbus.service"', self.setup_service)
|
||||||
|
self.assertIn('"gnome-remote-desktop-configuration.service"', self.setup_service)
|
||||||
|
self.assertNotIn("RemainAfterExit", self.setup_service)
|
||||||
|
self.assertIn('TimeoutStartSec = "2min";', self.setup_service)
|
||||||
|
self.assertIn('timeout --kill-after=5s 10s', self.setup_service)
|
||||||
|
self.assertIn('echo "grdctl command timed out: $*" >&2', self.setup_service)
|
||||||
|
self.assertIn('echo "grdctl command failed (exit $rc): $*" >&2', self.setup_service)
|
||||||
|
|
||||||
|
def test_setup_runs_grdctl_directly_as_root(self):
|
||||||
|
# The oneshot service already runs as root; system-mode grdctl must be
|
||||||
|
# invoked directly so it does not attempt pkexec authorization.
|
||||||
|
self.assertIn('grdctl --system "$@"', self.setup_service)
|
||||||
|
self.assertNotIn("runuser", self.setup_service)
|
||||||
|
self.assertNotIn("pkexec", self.setup_service)
|
||||||
|
self.assertNotIn("sudo", self.setup_service)
|
||||||
|
|
||||||
|
def test_privilege_escalation_packages_absent_from_setup_path(self):
|
||||||
|
self.assertNotIn("pkgs.polkit", self.setup_service)
|
||||||
|
self.assertNotIn("pkgs.util-linux", self.setup_service)
|
||||||
|
|
||||||
|
def test_hub_files_are_the_source_of_truth_for_username_and_password(self):
|
||||||
|
self.assertIn('DEFAULT_USERNAME="sovran"', self.setup_service)
|
||||||
|
self.assertIn('if [ ! -f "$USERNAME_FILE" ]; then', self.setup_service)
|
||||||
|
self.assertIn(USERNAME_READ, self.setup_service)
|
||||||
|
self.assertIn(USERNAME_LENGTH_GUARD, self.setup_service)
|
||||||
|
self.assertIn('case "$USERNAME" in', self.setup_service)
|
||||||
|
self.assertIn('[A-Za-z_][A-Za-z0-9_-]*)', self.setup_service)
|
||||||
|
self.assertIn("RDP username is too long (''${#USERNAME} characters, maximum 32)", self.setup_service)
|
||||||
|
self.assertIn("RDP username must start with a letter or underscore and contain only letters, numbers, underscores, and hyphens", self.setup_service)
|
||||||
|
self.assertIn('if [ ! -f "$PASSWORD_FILE" ]; then', self.setup_service)
|
||||||
|
self.assertIn("tr -d '\\n'", self.setup_service)
|
||||||
|
self.assertIn('"$PASSWORD_FILE"', self.setup_service)
|
||||||
|
self.assertIn(SHORT_PASSWORD_GUARD, self.setup_service)
|
||||||
|
self.assertIn("RDP password is too short (''${#PASSWORD} characters, minimum 8)", self.setup_service)
|
||||||
|
self.assertIn('grdctl_system rdp set-credentials "$USERNAME" "$PASSWORD"', self.setup_service)
|
||||||
|
self.assertNotIn('grdctl --system rdp set-credentials sovran "$PASSWORD"', self.setup_service)
|
||||||
|
|
||||||
|
def test_secure_permissions_are_enforced_for_state_and_secret_files(self):
|
||||||
|
self.assertIn('"d /var/lib/gnome-remote-desktop/tls 0700', self.source)
|
||||||
|
self.assertIn("chmod 700", self.setup_service)
|
||||||
|
self.assertIn('chmod 600 "$USERNAME_FILE"', self.setup_service)
|
||||||
|
self.assertIn('chmod 600 "$PASSWORD_FILE"', self.setup_service)
|
||||||
|
self.assertIn('chmod 600 "$CRED_FILE"', self.setup_service)
|
||||||
|
self.assertIn('chmod 600 "$TLS_DIR/rdp-tls.key"', self.setup_service)
|
||||||
|
self.assertIn('chmod 644 "$TLS_DIR/rdp-tls.crt"', self.setup_service)
|
||||||
|
self.assertIn('LOCAL_IP="$(hostname -I | awk \'{print $1}\')"', self.setup_service)
|
||||||
|
self.assertIn('LOCAL_IP="127.0.0.1"', self.setup_service)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Generated
+15
-15
@@ -5,11 +5,11 @@
|
|||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783086783,
|
"lastModified": 1783519926,
|
||||||
"narHash": "sha256-NxXpNF/9tq2nI+SxFHUxjro3u11SF3l4vs7bawdMKkQ=",
|
"narHash": "sha256-2zwAN4lNitHFrHVnRZG3YcvpdtWOoF0cOBstxMeB1KI=",
|
||||||
"owner": "emmanuelrosa",
|
"owner": "emmanuelrosa",
|
||||||
"repo": "btc-clients-nix",
|
"repo": "btc-clients-nix",
|
||||||
"rev": "4f6d07cae877ef58f0fbc9e731c99800ddb80859",
|
"rev": "731a1e11c2fefb14f0aa4b1f03cfa85c19c28d71",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -139,11 +139,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782999065,
|
"lastModified": 1783856661,
|
||||||
"narHash": "sha256-5Dgj5+pIQYZKrXUGaLCk7CKfN3MmpwIhO94++WVxvng=",
|
"narHash": "sha256-ZGP04e+Q6WyQJGA9ZvI5CL6+heGQldbAG9U1T9NGvmU=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "80d591ed473cfc46329932c2aadac9b435342c7c",
|
"rev": "569d578509928497eddc3fdbf94a799027050be4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -187,11 +187,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_3": {
|
"nixpkgs_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782959384,
|
"lastModified": 1783776592,
|
||||||
"narHash": "sha256-xnJJk+ct+D2+wdRxj1wk36w5zV9RVESwRqcklPdt3fM=",
|
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "65179426c83bb3f6bc14898b42ea1c6f01d374b0",
|
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -203,11 +203,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_4": {
|
"nixpkgs_4": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1782948114,
|
"lastModified": 1783791668,
|
||||||
"narHash": "sha256-AXmz9ho4Lud5CsbrZsuSVwpQZ4o5FgZ1chxBn5cJ8+0=",
|
"narHash": "sha256-zbcZ1dmBTPfJ7Mlqh/yLEPGpgJnwuv4Xr1xucy2WqMA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9e92285f211dad236540fd617d7e30e0b99bc0e1",
|
"rev": "716c7a2664ca8325617b8a7fbb609273f2c4cae7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -224,11 +224,11 @@
|
|||||||
"systems": "systems_2"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783173302,
|
"lastModified": 1783941741,
|
||||||
"narHash": "sha256-nlnOw/zsD2H2NHSZ5oNWwcjuM17vipyAapfXsO78GjY=",
|
"narHash": "sha256-F+3M1IZrJa920cx2/k2AMKqedEodxLF7COJVkLJwUBo=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixvim",
|
"repo": "nixvim",
|
||||||
"rev": "a402fdf2a1ef297d8ea7c95b90d6af0dbe90ab11",
|
"rev": "e6715f01d9f56f07a27a01386b85ae22b06f0705",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -72,17 +72,30 @@ $MATRIX {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$ELEMENT_CALLING {
|
$ELEMENT_CALLING {
|
||||||
handle /livekit/jwt/sfu/get {
|
# Route all current lk-jwt-service authorization endpoints to port 8073,
|
||||||
|
# stripping the /livekit/jwt prefix that Caddy adds on the public URL.
|
||||||
|
@lk_jwt path /livekit/jwt/sfu/get* /livekit/jwt/get_token* /livekit/jwt/healthz* /livekit/jwt/sfu_webhook* /livekit/jwt/delegate_delayed_leave*
|
||||||
|
handle @lk_jwt {
|
||||||
uri strip_prefix /livekit/jwt
|
uri strip_prefix /livekit/jwt
|
||||||
reverse_proxy [::1]:8073 {
|
reverse_proxy [::1]:8073 {
|
||||||
header_up Host {host}
|
header_up Host {host}
|
||||||
header_up X-Forwarded-Server {host}
|
header_up X-Forwarded-Server {host}
|
||||||
header_up X-Real-IP {remote_host}
|
header_up X-Real-IP {remote_host}
|
||||||
header_up X-Forwarded-For {remote_host}
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handle {
|
handle {
|
||||||
reverse_proxy localhost:7880
|
reverse_proxy localhost:7880 {
|
||||||
|
header_up Host {host}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
header_up X-Real-IP {remote_host}
|
||||||
|
transport http {
|
||||||
|
read_timeout 300s
|
||||||
|
write_timeout 300s
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
@@ -94,8 +107,11 @@ EOF
|
|||||||
# * reads the matrix domain from /var/lib/domains/matrix (never hardcoded)
|
# * reads the matrix domain from /var/lib/domains/matrix (never hardcoded)
|
||||||
# * copies Caddy's already-issued matrix cert/key into /var/lib/livekit
|
# * copies Caddy's already-issued matrix cert/key into /var/lib/livekit
|
||||||
# so LoadCredential can stage them for the (DynamicUser) livekit unit
|
# so LoadCredential can stage them for the (DynamicUser) livekit unit
|
||||||
# * writes a complete LiveKit config (with turn.domain substituted) that the
|
# * detects the primary network interface from the IPv4 default route so
|
||||||
# overridden ExecStart loads.
|
# LiveKit only advertises real ICE candidates — not VPN/container/private
|
||||||
|
# addresses from interfaces like Tailscale or Docker bridges
|
||||||
|
# * writes a complete LiveKit config (with turn.domain and interface
|
||||||
|
# substituted) that the overridden ExecStart loads.
|
||||||
systemd.services.livekit-turn-setup = {
|
systemd.services.livekit-turn-setup = {
|
||||||
description = "Stage TURN cert and generate LiveKit runtime config from domain files";
|
description = "Stage TURN cert and generate LiveKit runtime config from domain files";
|
||||||
after = [ "caddy.service" "livekit-key-setup.service" ];
|
after = [ "caddy.service" "livekit-key-setup.service" ];
|
||||||
@@ -109,7 +125,7 @@ EOF
|
|||||||
unitConfig = {
|
unitConfig = {
|
||||||
ConditionPathExists = "/var/lib/domains/element-calling";
|
ConditionPathExists = "/var/lib/domains/element-calling";
|
||||||
};
|
};
|
||||||
path = [ pkgs.coreutils pkgs.findutils ];
|
path = [ pkgs.coreutils pkgs.findutils pkgs.iproute2 pkgs.gawk ];
|
||||||
script = ''
|
script = ''
|
||||||
MATRIX=$(cat /var/lib/domains/matrix)
|
MATRIX=$(cat /var/lib/domains/matrix)
|
||||||
|
|
||||||
@@ -123,16 +139,37 @@ EOF
|
|||||||
cp "$KEY" /var/lib/livekit/turn.key
|
cp "$KEY" /var/lib/livekit/turn.key
|
||||||
chmod 640 /var/lib/livekit/turn.crt /var/lib/livekit/turn.key
|
chmod 640 /var/lib/livekit/turn.crt /var/lib/livekit/turn.key
|
||||||
|
|
||||||
# Generate the full LiveKit config the daemon will load. turn.domain is
|
# Detect the primary network interface from the IPv4 default route.
|
||||||
# only known at runtime, so it is substituted here. The cert/key paths
|
# Restricting LiveKit to this single interface prevents it from
|
||||||
# point at the LoadCredential-staged copies under /run/credentials.
|
# advertising VPN/container/private ICE candidates (e.g. Tailscale,
|
||||||
|
# Docker bridges) that remote peers cannot reach, which causes all
|
||||||
|
# ICE negotiation attempts to fail with responsesReceived: 0.
|
||||||
|
IFACE=$(ip -4 route show default | awk '/^default/ { for(i=1;i<=NF;i++) if($i=="dev" && (i+1)<=NF) { print $(i+1); exit } }')
|
||||||
|
if [ -z "$IFACE" ]; then
|
||||||
|
echo "ERROR: Could not detect a default-route network interface from 'ip -4 route show default'." >&2
|
||||||
|
echo "ERROR: Cannot generate a valid LiveKit config without a real interface to bind ICE candidates to." >&2
|
||||||
|
echo "ERROR: Ensure a default IPv4 route is configured, e.g.: ip route add default via <gateway> dev <interface>" >&2
|
||||||
|
echo "ERROR: Inspect the current routing table with: ip -4 route show" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Detected primary network interface: $IFACE"
|
||||||
|
|
||||||
|
# Generate the full LiveKit config the daemon will load. turn.domain and
|
||||||
|
# rtc.interfaces.includes are only known at runtime, so they are
|
||||||
|
# substituted here. The cert/key paths point at the LoadCredential-staged
|
||||||
|
# copies under /run/credentials.
|
||||||
cat > /run/livekit/livekit.yaml <<EOF
|
cat > /run/livekit/livekit.yaml <<EOF
|
||||||
port: 7880
|
port: 7880
|
||||||
rtc:
|
rtc:
|
||||||
use_external_ip: true
|
use_external_ip: true
|
||||||
|
skip_external_ip_validation: true
|
||||||
|
tcp_port: 7881
|
||||||
udp_port: 7882
|
udp_port: 7882
|
||||||
port_range_start: 30000
|
port_range_start: 30000
|
||||||
port_range_end: 40000
|
port_range_end: 40000
|
||||||
|
interfaces:
|
||||||
|
includes:
|
||||||
|
- $IFACE
|
||||||
room:
|
room:
|
||||||
auto_create: false
|
auto_create: false
|
||||||
turn:
|
turn:
|
||||||
@@ -155,6 +192,8 @@ EOF
|
|||||||
keyFile = livekitKeyFile;
|
keyFile = livekitKeyFile;
|
||||||
settings = {
|
settings = {
|
||||||
rtc.use_external_ip = true;
|
rtc.use_external_ip = true;
|
||||||
|
rtc.skip_external_ip_validation = true;
|
||||||
|
rtc.tcp_port = 7881;
|
||||||
rtc.udp_port = 7882;
|
rtc.udp_port = 7882;
|
||||||
rtc.port_range_start = 30000;
|
rtc.port_range_start = 30000;
|
||||||
rtc.port_range_end = 40000;
|
rtc.port_range_end = 40000;
|
||||||
@@ -186,6 +225,9 @@ EOF
|
|||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
|
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
|
||||||
networking.firewall.allowedUDPPorts = [ 3478 7882 ];
|
networking.firewall.allowedUDPPorts = [ 3478 7882 ];
|
||||||
|
networking.firewall.allowedUDPPortRanges = [
|
||||||
|
{ from = 30000; to = 40000; } # LiveKit internal TURN relay range
|
||||||
|
];
|
||||||
|
|
||||||
####### JWT SERVICE RUNTIME CONFIG #######
|
####### JWT SERVICE RUNTIME CONFIG #######
|
||||||
systemd.services.lk-jwt-service-runtime-config = {
|
systemd.services.lk-jwt-service-runtime-config = {
|
||||||
@@ -204,11 +246,13 @@ EOF
|
|||||||
path = [ pkgs.coreutils ];
|
path = [ pkgs.coreutils ];
|
||||||
script = ''
|
script = ''
|
||||||
ELEMENT_CALLING=$(cat /var/lib/domains/element-calling)
|
ELEMENT_CALLING=$(cat /var/lib/domains/element-calling)
|
||||||
|
MATRIX=$(cat /var/lib/domains/matrix)
|
||||||
|
|
||||||
mkdir -p /run/lk-jwt-service
|
mkdir -p /run/lk-jwt-service
|
||||||
|
|
||||||
cat > /run/lk-jwt-service/env <<EOF
|
cat > /run/lk-jwt-service/env <<EOF
|
||||||
LIVEKIT_URL=wss://$ELEMENT_CALLING
|
LIVEKIT_URL=wss://$ELEMENT_CALLING
|
||||||
|
LIVEKIT_FULL_ACCESS_HOMESERVERS=$MATRIX
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 640 /run/lk-jwt-service/env
|
chmod 640 /run/lk-jwt-service/env
|
||||||
|
|||||||
Executable → Regular
+105
-56
@@ -2,70 +2,97 @@
|
|||||||
|
|
||||||
lib.mkIf config.sovran_systemsOS.features.rdp {
|
lib.mkIf config.sovran_systemsOS.features.rdp {
|
||||||
|
|
||||||
users.users.gnome-remote-desktop = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = "gnome-remote-desktop";
|
|
||||||
home = "/var/lib/gnome-remote-desktop";
|
|
||||||
createHome = true;
|
|
||||||
};
|
|
||||||
users.groups.gnome-remote-desktop = {};
|
|
||||||
|
|
||||||
# Enable the GNOME Remote Desktop service at the system level
|
# Enable the GNOME Remote Desktop service at the system level
|
||||||
services.gnome.gnome-remote-desktop.enable = true;
|
services.gnome.gnome-remote-desktop.enable = true;
|
||||||
|
|
||||||
# Open RDP port in the firewall
|
# Open RDP port in the firewall
|
||||||
networking.firewall.allowedTCPPorts = [ 3389 ];
|
networking.firewall.allowedTCPPorts = [ 3389 ];
|
||||||
|
|
||||||
# Ensure the service actually starts and waits for setup to complete
|
# Ensure the service only starts after setup succeeds
|
||||||
systemd.services.gnome-remote-desktop = {
|
systemd.services.gnome-remote-desktop = {
|
||||||
wantedBy = [ "graphical.target" ];
|
wantedBy = [ "graphical.target" ];
|
||||||
after = [ "gnome-remote-desktop-setup.service" ];
|
after = [ "gnome-remote-desktop-setup.service" ];
|
||||||
wants = [ "gnome-remote-desktop-setup.service" ];
|
requires = [ "gnome-remote-desktop-setup.service" ];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d /var/lib/gnome-remote-desktop 0750 gnome-remote-desktop gnome-remote-desktop -"
|
"d /var/lib/gnome-remote-desktop/.local 0700 gnome-remote-desktop gnome-remote-desktop -"
|
||||||
"d /var/lib/gnome-remote-desktop/.local 0750 gnome-remote-desktop gnome-remote-desktop -"
|
"d /var/lib/gnome-remote-desktop/.local/share 0700 gnome-remote-desktop gnome-remote-desktop -"
|
||||||
"d /var/lib/gnome-remote-desktop/.local/share 0750 gnome-remote-desktop gnome-remote-desktop -"
|
"d /var/lib/gnome-remote-desktop/.local/share/gnome-remote-desktop 0700 gnome-remote-desktop gnome-remote-desktop -"
|
||||||
"d /var/lib/gnome-remote-desktop/.local/share/gnome-remote-desktop 0750 gnome-remote-desktop gnome-remote-desktop -"
|
"d /var/lib/gnome-remote-desktop/tls 0700 gnome-remote-desktop gnome-remote-desktop -"
|
||||||
];
|
];
|
||||||
|
|
||||||
systemd.services.gnome-remote-desktop-setup = {
|
systemd.services.gnome-remote-desktop-setup = {
|
||||||
description = "Configure GNOME Remote Desktop RDP";
|
description = "Configure GNOME Remote Desktop RDP";
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "graphical.target" ];
|
||||||
before = [ "gnome-remote-desktop.service" ];
|
before = [ "gnome-remote-desktop.service" ];
|
||||||
after = [ "systemd-tmpfiles-setup.service" "network-online.target" ];
|
after = [
|
||||||
wants = [ "network-online.target" ];
|
"dbus.service"
|
||||||
|
"systemd-tmpfiles-setup.service"
|
||||||
|
"network-online.target"
|
||||||
|
"gnome-remote-desktop-configuration.service"
|
||||||
|
];
|
||||||
|
wants = [
|
||||||
|
"network-online.target"
|
||||||
|
"gnome-remote-desktop-configuration.service"
|
||||||
|
];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "oneshot";
|
Type = "oneshot";
|
||||||
RemainAfterExit = true;
|
TimeoutStartSec = "2min";
|
||||||
};
|
};
|
||||||
path = [
|
path = [
|
||||||
pkgs.gnome-remote-desktop
|
pkgs.coreutils
|
||||||
pkgs.polkit
|
|
||||||
pkgs.openssl
|
|
||||||
pkgs.hostname
|
|
||||||
pkgs.gawk
|
pkgs.gawk
|
||||||
|
pkgs.gnome-remote-desktop
|
||||||
|
pkgs.hostname
|
||||||
|
pkgs.openssl
|
||||||
|
pkgs.systemd
|
||||||
];
|
];
|
||||||
script = ''
|
script = ''
|
||||||
# Ensure directory structure exists
|
set -euo pipefail
|
||||||
mkdir -p /var/lib/gnome-remote-desktop/.local/share/gnome-remote-desktop
|
|
||||||
chown -R gnome-remote-desktop:gnome-remote-desktop /var/lib/gnome-remote-desktop
|
|
||||||
|
|
||||||
TLS_DIR="/var/lib/gnome-remote-desktop/tls"
|
STATE_DIR="/var/lib/gnome-remote-desktop"
|
||||||
CRED_FILE="/var/lib/gnome-remote-desktop/rdp-credentials"
|
TLS_DIR="$STATE_DIR/tls"
|
||||||
|
USERNAME_FILE="$STATE_DIR/rdp-username"
|
||||||
|
PASSWORD_FILE="$STATE_DIR/rdp-password"
|
||||||
|
CRED_FILE="$STATE_DIR/rdp-credentials"
|
||||||
|
DEFAULT_USERNAME="sovran"
|
||||||
|
|
||||||
|
grdctl_system() {
|
||||||
|
local rc=0
|
||||||
|
|
||||||
|
if timeout --kill-after=5s 10s \
|
||||||
|
grdctl --system "$@"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$rc" -eq 124 ] || [ "$rc" -eq 137 ]; then
|
||||||
|
echo "grdctl command timed out: $*" >&2
|
||||||
|
fi
|
||||||
|
echo "grdctl command failed (exit $rc): $*" >&2
|
||||||
|
|
||||||
|
return "$rc"
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR/.local/share/gnome-remote-desktop" "$TLS_DIR"
|
||||||
|
chown -R gnome-remote-desktop:gnome-remote-desktop "$STATE_DIR"
|
||||||
|
chmod 700 \
|
||||||
|
"$STATE_DIR" \
|
||||||
|
"$STATE_DIR/.local" \
|
||||||
|
"$STATE_DIR/.local/share" \
|
||||||
|
"$STATE_DIR/.local/share/gnome-remote-desktop" \
|
||||||
|
"$TLS_DIR"
|
||||||
|
|
||||||
# Regenerate TLS certificate if missing OR if ownership is wrong
|
|
||||||
# (disable/re-enable cycle can break ownership or grdctl state)
|
|
||||||
NEED_REGEN=0
|
NEED_REGEN=0
|
||||||
if [ ! -f "$TLS_DIR/rdp-tls.crt" ] || [ ! -f "$TLS_DIR/rdp-tls.key" ]; then
|
if [ ! -f "$TLS_DIR/rdp-tls.crt" ] || [ ! -f "$TLS_DIR/rdp-tls.key" ]; then
|
||||||
NEED_REGEN=1
|
NEED_REGEN=1
|
||||||
elif [ "$(stat -c '%U' "$TLS_DIR/rdp-tls.key" 2>/dev/null)" != "gnome-remote-desktop" ]; then
|
elif [ "$(stat -c '%U:%G' "$TLS_DIR/rdp-tls.key" 2>/dev/null)" != "gnome-remote-desktop:gnome-remote-desktop" ]; then
|
||||||
NEED_REGEN=1
|
NEED_REGEN=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$NEED_REGEN" = "1" ]; then
|
if [ "$NEED_REGEN" = "1" ]; then
|
||||||
mkdir -p "$TLS_DIR"
|
|
||||||
rm -f "$TLS_DIR/rdp-tls.key" "$TLS_DIR/rdp-tls.crt"
|
rm -f "$TLS_DIR/rdp-tls.key" "$TLS_DIR/rdp-tls.crt"
|
||||||
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
|
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
|
||||||
-sha256 -nodes -days 3650 \
|
-sha256 -nodes -days 3650 \
|
||||||
@@ -75,39 +102,59 @@ lib.mkIf config.sovran_systemsOS.features.rdp {
|
|||||||
echo "Generated new RDP TLS certificate"
|
echo "Generated new RDP TLS certificate"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Always fix ownership and permissions (handles re-enable after disable)
|
chown gnome-remote-desktop:gnome-remote-desktop "$TLS_DIR/rdp-tls.key" "$TLS_DIR/rdp-tls.crt"
|
||||||
chown -R gnome-remote-desktop:gnome-remote-desktop "$TLS_DIR"
|
|
||||||
chmod 600 "$TLS_DIR/rdp-tls.key"
|
chmod 600 "$TLS_DIR/rdp-tls.key"
|
||||||
chmod 644 "$TLS_DIR/rdp-tls.crt"
|
chmod 644 "$TLS_DIR/rdp-tls.crt"
|
||||||
|
|
||||||
# Configure TLS certificate
|
if [ ! -f "$USERNAME_FILE" ]; then
|
||||||
grdctl --system rdp set-tls-cert "$TLS_DIR/rdp-tls.crt"
|
printf '%s\n' "$DEFAULT_USERNAME" > "$USERNAME_FILE"
|
||||||
grdctl --system rdp set-tls-key "$TLS_DIR/rdp-tls.key"
|
fi
|
||||||
|
USERNAME="$(tr -d '\n' < "$USERNAME_FILE")"
|
||||||
|
if [ -z "$USERNAME" ]; then
|
||||||
|
USERNAME="$DEFAULT_USERNAME"
|
||||||
|
printf '%s\n' "$USERNAME" > "$USERNAME_FILE"
|
||||||
|
fi
|
||||||
|
if [ "''${#USERNAME}" -gt 32 ]; then
|
||||||
|
echo "RDP username is too long (''${#USERNAME} characters, maximum 32): $USERNAME from $USERNAME_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
case "$USERNAME" in
|
||||||
|
[A-Za-z_][A-Za-z0-9_-]*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "RDP username must start with a letter or underscore and contain only letters, numbers, underscores, and hyphens: $USERNAME from $USERNAME_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
chown gnome-remote-desktop:gnome-remote-desktop "$USERNAME_FILE"
|
||||||
|
chmod 600 "$USERNAME_FILE"
|
||||||
|
|
||||||
# Generate password on first boot only
|
if [ ! -f "$PASSWORD_FILE" ]; then
|
||||||
PASSWORD=""
|
openssl rand -base64 16 > "$PASSWORD_FILE"
|
||||||
if [ ! -f /var/lib/gnome-remote-desktop/rdp-password ]; then
|
fi
|
||||||
PASSWORD=$(openssl rand -base64 16)
|
PASSWORD="$(tr -d '\n' < "$PASSWORD_FILE")"
|
||||||
echo "$PASSWORD" > /var/lib/gnome-remote-desktop/rdp-password
|
if [ -z "$PASSWORD" ]; then
|
||||||
chmod 600 /var/lib/gnome-remote-desktop/rdp-password
|
echo "RDP password file is empty: $PASSWORD_FILE" >&2
|
||||||
else
|
exit 1
|
||||||
PASSWORD=$(cat /var/lib/gnome-remote-desktop/rdp-password)
|
fi
|
||||||
|
if [ "''${#PASSWORD}" -lt 8 ]; then
|
||||||
|
echo "RDP password is too short (''${#PASSWORD} characters, minimum 8): $PASSWORD_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chown gnome-remote-desktop:gnome-remote-desktop "$PASSWORD_FILE"
|
||||||
|
chmod 600 "$PASSWORD_FILE"
|
||||||
|
|
||||||
|
LOCAL_IP="$(hostname -I | awk '{print $1}')"
|
||||||
|
if [ -z "$LOCAL_IP" ]; then
|
||||||
|
LOCAL_IP="127.0.0.1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Write username to a separate file for the hub
|
|
||||||
echo "sovran" > /var/lib/gnome-remote-desktop/rdp-username
|
|
||||||
chmod 600 /var/lib/gnome-remote-desktop/rdp-username
|
|
||||||
|
|
||||||
# Get current IP address
|
|
||||||
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
|
||||||
|
|
||||||
# Always rewrite the credentials file with the current IP
|
|
||||||
cat > "$CRED_FILE" <<EOF
|
cat > "$CRED_FILE" <<EOF
|
||||||
========================================
|
========================================
|
||||||
GNOME Remote Desktop (RDP) Credentials
|
GNOME Remote Desktop (RDP) Credentials
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
Username: sovran
|
Username: $USERNAME
|
||||||
Password: $PASSWORD
|
Password: $PASSWORD
|
||||||
|
|
||||||
Connect from any RDP client to:
|
Connect from any RDP client to:
|
||||||
@@ -116,11 +163,13 @@ lib.mkIf config.sovran_systemsOS.features.rdp {
|
|||||||
========================================
|
========================================
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
chown gnome-remote-desktop:gnome-remote-desktop "$CRED_FILE"
|
||||||
chmod 600 "$CRED_FILE"
|
chmod 600 "$CRED_FILE"
|
||||||
|
|
||||||
# Enable RDP backend and set credentials
|
grdctl_system rdp enable
|
||||||
grdctl --system rdp enable
|
grdctl_system rdp set-tls-cert "$TLS_DIR/rdp-tls.crt"
|
||||||
grdctl --system rdp set-credentials sovran "$PASSWORD"
|
grdctl_system rdp set-tls-key "$TLS_DIR/rdp-tls.key"
|
||||||
|
grdctl_system rdp set-credentials "$USERNAME" "$PASSWORD"
|
||||||
|
|
||||||
echo "GNOME Remote Desktop RDP configured successfully"
|
echo "GNOME Remote Desktop RDP configured successfully"
|
||||||
'';
|
'';
|
||||||
|
|||||||
Reference in New Issue
Block a user