wip(remote-desktop): scaffold gateway rustguac + kasmvnc + rustdesk + vault (dev)
Modules NixOS pour une gateway d'accès distant multi-protocole : rustguac (remplacement Guacamole, OIDC, audit, enregistrement), kasmvnc (bureau Wayland LXQt), rustdesk (relais P2P hbbs/hbbr), vault-rustguac (Vault en mode dev pour stocker les identifiants de connexion des deux premiers), plus les vhosts nginx associés. Importés dans services/default.nix mais NON activés sur aucun profil (enable reste false partout) : domaines (guac.example.com, auth.example.com), token Vault dev et secret OIDC sont encore des placeholders TODO dans les modules, à finir avant tout services.*.enable = true.
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.kasmvnc;
|
||||
|
||||
# Fetch KasmVNC from GitHub Releases (not in nixpkgs)
|
||||
kasmvncPackage = pkgs.stdenv.mkDerivation rec {
|
||||
pname = "kasmvnc";
|
||||
version = "1.5.0";
|
||||
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://github.com/kasmtech/KasmVNC/releases/download/v${version}/kasmvncserver_noble_${version}_amd64.deb";
|
||||
sha256 = "sha256-9Zn+AuIXW5gXthZfdKXSvr3HMRjd6Rgbo0EJY77Xrh4=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ pkgs.dpkg pkgs.patchelf ];
|
||||
buildInputs = [
|
||||
pkgs.glibc pkgs.gcc pkgs.libx11 pkgs.libxcb pkgs.libxkbcommon
|
||||
pkgs.wayland pkgs.libva pkgs.libdrm pkgs.mesa
|
||||
pkgs.openssl pkgs.zlib pkgs.libjpeg_turbo pkgs.libpng pkgs.libwebp
|
||||
];
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
installPhase = ''
|
||||
dpkg -x $src $out
|
||||
# The deb extracts to root/usr/..., so move to correct location
|
||||
if [ -d $out/root ]; then
|
||||
mv $out/root/* $out/
|
||||
rmdir $out/root
|
||||
fi
|
||||
# Check if binary is ELF before patchelf
|
||||
if file $out/usr/bin/kasmvncserver | grep -q ELF; then
|
||||
patchelf --set-rpath "$out/usr/lib/x86_64-linux-gnu:$out/lib" $out/usr/bin/kasmvncserver
|
||||
patchelf --set-rpath "$out/usr/lib/x86_64-linux-gnu:$out/lib" $out/usr/bin/kasmvncpasswd
|
||||
fi
|
||||
# Move binaries to $out/bin
|
||||
mkdir -p $out/bin
|
||||
ln -s $out/usr/bin/kasmvncserver $out/bin/kasmvncserver
|
||||
ln -s $out/usr/bin/kasmvncpasswd $out/bin/kasmvncpasswd
|
||||
ln -s $out/usr/bin/kasmvncconfig $out/bin/kasmvncconfig
|
||||
'';
|
||||
};
|
||||
|
||||
# LXQt + kwin_wayland packages
|
||||
lxqtPkgs = with pkgs; [
|
||||
lxqt.lxqt-session
|
||||
lxqt.lxqt-panel
|
||||
lxqt.lxqt-runner
|
||||
lxqt.lxqt-notificationd
|
||||
lxqt.lxqt-globalkeys
|
||||
lxqt.lxqt-powermanagement
|
||||
lxqt.lxqt-config
|
||||
lxqt.lxqt-about
|
||||
lxqt.lxqt-admin
|
||||
lxqt.lxqt-sudo
|
||||
lxqt.lxqt-openssh-askpass
|
||||
lxqt.lxqt-policykit
|
||||
kdePackages.kwin
|
||||
wayland
|
||||
xwayland
|
||||
qt6.qtwayland
|
||||
kdePackages.breeze
|
||||
kdePackages.breeze-icons
|
||||
kdePackages.konsole
|
||||
pcmanfm-qt
|
||||
];
|
||||
|
||||
# Generate kasmvnc.yaml config
|
||||
generateConfig = ''
|
||||
server:
|
||||
http:
|
||||
headers:
|
||||
- "Cross-Origin-Embedder-Policy=require-corp"
|
||||
- "Cross-Origin-Opener-Policy=same-origin"
|
||||
httpd_directory: ${kasmvncPackage}/usr/share/kasmvnc/www
|
||||
network:
|
||||
protocol: https
|
||||
interface: "${cfg.bindAddress}"
|
||||
port: ${toString cfg.port}
|
||||
websocket_port: auto
|
||||
use_ipv4: true
|
||||
use_ipv6: true
|
||||
udp:
|
||||
public_ip: auto
|
||||
port: auto
|
||||
stun_server: "stun.l.google.com:19302"
|
||||
ssl:
|
||||
pem_certificate: "${cfg.tls.certFile}"
|
||||
pem_key: "${cfg.tls.keyFile}"
|
||||
require_ssl: true
|
||||
desktop:
|
||||
resolution:
|
||||
width: ${toString (builtins.elemAt (builtins.split "x" cfg.geometry) 0)}
|
||||
height: ${toString (builtins.elemAt (builtins.split "x" cfg.geometry) 1)}
|
||||
allow_resize: true
|
||||
pixel_depth: 24
|
||||
gpu:
|
||||
hw3d: true
|
||||
drinode: "/dev/dri/renderD128"
|
||||
encoding:
|
||||
prefer_h264: true
|
||||
prefer_hevc: true
|
||||
prefer_av1: true
|
||||
prefer_vp9: true
|
||||
prefer_vp8: true
|
||||
lossless: false
|
||||
webp_quality: 80
|
||||
jpeg_quality: 85
|
||||
security:
|
||||
brute_force_protection:
|
||||
enabled: true
|
||||
blacklist_threshold: 5
|
||||
blacklist_timeout: 300
|
||||
advanced:
|
||||
kasm_password_file: "${cfg.passwordFile}"
|
||||
x_authority_file: auto
|
||||
auto_shutdown: false
|
||||
no_user_session_timeout: "never"
|
||||
active_user_session_timeout: "never"
|
||||
inactive_user_session_timeout: "never"
|
||||
user_session:
|
||||
new_session_disconnects_existing_exclusive_session: false
|
||||
concurrent_connections_prompt: false
|
||||
concurrent_connections_prompt_timeout: 10
|
||||
idle_timeout: "never"
|
||||
runtime_configuration:
|
||||
allow_client_to_override_kasm_server_settings: true
|
||||
allow_override_standard_vnc_server_settings: true
|
||||
allow_override_list:
|
||||
- "pointer.enabled"
|
||||
- "data_loss_prevention.clipboard.server_to_client.enabled"
|
||||
- "data_loss_prevention.clipboard.client_to_server.enabled"
|
||||
'';
|
||||
in {
|
||||
|
||||
options.services.kasmvnc = {
|
||||
enable = mkEnableOption "KasmVNC - Modern Web-native VNC Server";
|
||||
|
||||
# User to run as (systemd user service)
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "marc";
|
||||
description = "User to run KasmVNC (systemd user service)";
|
||||
};
|
||||
|
||||
# Display number
|
||||
display = mkOption {
|
||||
type = types.str;
|
||||
default = ":1";
|
||||
description = "X/Wayland display number";
|
||||
};
|
||||
|
||||
# Geometry
|
||||
geometry = mkOption {
|
||||
type = types.str;
|
||||
default = "1920x1080";
|
||||
description = "Desktop resolution (WIDTHxHEIGHT)";
|
||||
};
|
||||
|
||||
# Bind address and port
|
||||
bindAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "0.0.0.0";
|
||||
description = "Bind address for HTTPS/WebSocket";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 443;
|
||||
description = "HTTPS port";
|
||||
};
|
||||
|
||||
# TLS certificates (required)
|
||||
tls = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
certFile = mkOption { type = types.path; description = "Path to TLS certificate (PEM)"; };
|
||||
keyFile = mkOption { type = types.path; description = "Path to TLS private key (PEM)"; };
|
||||
};
|
||||
};
|
||||
description = "TLS configuration (required for HTTPS/WSS)";
|
||||
};
|
||||
|
||||
# Password file (basic auth)
|
||||
passwordFile = mkOption {
|
||||
type = types.path;
|
||||
default = "/home/${cfg.user}/.kasmpasswd";
|
||||
description = "Password file generated by kasmvncpasswd";
|
||||
};
|
||||
|
||||
# Desktop environment
|
||||
desktop = mkOption {
|
||||
type = types.enum [ "lxqt-wayland" "plasma-wayland" "xfce" "custom" ];
|
||||
default = "lxqt-wayland";
|
||||
description = "Desktop environment to launch";
|
||||
};
|
||||
|
||||
# Custom command for desktop=custom
|
||||
customCommand = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Custom command to launch (when desktop=custom)";
|
||||
};
|
||||
|
||||
# Extra packages for desktop
|
||||
extraPackages = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = [ ];
|
||||
description = "Extra packages to install for the desktop";
|
||||
};
|
||||
|
||||
# Extra YAML config (merged)
|
||||
extraYaml = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Extra YAML config merged into kasmvnc.yaml";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# User setup
|
||||
users.users.${cfg.user}.extraGroups = [ "video" "render" "seat" "ssl-cert" ];
|
||||
|
||||
# Packages
|
||||
environment.systemPackages = lxqtPkgs ++ cfg.extraPackages ++ [ kasmvncPackage ];
|
||||
|
||||
# Config directory and file
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /etc/kasmvnc 0750 root ${cfg.user} - -"
|
||||
];
|
||||
|
||||
environment.etc."kasmvnc/kasmvnc.yaml".text = generateConfig + cfg.extraYaml;
|
||||
|
||||
# Generate password file helper script
|
||||
environment.etc."kasmvnc/generate-password".text = ''
|
||||
#!/bin/sh
|
||||
# Run as user: kasmvncpasswd -f ~/.kasmpasswd
|
||||
exec ${kasmvncPackage}/bin/kasmvncpasswd -f "$HOME/.kasmpasswd"
|
||||
'';
|
||||
environment.etc."kasmvnc/generate-password".mode = "0755";
|
||||
|
||||
# Systemd USER service (not system service!)
|
||||
systemd.user.services.kasmvnc = {
|
||||
description = "KasmVNC Server on display ${cfg.display}";
|
||||
wantedBy = [ "graphical-session.target" ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Environment = [
|
||||
"XDG_SESSION_TYPE=wayland"
|
||||
"XDG_CURRENT_DESKTOP=LXQt"
|
||||
"QT_QPA_PLATFORM=wayland"
|
||||
"GDK_BACKEND=wayland,x11"
|
||||
"XDG_RUNTIME_DIR=%t"
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus"
|
||||
];
|
||||
ExecStartPre = "+${kasmvncPackage}/bin/kasmvncpasswd -f ${cfg.passwordFile} || true";
|
||||
ExecStart = ''
|
||||
${kasmvncPackage}/bin/kasmvncserver ${cfg.display} \
|
||||
--wayland \
|
||||
-- ${pkgs.kdePackages.kwin}/bin/kwin_wayland --xwayland --exit-with-session=${pkgs.lxqt.lxqt-session}/bin/startlxqt
|
||||
'';
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
StandardOutput = "journal";
|
||||
StandardError = "journal";
|
||||
};
|
||||
};
|
||||
|
||||
# Note: user lingering should be enabled via users.users.<name>.linger = true
|
||||
# or loginctl enable-linger <user>
|
||||
|
||||
# Firewall
|
||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||
networking.firewall.allowedUDPPorts = [ cfg.port ]; # for WebRTC UDP
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.nginx.kasmvnc;
|
||||
in {
|
||||
|
||||
options.services.nginx.kasmvnc = {
|
||||
enable = mkEnableOption "nginx reverse proxy for KasmVNC";
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = "Domain name (e.g. kasm.example.com)";
|
||||
};
|
||||
|
||||
upstream = mkOption {
|
||||
type = types.str;
|
||||
default = "https://127.0.0.1:8443";
|
||||
description = "KasmVNC upstream (https://host:port)";
|
||||
};
|
||||
|
||||
# ACME/Let's Encrypt
|
||||
acme = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
# WebSocket support for KasmVNC signaling
|
||||
websocket = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
# Note: Full WebRTC UDP requires coturn or nginx stream module
|
||||
# This config handles HTTPS + WSS signaling only
|
||||
extraConfig = mkOption { type = types.str; default = ""; };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedTlsSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedProxySettings = true;
|
||||
|
||||
virtualHosts = {
|
||||
"${cfg.domain}" = {
|
||||
forceSSL = cfg.acme;
|
||||
enableACME = cfg.acme;
|
||||
|
||||
# Proxy HTTPS + WSS to KasmVNC
|
||||
locations."/" = {
|
||||
proxyPass = cfg.upstream;
|
||||
proxyWebsockets = cfg.websocket;
|
||||
extraConfig = ''
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
# KasmVNC specific headers
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Server $host;
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Note about WebRTC UDP
|
||||
# For full WebRTC (UDP media), you need either:
|
||||
# 1. Direct access to KasmVNC port (no nginx for UDP)
|
||||
# 2. coturn STUN/TURN server
|
||||
# 3. nginx stream module with UDP proxy (complex)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.nginx.rustguac;
|
||||
in {
|
||||
|
||||
options.services.nginx.rustguac = {
|
||||
enable = mkEnableOption "nginx reverse proxy for rustguac";
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = "Domain name (e.g. guac.example.com)";
|
||||
};
|
||||
|
||||
upstream = mkOption {
|
||||
type = types.str;
|
||||
default = "http://127.0.0.1:8089";
|
||||
description = "rustguac upstream (protocol://host:port)";
|
||||
};
|
||||
|
||||
# ACME/Let's Enccrypt (uses global security.acme.email)
|
||||
acme = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable ACME/Let's Encrypt";
|
||||
};
|
||||
|
||||
# WebSocket support
|
||||
websocket = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable WebSocket proxying for Guacamole protocol";
|
||||
};
|
||||
|
||||
# Extra nginx config
|
||||
extraConfig = mkOption { type = types.str; default = ""; };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
recommendedTlsSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedProxySettings = true;
|
||||
|
||||
virtualHosts = {
|
||||
"${cfg.domain}" = {
|
||||
forceSSL = cfg.acme;
|
||||
enableACME = cfg.acme;
|
||||
|
||||
locations."/" = {
|
||||
proxyPass = cfg.upstream;
|
||||
proxyWebsockets = cfg.websocket;
|
||||
extraConfig = ''
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.rustdesk;
|
||||
in {
|
||||
|
||||
options.services.rustdesk = {
|
||||
enable = mkEnableOption "RustDesk server (hbbs + hbbr)";
|
||||
|
||||
hbbs = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
enable = mkOption { type = types.bool; default = true; };
|
||||
host = mkOption { type = types.str; default = "0.0.0.0"; };
|
||||
port = mkOption { type = types.port; default = 21115; };
|
||||
keyPath = mkOption { type = types.nullOr types.path; default = null; };
|
||||
};
|
||||
};
|
||||
default = { enable = true; };
|
||||
};
|
||||
|
||||
hbbr = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
enable = mkOption { type = types.bool; default = true; };
|
||||
host = mkOption { type = types.str; default = "0.0.0.0"; };
|
||||
port = mkOption { type = types.port; default = 21116; };
|
||||
apiPort = mkOption { type = types.port; default = 21118; };
|
||||
};
|
||||
};
|
||||
default = { enable = true; };
|
||||
};
|
||||
|
||||
dashboard = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
enable = mkOption { type = types.bool; default = false; };
|
||||
port = mkOption { type = types.port; default = 21114; };
|
||||
};
|
||||
};
|
||||
default = { enable = false; };
|
||||
};
|
||||
|
||||
openFirewall = mkOption { type = types.bool; default = true; };
|
||||
|
||||
user = mkOption { type = types.str; default = "rustdesk"; };
|
||||
group = mkOption { type = types.str; default = "rustdesk"; };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = "/var/lib/rustdesk";
|
||||
description = "RustDesk service user";
|
||||
};
|
||||
users.groups.${cfg.group} = { };
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/rustdesk 0750 ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
# hbbs service
|
||||
systemd.services.rustdesk-hbbs = mkIf cfg.hbbs.enable {
|
||||
description = "RustDesk hbbs (rendezvous/id server)";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${pkgs.rustdesk-server}/bin/hbbs -i ${cfg.hbbs.host} -p ${toString cfg.hbbs.port}" + (if cfg.hbbs.keyPath != null then " -k ${cfg.hbbs.keyPath}" else "");
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
};
|
||||
|
||||
# hbbr service
|
||||
systemd.services.rustdesk-hbbr = mkIf cfg.hbbr.enable {
|
||||
description = "RustDesk hbbr (relay server)";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${pkgs.rustdesk-server}/bin/hbbr -i ${cfg.hbbr.host} -p ${toString cfg.hbbr.port} -a ${cfg.hbbr.host}:${toString cfg.hbbr.apiPort}";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
};
|
||||
|
||||
# Dashboard (if enabled)
|
||||
systemd.services.rustdesk-dashboard = mkIf cfg.dashboard.enable {
|
||||
description = "RustDesk web dashboard";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${pkgs.rustdesk-server}/bin/hbbs -w ${toString cfg.dashboard.port}";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall
|
||||
([ cfg.hbbs.port ] ++ [ cfg.hbbr.port cfg.hbbr.apiPort ] ++ lib.optionals cfg.dashboard.enable [ cfg.dashboard.port ]);
|
||||
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ cfg.hbbs.port cfg.hbbr.port ];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.rustguac;
|
||||
|
||||
# Fetch rustguac pre-built binary from GitHub Releases (not in nixpkgs yet)
|
||||
rustguacPackage = pkgs.stdenv.mkDerivation rec {
|
||||
pname = "rustguac";
|
||||
version = "1.10.0";
|
||||
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://github.com/sol1/rustguac/releases/download/v${version}/rustguac-${version}-linux-amd64.tar.gz";
|
||||
sha256 = "sha256-z9MPBTpb/qWby9KYT3ELSisy06mbvfIE/VSpmcsIuQg=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ ];
|
||||
|
||||
installPhase = ''
|
||||
tar -xzf $src
|
||||
mkdir -p $out/bin
|
||||
cp rustguac-*/bin/rustguac $out/bin/
|
||||
'';
|
||||
};
|
||||
|
||||
guacdPackage = pkgs.guacamole-server;
|
||||
|
||||
# Generate api_keys TOML section
|
||||
apiKeysToml = "";
|
||||
|
||||
# Generate oidc TOML section
|
||||
oidcToml = optionalString (cfg.oidc != null) ''
|
||||
[oidc]
|
||||
issuer_url = "${cfg.oidc.issuerUrl}"
|
||||
client_id = "${cfg.oidc.clientId}"
|
||||
client_secret = "${cfg.oidc.clientSecret}"
|
||||
redirect_url = "${cfg.oidc.redirectUrl}"
|
||||
scopes = [${concatStringsSep " " (map (s: "\"${s}\"") cfg.oidc.scopes)}]
|
||||
username_claim = "${cfg.oidc.usernameClaim}"
|
||||
groups_claim = "${cfg.oidc.groupsClaim}"
|
||||
'';
|
||||
|
||||
# Generate tls TOML section
|
||||
tlsToml = optionalString (cfg.tls != null) ''
|
||||
tls_cert = "${cfg.tls.certFile}"
|
||||
tls_key = "${cfg.tls.keyFile}"
|
||||
'';
|
||||
in {
|
||||
|
||||
options.services.rustguac = {
|
||||
enable = mkEnableOption "rustguac - Lightweight Rust replacement for Apache Guacamole";
|
||||
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Bind address for rustguac HTTP server";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8089;
|
||||
description = "Port for rustguac HTTP server";
|
||||
};
|
||||
|
||||
tls = mkOption {
|
||||
type = types.nullOr (types.submodule {
|
||||
options = {
|
||||
certFile = mkOption { type = types.path; description = "Path to TLS certificate"; };
|
||||
keyFile = mkOption { type = types.path; description = "Path to TLS private key"; };
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
description = "TLS configuration (null = use nginx reverse proxy)";
|
||||
};
|
||||
|
||||
guacd = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
host = mkOption { type = types.str; default = "127.0.0.1"; };
|
||||
port = mkOption { type = types.port; default = 4822; };
|
||||
tls = mkOption { type = types.bool; default = true; };
|
||||
};
|
||||
};
|
||||
default = { host = "127.0.0.1"; port = 4822; tls = true; };
|
||||
};
|
||||
|
||||
vault = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
address = mkOption { type = types.str; default = "http://127.0.0.1:8200"; };
|
||||
token = mkOption { type = types.str; description = "Vault root token (or use AppRole)"; };
|
||||
mountPath = mkOption { type = types.str; default = "secret"; };
|
||||
connectionsPath = mkOption { type = types.str; default = "connections"; };
|
||||
};
|
||||
};
|
||||
default = { address = "http://127.0.0.1:8200"; mountPath = "secret"; connectionsPath = "connections"; };
|
||||
};
|
||||
|
||||
oidc = mkOption {
|
||||
type = types.nullOr (types.submodule {
|
||||
options = {
|
||||
issuerUrl = mkOption { type = types.str; description = "OIDC issuer URL (e.g. https://auth.example.com)"; };
|
||||
clientId = mkOption { type = types.str; };
|
||||
clientSecret = mkOption { type = types.str; };
|
||||
redirectUrl = mkOption { type = types.str; };
|
||||
scopes = mkOption { type = types.listOf types.str; default = [ "openid" "profile" "email" ]; };
|
||||
usernameClaim = mkOption { type = types.str; default = "email"; };
|
||||
groupsClaim = mkOption { type = types.str; default = "groups"; };
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
description = "OIDC configuration (null = disabled, use API keys)";
|
||||
};
|
||||
|
||||
apiKeys = mkOption {
|
||||
type = types.listOf (types.submodule {
|
||||
options = {
|
||||
name = mkOption { type = types.str; };
|
||||
keyHash = mkOption { type = types.str; description = "SHA-256 hash of the API key"; };
|
||||
roles = mkOption { type = types.listOf (types.enum [ "admin" "poweruser" "operator" "viewer" ]); default = [ "viewer" ]; };
|
||||
ipAllowlist = mkOption { type = types.listOf types.str; default = [ ]; };
|
||||
expiry = mkOption { type = types.nullOr types.str; default = null; };
|
||||
};
|
||||
});
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
recording = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
enable = mkOption { type = types.bool; default = true; };
|
||||
storagePath = mkOption { type = types.path; default = "/var/lib/rustguac/recordings"; };
|
||||
maxSize = mkOption { type = types.str; default = "10G"; };
|
||||
maxAge = mkOption { type = types.str; default = "30d"; };
|
||||
};
|
||||
};
|
||||
default = { enable = true; storagePath = "/var/lib/rustguac/recordings"; };
|
||||
};
|
||||
|
||||
rateLimit = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
enabled = mkOption { type = types.bool; default = true; };
|
||||
requestsPerMinute = mkOption { type = types.int; default = 60; };
|
||||
};
|
||||
};
|
||||
default = { enabled = true; requestsPerMinute = 60; };
|
||||
};
|
||||
|
||||
user = mkOption { type = types.str; default = "rustguac"; };
|
||||
group = mkOption { type = types.str; default = "rustguac"; };
|
||||
|
||||
extraConfig = mkOption { type = types.str; default = ""; };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = "/var/lib/rustguac";
|
||||
description = "rustguac service user";
|
||||
};
|
||||
users.groups.${cfg.group} = { };
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/rustguac 0750 ${cfg.user} ${cfg.group} - -"
|
||||
"d ${cfg.recording.storagePath} 0750 ${cfg.user} ${cfg.group} - -"
|
||||
"d /etc/rustguac 0750 ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
environment.etc."rustguac/config.toml".text = ''
|
||||
[server]
|
||||
host = "${cfg.host}"
|
||||
port = ${toString cfg.port}
|
||||
${tlsToml}
|
||||
|
||||
[guacd]
|
||||
host = "${cfg.guacd.host}"
|
||||
port = ${toString cfg.guacd.port}
|
||||
tls = ${toString cfg.guacd.tls}
|
||||
|
||||
[vault]
|
||||
address = "${cfg.vault.address}"
|
||||
token = "${cfg.vault.token}"
|
||||
mount_path = "${cfg.vault.mountPath}"
|
||||
connections_path = "${cfg.vault.connectionsPath}"
|
||||
|
||||
${oidcToml}
|
||||
|
||||
[recording]
|
||||
enabled = ${toString cfg.recording.enable}
|
||||
storage_path = "${cfg.recording.storagePath}"
|
||||
max_size = "${cfg.recording.maxSize}"
|
||||
max_age = "${cfg.recording.maxAge}"
|
||||
|
||||
[rate_limit]
|
||||
enabled = ${toString cfg.rateLimit.enabled}
|
||||
requests_per_minute = ${toString cfg.rateLimit.requestsPerMinute}
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
|
||||
systemd.services.rustguac = {
|
||||
description = "rustguac - Lightweight Rust replacement for Apache Guacamole";
|
||||
after = [ "network.target" "vault.service" ];
|
||||
wants = [ "vault.service" ];
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${rustguacPackage}/bin/rustguac --config /etc/rustguac/config.toml";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
LimitNOFILE = 65536;
|
||||
Environment = "RUST_LOG=info";
|
||||
AmbientCapabilities = "CAP_NET_BIND_SERVICE";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.guacd = {
|
||||
description = "guacd - Guacamole proxy daemon";
|
||||
after = [ "network.target" ];
|
||||
serviceConfig = {
|
||||
User = "guacd";
|
||||
Group = "guacd";
|
||||
ExecStart = "${guacdPackage}/sbin/guacd -f -L ${cfg.guacd.host} -p ${toString cfg.guacd.port} ${optionalString cfg.guacd.tls "-t"}";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
users.users.guacd = { isSystemUser = true; group = "guacd"; };
|
||||
users.groups.guacd = { };
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ cfg.port cfg.guacd.port ];
|
||||
|
||||
systemd.services.rustguac.wantedBy = [ "multi-user.target" ];
|
||||
systemd.services.guacd.wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
# Example configuration for hosts/tower/configuration.nix
|
||||
# Wire together: rustguac (gateway) + KasmVNC (personal desktop) + RustDesk (P2P) + Vault + nginx
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
# Core modules
|
||||
./modules/nixos/rustguac
|
||||
./modules/nixos/kasmvnc
|
||||
./modules/nixos/vault-rustguac
|
||||
./modules/nixos/rustdesk
|
||||
./modules/nixos/nginx/rustguac
|
||||
./modules/nixos/nginx/kasmvnc
|
||||
];
|
||||
|
||||
# ========================================================================
|
||||
# VAULT (dev mode for testing - CHANGE FOR PRODUCTION!)
|
||||
# ========================================================================
|
||||
services.vault-rustguac = {
|
||||
enable = true;
|
||||
mode = "dev"; # "dev" for testing, "ha" for production
|
||||
address = "127.0.0.1";
|
||||
port = 8200;
|
||||
devRootToken = "rustguac-dev-root-token-change-me"; # Use sops in prod!
|
||||
ui = true;
|
||||
};
|
||||
|
||||
# ========================================================================
|
||||
# RUSTGUAC - Gateway centralisée (multi-protocole, OIDC, audit)
|
||||
# ========================================================================
|
||||
services.rustguac = {
|
||||
enable = true;
|
||||
host = "127.0.0.1";
|
||||
port = 8089;
|
||||
|
||||
# TLS handled by nginx reverse proxy
|
||||
tls = null;
|
||||
|
||||
# guacd connection
|
||||
guacd = {
|
||||
host = "127.0.0.1";
|
||||
port = 4822;
|
||||
tls = true;
|
||||
};
|
||||
|
||||
# Vault for connections storage
|
||||
vault = {
|
||||
address = "http://127.0.0.1:8200";
|
||||
token = "rustguac-dev-root-token-change-me"; # Use sops!
|
||||
mountPath = "secret";
|
||||
connectionsPath = "rustguac/connections";
|
||||
};
|
||||
|
||||
# OIDC (configure with your provider: Authentik, Keycloak, Google, Okta)
|
||||
oidc = {
|
||||
issuerUrl = "https://auth.example.com"; # YOUR OIDC PROVIDER
|
||||
clientId = "rustguac";
|
||||
clientSecret = "***"; # Use sops!
|
||||
redirectUrl = "https://guac.example.com/api/oidc/callback";
|
||||
scopes = [ "openid" "profile" "email" "groups" ];
|
||||
usernameClaim = "email";
|
||||
groupsClaim = "groups";
|
||||
};
|
||||
|
||||
# API Keys as fallback (for agents/scripts)
|
||||
apiKeys = [
|
||||
{
|
||||
name = "ci-bot";
|
||||
keyHash = "sha256:..."; # echo -n "your-key" | sha256sum
|
||||
roles = [ "operator" ];
|
||||
ipAllowlist = [ "10.0.0.0/8" ];
|
||||
}
|
||||
];
|
||||
|
||||
# Session recording
|
||||
recording = {
|
||||
enable = true;
|
||||
storagePath = "/var/lib/rustguac/recordings";
|
||||
maxSize = "10G";
|
||||
maxAge = "30d";
|
||||
};
|
||||
|
||||
rateLimit = {
|
||||
enabled = true;
|
||||
requestsPerMinute = 120;
|
||||
};
|
||||
};
|
||||
|
||||
# ========================================================================
|
||||
# KASMVNC - Bureau personnel Wayland haute perf (LXQt + kwin_wayland)
|
||||
# ========================================================================
|
||||
services.kasmvnc = {
|
||||
enable = true;
|
||||
user = "marc"; # Your user
|
||||
display = ":1";
|
||||
geometry = "1920x1080";
|
||||
bindAddress = "127.0.0.1"; # Behind nginx
|
||||
port = 8443; # Internal HTTPS port
|
||||
|
||||
# TLS certs (use sops for private key!)
|
||||
tls = {
|
||||
certFile = "/run/secrets/kasmvnc-cert.pem";
|
||||
keyFile = "/run/secrets/kasmvnc-key.pem";
|
||||
};
|
||||
|
||||
# Password file (generated by kasmvncpasswd)
|
||||
passwordFile = "/home/marc/.kasmpasswd";
|
||||
|
||||
desktop = "lxqt-wayland";
|
||||
extraPackages = with pkgs; [
|
||||
firefox
|
||||
code
|
||||
gnome-terminal
|
||||
];
|
||||
};
|
||||
|
||||
# ========================================================================
|
||||
# RUSTDESK - P2P relay pour accès natif
|
||||
# ========================================================================
|
||||
services.rustdesk = {
|
||||
enable = true;
|
||||
hbbs = { enable = true; port = 21115; };
|
||||
hbbr = { enable = true; port = 21116; apiPort = 21118; };
|
||||
dashboard = { enable = true; port = 21114; };
|
||||
openFirewall = true;
|
||||
};
|
||||
|
||||
# ========================================================================
|
||||
# NGINX - Reverse proxy avec ACME pour les deux services
|
||||
# ========================================================================
|
||||
services.nginx.rustguac = {
|
||||
enable = true;
|
||||
domain = "guac.example.com"; # YOUR DOMAIN
|
||||
upstream = "http://127.0.0.1:8089";
|
||||
acme = true;
|
||||
acmeEmail = "admin@example.com";
|
||||
websocket = true;
|
||||
};
|
||||
|
||||
services.nginx.kasmvnc = {
|
||||
enable = true;
|
||||
domain = "kasm.example.com"; # YOUR DOMAIN
|
||||
upstream = "https://127.0.0.1:8443";
|
||||
acme = true;
|
||||
acmeEmail = "admin@example.com";
|
||||
websocket = true;
|
||||
};
|
||||
|
||||
# ========================================================================
|
||||
# SECRETS (sops-nix) - À configurer dans ton flake
|
||||
# ========================================================================
|
||||
# sops.secrets = {
|
||||
# rustguac-oidc-secret = { ... };
|
||||
# vault-root-token = { ... };
|
||||
# kasmvnc-tls-cert = { ... };
|
||||
# kasmvnc-tls-key = { ... };
|
||||
# rustguac-vault-token = { ... };
|
||||
# };
|
||||
|
||||
# ========================================================================
|
||||
# USER LINGERING (pour systemd user services KasmVNC)
|
||||
# ========================================================================
|
||||
systemd.user.enable = true;
|
||||
systemd.user.lingerUsers = [ "marc" ];
|
||||
|
||||
# ========================================================================
|
||||
# FIREWALL (ports additionnels si needed)
|
||||
# ========================================================================
|
||||
networking.firewall.allowedTCPPorts = [ 22 80 443 ]; # SSH, HTTP, HTTPS
|
||||
networking.firewall.allowedUDPPorts = [ ]; # WebRTC UDP handled by KasmVNC directly
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.vault-rustguac;
|
||||
in {
|
||||
|
||||
options.services.vault-rustguac = {
|
||||
enable = mkEnableOption "Vault - Secret management (dev mode for testing)";
|
||||
|
||||
# Mode: "dev" (in-memory, auto-unseal) or "ha" (Raft, persistent)
|
||||
mode = mkOption {
|
||||
type = types.enum [ "dev" "ha" ];
|
||||
default = "dev";
|
||||
description = "Vault mode: dev (testing) or ha (production Raft)";
|
||||
};
|
||||
|
||||
# Network
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Bind address";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8200;
|
||||
description = "HTTP/HTTPS port";
|
||||
};
|
||||
|
||||
# TLS (dev mode can use HTTP)
|
||||
tls = mkOption {
|
||||
type = types.nullOr (types.submodule {
|
||||
options = {
|
||||
certFile = mkOption { type = types.path; };
|
||||
keyFile = mkOption { type = types.path; };
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
};
|
||||
|
||||
# Dev mode settings
|
||||
devRootToken = mkOption {
|
||||
type = types.str;
|
||||
default = "root-token-dev-only";
|
||||
description = "Root token for dev mode (CHANGE IN PROD!)";
|
||||
};
|
||||
|
||||
# HA/Raft settings
|
||||
ha = mkOption {
|
||||
type = types.nullOr (types.submodule {
|
||||
options = {
|
||||
clusterName = mkOption { type = types.str; default = "vault-cluster"; };
|
||||
dataPath = mkOption { type = types.path; default = "/var/lib/vault"; };
|
||||
nodeId = mkOption { type = types.str; default = "node1"; };
|
||||
apiAddr = mkOption { type = types.str; default = "https://127.0.0.1:8200"; };
|
||||
clusterAddr = mkOption { type = types.str; default = "https://127.0.0.1:8201"; };
|
||||
retryJoin = mkOption { type = types.listOf types.str; default = [ ]; };
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
};
|
||||
|
||||
# User/group
|
||||
user = mkOption { type = types.str; default = "vault"; };
|
||||
group = mkOption { type = types.str; default = "vault"; };
|
||||
|
||||
# UI
|
||||
ui = mkOption { type = types.bool; default = true; };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# User/group
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = "/var/lib/vault";
|
||||
description = "Vault service user";
|
||||
};
|
||||
users.groups.${cfg.group} = { };
|
||||
|
||||
# Directories
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/vault 0750 ${cfg.user} ${cfg.group} - -"
|
||||
"d /etc/vault 0750 ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
# Config
|
||||
environment.etc."vault/config.hcl".text = ''
|
||||
${if cfg.mode == "dev" then ''
|
||||
storage "inmem" {}
|
||||
|
||||
listener "tcp" {
|
||||
address = "${cfg.address}:${toString cfg.port}"
|
||||
${if cfg.tls != null then ''
|
||||
tls_cert_file = "${cfg.tls.certFile}"
|
||||
tls_key_file = "${cfg.tls.keyFile}"
|
||||
'' else "tls_disable = true"}
|
||||
}
|
||||
|
||||
dev_root_token_id = "${cfg.devRootToken}"
|
||||
dev_listen_address = "${cfg.address}:${toString cfg.port}"
|
||||
'' else ''
|
||||
storage "raft" {
|
||||
path = "${cfg.ha.dataPath}"
|
||||
node_id = "${cfg.ha.nodeId}"
|
||||
${concatStringsSep "\n " (map (addr: "retry_join { leader_api_addr = \"${addr}\" }") cfg.ha.retryJoin)}
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "${cfg.address}:${toString cfg.port}"
|
||||
${if cfg.tls != null then ''
|
||||
tls_cert_file = "${cfg.tls.certFile}"
|
||||
tls_key_file = "${cfg.tls.keyFile}"
|
||||
'' else "tls_disable = true"}
|
||||
}
|
||||
|
||||
cluster_addr = "${cfg.ha.clusterAddr}"
|
||||
api_addr = "${cfg.ha.apiAddr}"
|
||||
''}
|
||||
|
||||
ui = ${toString cfg.ui}
|
||||
disable_mlock = true
|
||||
'';
|
||||
|
||||
# Systemd service
|
||||
systemd.services.vault = {
|
||||
description = "Vault - Secret management";
|
||||
after = [ "network.target" ];
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${pkgs.vault}/bin/vault server -config /etc/vault/config.hcl";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
LimitNOFILE = 65536;
|
||||
Environment = "VAULT_ADDR=${if cfg.tls != null then "https" else "http"}://${cfg.address}:${toString cfg.port}";
|
||||
# For mlock (disabled in config)
|
||||
CapabilityBoundingSet = "CAP_IPC_LOCK";
|
||||
};
|
||||
};
|
||||
|
||||
# Firewall
|
||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||
|
||||
# Auto-unseal for dev mode (already unsealed)
|
||||
# For HA, you'd need to run: vault operator init && vault operator unseal
|
||||
|
||||
systemd.services.vault.wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
}
|
||||
@@ -63,5 +63,15 @@
|
||||
# ./kanidm
|
||||
# ./syncserver
|
||||
# ./newt # attendre maj flakes
|
||||
|
||||
# Remote desktop gateway & access (WIP, non activé sur aucun profil : les
|
||||
# modules définissent juste les options, `enable` reste à false partout.
|
||||
# Domaines/secrets encore en placeholder dans les modules, cf TODO internes.)
|
||||
../modules/nixos/vault-rustguac
|
||||
../modules/nixos/rustguac
|
||||
../modules/nixos/kasmvnc
|
||||
../modules/nixos/rustdesk
|
||||
../modules/nixos/nginx/rustguac
|
||||
../modules/nixos/nginx/kasmvnc
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user