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:
Hermes Agent
2026-09-27 23:59:36 -04:00
parent 79032ac0ab
commit 605c355d3a
8 changed files with 1116 additions and 0 deletions
+278
View File
@@ -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
};
}
+83
View File
@@ -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)
};
}
+74
View File
@@ -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}
'';
};
};
};
};
};
}
+111
View File
@@ -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 ];
};
}
+237
View File
@@ -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" ];
};
}
+172
View File
@@ -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
}
+151
View File
@@ -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" ];
};
}
+10
View File
@@ -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
];
}