feat(tower): remote-desktop (rustguac, kasmvnc, rustdesk) + acl 777 access journal
Rustguac : guacd en loopback (tls=false) + vhost guac.marcpartensky.com (sans ACME, TLS par Pangolin). KasmVNC : display :20 (pas :1, session X existe deja), desktop lxqt (lxqt-wayland refuse), mot de passe regeneré par empreinte sops, auth HTTP 200/401 testee. Rustdesk : ports reels 1.1.16, hbbs/hbbr corriges; nginx guac/kasm en loopback. Vault/rustguac secrets lues par le bon user (vault, rustguac, marc) au lieu de root. WireGuard client sur wg1 (10.100.0.3/32, endpoint VPS 104.129.12.158:443) desactive tant que le secret sops wireguard/tower_private_key n'existe pas (evite le blocage sops-nix sur tout le build). hermes.extraGroups += systemd-journal.
This commit is contained in:
@@ -76,3 +76,7 @@ creation_rules:
|
||||
key_groups:
|
||||
- age:
|
||||
- *tower
|
||||
- path_regex: secrets/kasmvnc\.yml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *tower
|
||||
|
||||
@@ -5,6 +5,28 @@ with lib;
|
||||
let
|
||||
cfg = config.services.kasmvnc;
|
||||
|
||||
# L'ancien code faisait `elemAt (builtins.split "x" cfg.geometry) 0/1` :
|
||||
# builtins.split renvoie [prefixe, groupes, suffixe], donc l'index 0 donne bien
|
||||
# "1920" mais l'index 1 est la LISTE des groupes de capture, pas "1080". Le
|
||||
# height partait donc vide dans le YAML. splitString donne exactement les deux
|
||||
# morceaux attendus.
|
||||
geometryParts = splitString "x" cfg.geometry;
|
||||
width = elemAt geometryParts 0;
|
||||
height = elemAt geometryParts 1;
|
||||
|
||||
# perl + les modules CPAN que le wrapper kasmvncserver importe et que le deb ne
|
||||
# fournit pas (le deb ne livre que ses propres modules KasmVNC::*).
|
||||
perlLib = pkgs.perl.withPackages (ps: with ps; [
|
||||
Switch
|
||||
ListMoreUtils
|
||||
TryTiny
|
||||
DateTime
|
||||
DateTimeTimeZone
|
||||
DateTimeLocale
|
||||
YAMLTiny
|
||||
HashMergeSimple
|
||||
]);
|
||||
|
||||
# Fetch KasmVNC from GitHub Releases (not in nixpkgs)
|
||||
kasmvncPackage = pkgs.stdenv.mkDerivation rec {
|
||||
pname = "kasmvnc";
|
||||
@@ -15,35 +37,115 @@ let
|
||||
sha256 = "sha256-9Zn+AuIXW5gXthZfdKXSvr3HMRjd6Rgbo0EJY77Xrh4=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ pkgs.dpkg pkgs.patchelf ];
|
||||
# kasmvncserver et kasmvncconfig sont des SCRIPTS PERL, pas des ELF : l'ancien
|
||||
# garde `if file .../kasmvncserver | grep -q ELF` testait le script, donc etait
|
||||
# toujours FAUX et aucun binaire n'etait patche -> kasmvncpasswd sortait en
|
||||
# « error while loading shared libraries: libcrypt.so.1 ». autoPatchelfHook
|
||||
# patche tous les ELF du deb et resout les libs depuis buildInputs.
|
||||
nativeBuildInputs = [ pkgs.dpkg pkgs.autoPatchelfHook pkgs.patchelf pkgs.perl pkgs.makeWrapper ];
|
||||
buildInputs = [
|
||||
pkgs.glibc pkgs.gcc pkgs.libx11 pkgs.libxcb pkgs.libxkbcommon
|
||||
pkgs.wayland pkgs.libva pkgs.libdrm pkgs.mesa
|
||||
# libxcrypt-legacy et PAS libxcrypt : le .deb demande libcrypt.so.1 (soname
|
||||
# Debian) alors que pkgs.libxcrypt ne fournit que libcrypt.so.2. C'est
|
||||
# exactement l'erreur « libcrypt.so.1: cannot open shared object file ».
|
||||
pkgs.libxcrypt-legacy pkgs.glibc pkgs.gcc pkgs.libx11 pkgs.libxcb pkgs.libxkbcommon
|
||||
pkgs.wayland pkgs.libva pkgs.libdrm pkgs.mesa pkgs.libunwind pkgs.pixman
|
||||
pkgs.freetype pkgs.systemd pkgs.libxshmfence pkgs.libgbm
|
||||
# noms non-deprecies (pkgs.xorg.* emet un warning d'evaluation)
|
||||
pkgs.libxfont_2 pkgs.libxau pkgs.libxdmcp pkgs.libxtst
|
||||
pkgs.libxrandr pkgs.libxcursor
|
||||
pkgs.openssl pkgs.zlib pkgs.libjpeg_turbo pkgs.libpng pkgs.libwebp
|
||||
];
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
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
|
||||
# TOUS les binaires dans $out/bin : kasmvncserver (perl) localise Xkasmvnc
|
||||
# dans le dossier de $0 (DetectBinariesDir), donc les symlinks doivent etre
|
||||
# cote a cote (systemPackages les expose ensuite dans sw/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
|
||||
for b in kasmvncserver kasmvncpasswd kasmvncconfig kasmxproxy Xkasmvnc; do
|
||||
ln -sf $out/usr/bin/$b $out/bin/$b
|
||||
done
|
||||
# Le wrapper perl cherche LITTERALEMENT $exedir/Xvnc et $exedir/vncpasswd
|
||||
# (lignes 493-508) alors que le deb ne livre que Xkasmvnc / kasmvncpasswd :
|
||||
# sans ces deux alias il meurt sur « couldn't find ".../bin/Xvnc" ».
|
||||
ln -sf $out/usr/bin/Xkasmvnc $out/bin/Xvnc
|
||||
ln -sf $out/usr/bin/kasmvncpasswd $out/bin/vncpasswd
|
||||
# kasmvncserver/kasmvncconfig ont un shebang Debian #!/usr/bin/perl qui
|
||||
# n'existe pas sur NixOS (« No such file or directory » a l'execution) : le
|
||||
# fixupPhase de stdenv ne patche pas $out/usr/bin, donc explicitement ici.
|
||||
patchShebangs $out/usr/bin
|
||||
# Le wrapper charge ses valeurs par defaut depuis
|
||||
# /usr/share/kasmvnc/kasmvnc_defaults.yaml (chemin Debian absent sur NixOS,
|
||||
# donc les defauts n'etaient jamais lus). On reecrit vers le store.
|
||||
substituteInPlace $out/usr/bin/kasmvncserver \
|
||||
--replace-fail "/usr/share/kasmvnc/" "$out/usr/share/kasmvnc/"
|
||||
# Le wrapper calcule le selecteur de bureau via LocalSelectDePath()
|
||||
# (« $exedir/../builder/startup/deb/select-de.sh », arborescence du repo
|
||||
# KasmVNC) alors que le deb livre le fichier dans usr/lib/kasmvncserver/ :
|
||||
# sans cet alias la session sort en status=2 INVALIDARGUMENT avant Xvnc.
|
||||
mkdir -p $out/builder/startup/deb
|
||||
cp $out/usr/lib/kasmvncserver/select-de.sh $out/builder/startup/deb/select-de.sh
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
# Le wrapper perl doit etre autonome : PERL5LIB (ses propres modules + les
|
||||
# modules CPAN absents du deb) et le PATH (xauth, xkbcomp) sont cuits dans un
|
||||
# wrapper, pour qu'on n'ait plus besoin d'exporter quoi que ce soit avant de
|
||||
# lancer kasmvncserver (ni dans le shell, ni dans l'unite systemd).
|
||||
postFixup = ''
|
||||
wrapProgram $out/bin/kasmvncserver \
|
||||
--prefix PERL5LIB : "${perlLib}/lib/perl5/site_perl:$out/usr/share/perl5" \
|
||||
--prefix PATH : "${lib.makeBinPath [ pkgs.xauth pkgs.xkbcomp pkgs.xrandr pkgs.procps pkgs.inetutils pkgs.which pkgs.util-linux ]}"
|
||||
'';
|
||||
|
||||
# Le wrapper perl charge les modules KasmVNC::* (livres dans
|
||||
# usr/share/perl5) et quelques modules CPAN absents du deb : PERL5LIB est
|
||||
# pose par le module (services.kasmvnc.environment), pas ici.
|
||||
passthru = {
|
||||
# Liste exacte des modules absents du deb, obtenue en executant le wrapper :
|
||||
# kasmvncserver importe YAML::Tiny (KasmVNC/Config.pm) et
|
||||
# Hash::Merge::Simple en plus des modules DateTime/Switch/List::MoreUtils.
|
||||
perlModules = with pkgs.perlPackages; [
|
||||
Switch
|
||||
ListMoreUtils
|
||||
TryTiny
|
||||
DateTime
|
||||
DateTimeTimeZone
|
||||
DateTimeLocale
|
||||
YAMLTiny
|
||||
HashMergeSimple
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Session de bureau lancee par xstartup (X11 : kasmvncserver 1.5.0 n'a pas de
|
||||
# mode wayland). startlxqt herite du bus de session fourni par l'unite systemd.
|
||||
# PATH : l'unite fixe un `path` court (xauth, xkbcomp, hostname), ce qui
|
||||
# remplace le PATH par defaut de systemd et fait disparaitre le profil systeme.
|
||||
# Or startlxqt exec `lxqt-session` par son nom, d'ou « exec: lxqt-session: not
|
||||
# found » puis la fin immediate de la session (et l'arret de Xvnc). On remet
|
||||
# donc le profil systeme en tete : c'est la que vivent les paquets LXQt de marc
|
||||
# (lxqt-session, lxqt-panel, pcmanfm-qt, lxqt-runner).
|
||||
xstartup = pkgs.writeShellScript "kasmvnc-xstartup" (
|
||||
if cfg.desktop == "custom" then cfg.customCommand
|
||||
else if cfg.desktop == "xfce" then "exec ${pkgs.xfce.xfce4-session}/bin/xfce4-session"
|
||||
else ''
|
||||
export PATH="/run/current-system/sw/bin:${pkgs.lxqt.lxqt-session}/bin:$PATH"
|
||||
export XDG_SESSION_TYPE=x11
|
||||
export QT_QPA_PLATFORM=xcb
|
||||
export GDK_BACKEND=x11
|
||||
exec ${pkgs.lxqt.lxqt-session}/bin/startlxqt
|
||||
''
|
||||
);
|
||||
|
||||
# LXQt + kwin_wayland packages
|
||||
lxqtPkgs = with pkgs; [
|
||||
lxqt.lxqt-session
|
||||
@@ -68,77 +170,103 @@ let
|
||||
pcmanfm-qt
|
||||
];
|
||||
|
||||
# Generate kasmvnc.yaml config
|
||||
# Generate kasmvnc.yaml, au schema REEL de KasmVNC 1.5.0. L'ancien fichier
|
||||
# imbriquait network/desktop/encoding/gpu/advanced sous un bloc `server:` qui
|
||||
# n'existe pas a ce niveau : kasmvncserver repondait
|
||||
# « Unsupported config keys found: server.desktop.allow_resize, ... »
|
||||
# et ignorait donc TOUTE la config (resolution, port, headers, mot de passe).
|
||||
# Les cles de premier niveau sont : network, desktop, encoding, security,
|
||||
# server (http/advanced/auto_shutdown), user_session, runtime_configuration.
|
||||
# Reference : usr/share/kasmvnc/kasmvnc_defaults.yaml et etc/kasmvnc/kasmvnc.yaml
|
||||
# livres dans le .deb.
|
||||
generateConfig = ''
|
||||
network:
|
||||
# Valeurs acceptees : http | vnc (verifie par kasmvncserver :
|
||||
# « network.protocol 'https': must be one of [http, vnc] »). Le TLS web
|
||||
# vient de network.ssl.require_ssl, pas du protocole.
|
||||
protocol: http
|
||||
interface: "${cfg.bindAddress}"
|
||||
# KasmVNC 1.5.0 n'a PAS de network.port : le port web est websocket_port.
|
||||
websocket_port: ${toString cfg.port}
|
||||
use_ipv4: true
|
||||
use_ipv6: true
|
||||
ssl:
|
||||
pem_certificate: "${cfg.tls.certFile}"
|
||||
pem_key: "${cfg.tls.keyFile}"
|
||||
require_ssl: true
|
||||
desktop:
|
||||
resolution:
|
||||
width: ${width}
|
||||
height: ${height}
|
||||
allow_resize: true
|
||||
pixel_depth: 24
|
||||
# gpu est un SOUS-bloc de desktop (pas une cle de premier niveau).
|
||||
gpu:
|
||||
hw3d: true
|
||||
drinode: "/dev/dri/renderD128"
|
||||
encoding:
|
||||
# Pas de jpeg_quality/webp_quality ici : kasmvncserver les traduit en
|
||||
# « -WebpVideoQuality » que Xkasmvnc de cette version refuse
|
||||
# (« Fatal server error: Unrecognized option: -WebpVideoQuality »), ce qui
|
||||
# empechait le serveur de demarrer. Les defauts conviennent.
|
||||
video_encoding_mode:
|
||||
webp_encoding_time: 30
|
||||
security:
|
||||
brute_force_protection:
|
||||
blacklist_threshold: 5
|
||||
blacklist_timeout: 300
|
||||
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
|
||||
httpd_directory: ${cfg.package}/usr/share/kasmvnc/www
|
||||
advanced:
|
||||
kasm_password_file: "${cfg.passwordFile}"
|
||||
x_authority_file: auto
|
||||
auto_shutdown: false
|
||||
# « auto » cherche /usr/share/X11/fonts, inexistant sur NixOS : les
|
||||
# polices vivent dans /run/current-system/sw/share/X11/fonts.
|
||||
x_font_path: "/run/current-system/sw/share/X11/fonts"
|
||||
# Les timeouts sont dans la section auto_shutdown, pas dans advanced.
|
||||
auto_shutdown:
|
||||
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"
|
||||
'';
|
||||
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"
|
||||
# Sans ça kasmvncserver lance une invite interactive « Please choose Desktop
|
||||
# Environment to run » (select-de.sh) au demarrage : impossible dans une unite
|
||||
# systemd (pas de tty), sortie en status=2 INVALIDARGUMENT avant Xvnc.
|
||||
command_line:
|
||||
prompt: false
|
||||
'';
|
||||
in {
|
||||
|
||||
options.services.kasmvnc = {
|
||||
enable = mkEnableOption "KasmVNC - Modern Web-native VNC Server";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = kasmvncPackage;
|
||||
description = "Paquet KasmVNC (binaires dans $out/bin, y compris Xkasmvnc)";
|
||||
};
|
||||
|
||||
perlModules = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = kasmvncPackage.perlModules;
|
||||
description = "Modules CPAN requis par le wrapper perl kasmvncserver (PERL5LIB)";
|
||||
};
|
||||
|
||||
# User to run as (systemd user service)
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
@@ -192,9 +320,9 @@ in {
|
||||
|
||||
# Desktop environment
|
||||
desktop = mkOption {
|
||||
type = types.enum [ "lxqt-wayland" "plasma-wayland" "xfce" "custom" ];
|
||||
default = "lxqt-wayland";
|
||||
description = "Desktop environment to launch";
|
||||
type = types.enum [ "lxqt" "lxqt-wayland" "xfce" "custom" ];
|
||||
default = "lxqt";
|
||||
description = "Desktop environment to launch (le .deb 1.5.0 ne sait pas lancer de session Wayland : -xstartup -> X11)";
|
||||
};
|
||||
|
||||
# Custom command for desktop=custom
|
||||
@@ -224,45 +352,75 @@ in {
|
||||
users.users.${cfg.user}.extraGroups = [ "video" "render" "seat" "ssl-cert" ];
|
||||
|
||||
# Packages
|
||||
environment.systemPackages = lxqtPkgs ++ cfg.extraPackages ++ [ kasmvncPackage ];
|
||||
environment.systemPackages = lxqtPkgs ++ cfg.extraPackages ++ [ cfg.package ];
|
||||
|
||||
# Config directory and file
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /etc/kasmvnc 0750 root ${cfg.user} - -"
|
||||
# Xkasmvnc est un binaire Debian : le chemin de xkbcomp est COMPILE dedans
|
||||
# (« %s%sxkbcomp » avec XKB_BIN_DIRECTORY=/usr/bin) et l'option -xkbdir ne
|
||||
# change que les donnees, pas le binaire. Sans ce lien il meurt sur
|
||||
# sh: /usr/bin/xkbcomp: No such file or directory
|
||||
# XKB: Failed to compile keymap / Fatal server error
|
||||
# NixOS n'ayant pas de /usr, on cree le lien declarativement ici.
|
||||
"d /usr/bin 0755 root root - -"
|
||||
"L+ /usr/bin/xkbcomp - - - - ${pkgs.xkbcomp}/bin/xkbcomp"
|
||||
];
|
||||
|
||||
environment.etc."kasmvnc/kasmvnc.yaml".text = generateConfig + cfg.extraYaml;
|
||||
|
||||
# Session de bureau via script xstartup : c'est le SEUL mecanisme de
|
||||
# kasmvncserver 1.5.0 (aucun support de --wayland : « Fatal server error:
|
||||
# Unrecognized option: --wayland »). Le script est lance par le wrapper une
|
||||
# fois Xvnc en place.
|
||||
environment.etc."kasmvnc/xstartup".source = xstartup;
|
||||
|
||||
# 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"
|
||||
# kasmvncpasswd n'a PAS d'option -f (usage reel :
|
||||
# kasmvncpasswd -u <utilisateur> [fichier]) et refuse de tourner sans
|
||||
# terminal : a lancer depuis une session interactive de l'utilisateur.
|
||||
exec ${cfg.package}/bin/kasmvncpasswd -u "${cfg.user}" "$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" ];
|
||||
# KasmVNC est un serveur X autonome pour l'acces distant : il ne doit pas
|
||||
# dependre de la session graphique locale (Xwayland sur :1). En le voulant
|
||||
# par default.target il demarre aussi pour un utilisateur linger, sans
|
||||
# login local, ce qui est le but d'un acces distant.
|
||||
wantedBy = [ "default.target" ];
|
||||
# le wrapper perl exige xauth, xkbcomp et hostname sur le PATH, sinon il
|
||||
# refuse de demarrer avant meme de lire sa config (inetutils fournit
|
||||
# hostname : « couldn't find "hostname" on your PATH »).
|
||||
path = [ pkgs.xauth pkgs.xkbcomp pkgs.xrandr pkgs.procps pkgs.inetutils pkgs.which pkgs.util-linux ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Environment = [
|
||||
"XDG_SESSION_TYPE=wayland"
|
||||
"XDG_SESSION_TYPE=x11"
|
||||
"XDG_CURRENT_DESKTOP=LXQt"
|
||||
"QT_QPA_PLATFORM=wayland"
|
||||
"GDK_BACKEND=wayland,x11"
|
||||
"QT_QPA_PLATFORM=xcb"
|
||||
"GDK_BACKEND=x11"
|
||||
"XDG_RUNTIME_DIR=%t"
|
||||
"DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus"
|
||||
];
|
||||
ExecStartPre = "+${kasmvncPackage}/bin/kasmvncpasswd -f ${cfg.passwordFile} || true";
|
||||
# -xkbdir : Xkasmvnc compile son keymap depuis /usr/share/X11/xkb
|
||||
# (absent sur NixOS) et meurt sinon sur « XKB: Failed to compile keymap
|
||||
# / Could not start Xvnc ». Le wrapper perl relaie les options inconnues
|
||||
# a Xvnc, donc il suffit de lui passer le dossier du store.
|
||||
ExecStart = ''
|
||||
${kasmvncPackage}/bin/kasmvncserver ${cfg.display} \
|
||||
--wayland \
|
||||
-- ${pkgs.kdePackages.kwin}/bin/kwin_wayland --xwayland --exit-with-session=${pkgs.lxqt.lxqt-session}/bin/startlxqt
|
||||
${cfg.package}/bin/kasmvncserver ${cfg.display} \
|
||||
-xkbdir ${pkgs.xkeyboard_config}/share/X11/xkb \
|
||||
-xstartup /etc/kasmvnc/xstartup \
|
||||
-fg
|
||||
'';
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
# une session qui se termine (logout, plantage de LXQt) ne doit pas
|
||||
# laisser l'acces distant mort : on relance toujours.
|
||||
Restart = "always";
|
||||
RestartSec = 10;
|
||||
StandardOutput = "journal";
|
||||
StandardError = "journal";
|
||||
};
|
||||
@@ -271,6 +429,29 @@ in {
|
||||
# Note: user lingering should be enabled via users.users.<name>.linger = true
|
||||
# or loginctl enable-linger <user>
|
||||
|
||||
# Amorcage (root) de l'unite utilisateur : une unite utilisateur qui a
|
||||
# crash-loop est laissee en start-limit-hit par systemd, et seul un
|
||||
# `systemctl --user reset-failed` la debloque. Sans ca, l'acces distant
|
||||
# reste mort apres un plantage jusqu'a ce que quelqu'un ouvre une session
|
||||
# locale, ce qui est exactement la situation qu'on veut eviter. Ce oneshot
|
||||
# systeme est idempotent : reset-failed ne fait rien si l'unite est saine,
|
||||
# start ne fait rien si elle tourne deja.
|
||||
systemd.services.kasmvnc-kick = {
|
||||
description = "Degage l'unite utilisateur kasmvnc (start-limit-hit) et la (re)lance";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "systemd-user-sessions.service" ];
|
||||
serviceConfig = {
|
||||
# pas de RemainAfterExit : l'unite reste inactive, donc chaque
|
||||
# nixos-rebuild switch la relance (et remet kasmvnc en route apres un
|
||||
# plantage), au lieu de la considerer comme deja faite pour toujours.
|
||||
Type = "oneshot";
|
||||
};
|
||||
script = ''
|
||||
${pkgs.systemd}/bin/systemctl --user -M ${cfg.user}@.host reset-failed kasmvnc.service || true
|
||||
${pkgs.systemd}/bin/systemctl --user -M ${cfg.user}@.host start kasmvnc.service || true
|
||||
'';
|
||||
};
|
||||
|
||||
# Firewall
|
||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||
networking.firewall.allowedUDPPorts = [ cfg.port ]; # for WebRTC UDP
|
||||
|
||||
@@ -26,6 +26,21 @@ in {
|
||||
default = true;
|
||||
};
|
||||
|
||||
# Ecoute locale, pour une exposition sans ACME (reverse proxy qui porte le
|
||||
# TLS, ex. newt/Pangolin). listenPort = null garde les defauts du module
|
||||
# nginx (0.0.0.0:80 + 443 ssl).
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Adresse d'ecoute quand listenPort est defini";
|
||||
};
|
||||
|
||||
listenPort = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
description = "Port d'ecoute local (HTTP) ; null = defauts nginx (80/443)";
|
||||
};
|
||||
|
||||
# WebSocket support for KasmVNC signaling
|
||||
websocket = mkOption {
|
||||
type = types.bool;
|
||||
@@ -48,20 +63,19 @@ in {
|
||||
"${cfg.domain}" = {
|
||||
forceSSL = cfg.acme;
|
||||
enableACME = cfg.acme;
|
||||
listen = optional (cfg.listenPort != null) {
|
||||
addr = cfg.listenAddress;
|
||||
port = cfg.listenPort;
|
||||
};
|
||||
|
||||
# Proxy HTTPS + WSS to KasmVNC
|
||||
locations."/" = {
|
||||
proxyPass = cfg.upstream;
|
||||
proxyWebsockets = cfg.websocket;
|
||||
# Idem rustguac : recommendedProxySettings pose deja les en-tetes et
|
||||
# proxy_http_version, un doublon fait echouer `nginx -t`.
|
||||
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
|
||||
|
||||
@@ -20,13 +20,28 @@ in {
|
||||
description = "rustguac upstream (protocol://host:port)";
|
||||
};
|
||||
|
||||
# ACME/Let's Enccrypt (uses global security.acme.email)
|
||||
# ACME/Let's Enccrypt (uses security.acme.defaults.email)
|
||||
acme = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable ACME/Let's Encrypt";
|
||||
};
|
||||
|
||||
# Ecoute locale, pour une exposition sans ACME (typiquement derriere un
|
||||
# reverse proxy qui porte deja le TLS, ex. newt/Pangolin).
|
||||
# listenPort = null garde les defauts du module nginx (0.0.0.0:80 + 443 ssl).
|
||||
listenAddress = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Adresse d'ecoute quand listenPort est defini";
|
||||
};
|
||||
|
||||
listenPort = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
description = "Port d'ecoute local (HTTP) ; null = defauts nginx (80/443)";
|
||||
};
|
||||
|
||||
# WebSocket support
|
||||
websocket = mkOption {
|
||||
type = types.bool;
|
||||
@@ -49,19 +64,21 @@ in {
|
||||
"${cfg.domain}" = {
|
||||
forceSSL = cfg.acme;
|
||||
enableACME = cfg.acme;
|
||||
listen = optional (cfg.listenPort != null) {
|
||||
addr = cfg.listenAddress;
|
||||
port = cfg.listenPort;
|
||||
};
|
||||
|
||||
locations."/" = {
|
||||
proxyPass = cfg.upstream;
|
||||
proxyWebsockets = cfg.websocket;
|
||||
# recommendedProxySettings pose deja proxy_http_version, Upgrade,
|
||||
# Connection, Host, X-Real-IP, X-Forwarded-* (includes) : les
|
||||
# redeclarer fait echouer « nginx -t » (directive is duplicate) et
|
||||
# tombe TOUT nginx, donc tout ce qui est derriere. Ne garder ici que
|
||||
# ce que le module nginx ne pose pas.
|
||||
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}
|
||||
|
||||
@@ -9,12 +9,26 @@ in {
|
||||
options.services.rustdesk = {
|
||||
enable = mkEnableOption "RustDesk server (hbbs + hbbr)";
|
||||
|
||||
# La CLI de rustdesk-server 1.1.16 n'a que : hbbs -p <port id/rendezvous>,
|
||||
# -r <relais>, -k <cle>, -R, -M, -s, -u ; hbbr -p <port relais>, -k <cle>.
|
||||
# Il n'y a NI -i (bind) NI -a (autre port) : passer ces drapeaux fait sortir
|
||||
# le binaire en « Found argument '-i' which wasn't expected ».
|
||||
# hbbs se lie tout seul sur 21115 (test NAT) / 21116 (ID, TCP+UDP) /
|
||||
# 21118 (websocket), hbbr sur 21117 (relais) et 21119 (websocket).
|
||||
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; };
|
||||
idPort = mkOption {
|
||||
type = types.port;
|
||||
default = 21116;
|
||||
description = "Port ID/rendezvous (TCP+UDP)";
|
||||
};
|
||||
relayServers = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Relais annonces aux clients (-r), virgule-separes";
|
||||
};
|
||||
keyPath = mkOption { type = types.nullOr types.path; default = null; };
|
||||
};
|
||||
};
|
||||
@@ -25,9 +39,12 @@ in {
|
||||
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; };
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 21117;
|
||||
description = "Port relais (TCP)";
|
||||
};
|
||||
keyPath = mkOption { type = types.nullOr types.path; default = null; };
|
||||
};
|
||||
};
|
||||
default = { enable = true; };
|
||||
@@ -70,7 +87,13 @@ in {
|
||||
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 "");
|
||||
# hbbs ecrit sa paire de cles dans le repertoire courant quand -k n'est
|
||||
# pas fourni : sans WorkingDirectory il tente d'ecrire dans / et sort en
|
||||
# erreur (le service sortait en status=1 avant meme de se lier).
|
||||
WorkingDirectory = "/var/lib/rustdesk";
|
||||
ExecStart = "${pkgs.rustdesk-server}/bin/hbbs -p ${toString cfg.hbbs.idPort}"
|
||||
+ optionalString (cfg.hbbs.relayServers != null) " -r ${cfg.hbbs.relayServers}"
|
||||
+ optionalString (cfg.hbbs.keyPath != null) " -k ${cfg.hbbs.keyPath}";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
@@ -84,7 +107,9 @@ in {
|
||||
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}";
|
||||
WorkingDirectory = "/var/lib/rustdesk";
|
||||
ExecStart = "${pkgs.rustdesk-server}/bin/hbbr -p ${toString cfg.hbbr.port}"
|
||||
+ optionalString (cfg.hbbr.keyPath != null) " -k ${cfg.hbbr.keyPath}";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
@@ -104,8 +129,9 @@ in {
|
||||
};
|
||||
};
|
||||
|
||||
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 ];
|
||||
# Ports reels des deux binaires (pas de drapeau pour les deplacer) :
|
||||
# 21115 test NAT, 21116 ID TCP+UDP, 21117 relais, 21118/21119 websocket.
|
||||
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall ([ 21115 21116 21117 21118 21119 ]);
|
||||
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [ 21116 ];
|
||||
};
|
||||
}
|
||||
+234
-115
@@ -5,46 +5,150 @@ with lib;
|
||||
let
|
||||
cfg = config.services.rustguac;
|
||||
|
||||
# Fetch rustguac pre-built binary from GitHub Releases (not in nixpkgs yet)
|
||||
rustguacPackage = pkgs.stdenv.mkDerivation rec {
|
||||
# Binaire pre-built de la release GitHub (rustguac n'est pas dans nixpkgs).
|
||||
# PIEGE : la release est une ARCHIVE COMPLETE (bin/, static/, lib/, sbin/,
|
||||
# scripts/, config.toml.default). L'ancienne installPhase ne copiait que
|
||||
# bin/rustguac, donc l'UI web (static/) manquait et le serveur n'avait aucune
|
||||
# page a servir.
|
||||
rustguacPackage = pkgs.stdenv.mkDerivation {
|
||||
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";
|
||||
url = "https://github.com/sol1/rustguac/releases/download/v1.10.0/rustguac-1.10.0-linux-amd64.tar.gz";
|
||||
sha256 = "sha256-z9MPBTpb/qWby9KYT3ELSisy06mbvfIE/VSpmcsIuQg=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ ];
|
||||
|
||||
# stdenv a deja deballe l'archive ET s'est place dans son unique dossier
|
||||
# racine (sourceRoot) : il suffit de reprendre l'arbre courant tel quel.
|
||||
installPhase = ''
|
||||
tar -xzf $src
|
||||
mkdir -p $out/bin
|
||||
cp rustguac-*/bin/rustguac $out/bin/
|
||||
runHook preInstall
|
||||
mkdir -p $out
|
||||
cp -a ./. $out/
|
||||
rm -rf $out/install.sh $out/systemd $out/config.toml.default
|
||||
runHook postInstall
|
||||
'';
|
||||
};
|
||||
|
||||
guacdPackage = pkgs.guacamole-server;
|
||||
|
||||
# Generate api_keys TOML section
|
||||
apiKeysToml = "";
|
||||
stateDir = "/var/lib/rustguac";
|
||||
|
||||
# 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}"
|
||||
'';
|
||||
# Le role_id vient du bootstrap Vault (fichier ecrit a l'execution) : la
|
||||
# section [vault] ne peut donc pas etre figee dans le store. Le wrapper la
|
||||
# colle au config.toml au demarrage et exporte le secret_id via
|
||||
# VAULT_SECRET_ID (le seul chemin documente par rustguac : role_id en config,
|
||||
# secret_id en env).
|
||||
runScript = pkgs.writeShellApplication {
|
||||
name = "rustguac-run";
|
||||
runtimeInputs = [ rustguacPackage pkgs.coreutils ];
|
||||
text = ''
|
||||
set -euo pipefail
|
||||
conf="''${RUSTGUAC_RUNTIME_DIR:?}/config.toml"
|
||||
# `cat > fichier` et pas `cp` : cp recopie AUSSI le mode du fichier du
|
||||
# store (0444), et le `>>` d'ajout de la section [vault] juste apres
|
||||
# echouait en « Permission denied » (le fichier venait d'etre cree en
|
||||
# lecture seule).
|
||||
cat ${configFile} > "$conf"
|
||||
${optionalString (cfg.oidc != null && cfg.oidc.clientSecretFile != null) ''
|
||||
OIDC_CLIENT_SECRET="$(cat ${cfg.oidc.clientSecretFile})"
|
||||
export OIDC_CLIENT_SECRET
|
||||
''}
|
||||
${optionalString (cfg.vault != null) ''
|
||||
VAULT_ADDR="${cfg.vault.address}"
|
||||
VAULT_SECRET_ID="$(cat ${cfg.vault.secretIdFile})"
|
||||
export VAULT_ADDR VAULT_SECRET_ID
|
||||
{
|
||||
echo ""
|
||||
echo "[vault]"
|
||||
echo "addr = \"${cfg.vault.address}\""
|
||||
echo "mount = \"${cfg.vault.mountPath}\""
|
||||
echo "base_path = \"${cfg.vault.basePath}\""
|
||||
echo "role_id = \"$(cat ${cfg.vault.roleIdFile})\""
|
||||
${optionalString (cfg.vault.instanceName != null) ''
|
||||
echo "instance_name = \"${cfg.vault.instanceName}\""
|
||||
''}
|
||||
} >> "$conf"
|
||||
''}
|
||||
exec rustguac --config "$conf" serve
|
||||
'';
|
||||
};
|
||||
|
||||
# Generate tls TOML section
|
||||
tlsToml = optionalString (cfg.tls != null) ''
|
||||
tls_cert = "${cfg.tls.certFile}"
|
||||
tls_key = "${cfg.tls.keyFile}"
|
||||
# rustguac n'accepte PAS un simple token Vault : il lui faut role_id (config)
|
||||
# + secret_id (env VAULT_SECRET_ID), donc une auth AppRole. Le token root ne
|
||||
# sert qu'a creer la policy, le role, et a sortir ces deux valeurs.
|
||||
vaultBootstrap = pkgs.writeShellApplication {
|
||||
name = "rustguac-vault-bootstrap";
|
||||
runtimeInputs = [ pkgs.vault pkgs.coreutils ];
|
||||
text = ''
|
||||
set -euo pipefail
|
||||
VAULT_ADDR="${cfg.vault.address}"
|
||||
VAULT_TOKEN="$(cat ${cfg.vault.rootTokenFile})"
|
||||
export VAULT_ADDR VAULT_TOKEN
|
||||
install -d -m 0750 -o ${cfg.user} -g ${cfg.group} ${stateDir}
|
||||
|
||||
vault auth enable approle 2>/dev/null || true
|
||||
vault policy write rustguac - >/dev/null <<'POLICY'
|
||||
path "${cfg.vault.mountPath}/data/${cfg.vault.basePath}/*" {
|
||||
capabilities = ["create", "read", "update", "delete", "list"]
|
||||
}
|
||||
path "${cfg.vault.mountPath}/metadata/${cfg.vault.basePath}/*" {
|
||||
capabilities = ["read", "list", "delete"]
|
||||
}
|
||||
POLICY
|
||||
vault write "auth/approle/role/${cfg.vault.roleName}" \
|
||||
token_policies=rustguac \
|
||||
token_ttl=1h token_max_ttl=24h \
|
||||
secret_id_ttl=0 bind_secret_id=true >/dev/null
|
||||
|
||||
umask 077
|
||||
vault read -field=role_id "auth/approle/role/${cfg.vault.roleName}/role-id" > ${cfg.vault.roleIdFile}
|
||||
vault write -f -field=secret_id "auth/approle/role/${cfg.vault.roleName}/secret-id" > ${cfg.vault.secretIdFile}
|
||||
chown ${cfg.user}:${cfg.group} ${cfg.vault.roleIdFile} ${cfg.vault.secretIdFile}
|
||||
chmod 0400 ${cfg.vault.roleIdFile} ${cfg.vault.secretIdFile}
|
||||
echo "rustguac: AppRole ${cfg.vault.roleName} pret (role_id/secret_id sous ${stateDir})"
|
||||
'';
|
||||
};
|
||||
|
||||
# Schema REEL de rustguac 1.10.0 (src/config.rs) : listen_addr / guacd_addr /
|
||||
# static_path / db_path / rate_limit / trusted_proxies sont des cles RACINE
|
||||
# (pas de section [server] ni [guacd]), [oidc] attend redirect_uri +
|
||||
# extra_scopes + default_role, [recording] attend path, et le TLS guacd
|
||||
# s'exprime par [tls] guacd_cert_path (pas un booleen).
|
||||
configFile = pkgs.writeText "rustguac-config.toml" ''
|
||||
listen_addr = "${cfg.host}:${toString cfg.port}"
|
||||
guacd_addr = "${cfg.guacd.host}:${toString cfg.guacd.port}"
|
||||
recording_path = "${cfg.recording.path}"
|
||||
static_path = "${rustguacPackage}/static"
|
||||
db_path = "${cfg.dbPath}"
|
||||
site_title = "${cfg.siteTitle}"
|
||||
rate_limit = ${boolToString cfg.rateLimit}
|
||||
trusted_proxies = [${concatStringsSep ", " (map (p: "\"${p}\"") cfg.trustedProxies)}]
|
||||
${optionalString (cfg.tls != null) ''
|
||||
[tls]
|
||||
cert_path = "${cfg.tls.certFile}"
|
||||
key_path = "${cfg.tls.keyFile}"
|
||||
${optionalString cfg.guacd.tls ''guacd_cert_path = "${cfg.guacd.certFile}"''}
|
||||
''}
|
||||
${optionalString (cfg.tls == null && cfg.guacd.tls) ''
|
||||
[tls]
|
||||
guacd_cert_path = "${cfg.guacd.certFile}"
|
||||
''}
|
||||
${optionalString (cfg.oidc != null) ''
|
||||
[oidc]
|
||||
issuer_url = "${cfg.oidc.issuerUrl}"
|
||||
client_id = "${cfg.oidc.clientId}"
|
||||
${optionalString (cfg.oidc.clientSecret != null) ''client_secret = "${cfg.oidc.clientSecret}"''}
|
||||
redirect_uri = "${cfg.oidc.redirectUri}"
|
||||
default_role = "${cfg.oidc.defaultRole}"
|
||||
groups_claim = "${cfg.oidc.groupsClaim}"
|
||||
${optionalString (cfg.oidc.extraScopes != [ ]) ''extra_scopes = [${concatStringsSep ", " (map (s: "\"${s}\"") cfg.oidc.extraScopes)}]''}
|
||||
''}
|
||||
[recording]
|
||||
enabled = ${boolToString cfg.recording.enable}
|
||||
path = "${cfg.recording.path}"
|
||||
max_disk_percent = ${toString cfg.recording.maxDiskPercent}
|
||||
max_recordings = ${toString cfg.recording.maxRecordings}
|
||||
'';
|
||||
in {
|
||||
|
||||
@@ -54,23 +158,30 @@ in {
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "Bind address for rustguac HTTP server";
|
||||
description = "Adresse d'ecoute (listen_addr)";
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 8089;
|
||||
description = "Port for rustguac HTTP server";
|
||||
description = "Port HTTP de rustguac";
|
||||
};
|
||||
|
||||
dbPath = mkOption {
|
||||
type = types.str;
|
||||
default = "${stateDir}/rustguac.db";
|
||||
description = "Base sqlite (admins, utilisateurs OIDC, sessions)";
|
||||
};
|
||||
siteTitle = mkOption { type = types.str; default = "rustguac"; };
|
||||
|
||||
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"; };
|
||||
certFile = mkOption { type = types.path; };
|
||||
keyFile = mkOption { type = types.path; };
|
||||
};
|
||||
});
|
||||
default = null;
|
||||
description = "TLS configuration (null = use nginx reverse proxy)";
|
||||
description = "HTTPS cote client (null = HTTP derriere un reverse proxy)";
|
||||
};
|
||||
|
||||
guacd = mkOption {
|
||||
@@ -78,160 +189,168 @@ in {
|
||||
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; };
|
||||
# guacd n'a pas de drapeau "activer TLS" : il sert du TLS des qu'on lui
|
||||
# donne -C certificat -K cle. rustguac de son cote ne parle TLS que si
|
||||
# [tls] guacd_cert_path est renseigne. Les deux doivent etre d'accord.
|
||||
tls = mkOption { type = types.bool; default = false; };
|
||||
certFile = mkOption { type = types.nullOr types.path; default = null; };
|
||||
keyFile = mkOption { type = types.nullOr types.path; default = null; };
|
||||
};
|
||||
};
|
||||
default = { host = "127.0.0.1"; port = 4822; tls = true; };
|
||||
default = { host = "127.0.0.1"; port = 4822; tls = false; };
|
||||
};
|
||||
|
||||
vault = mkOption {
|
||||
type = types.submodule {
|
||||
type = types.nullOr (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)"; };
|
||||
rootTokenFile = mkOption {
|
||||
type = types.path;
|
||||
description = "Fichier contenant le token root, lu par le bootstrap (root)";
|
||||
};
|
||||
mountPath = mkOption { type = types.str; default = "secret"; };
|
||||
connectionsPath = mkOption { type = types.str; default = "connections"; };
|
||||
basePath = mkOption { type = types.str; default = "rustguac"; };
|
||||
instanceName = mkOption { type = types.nullOr types.str; default = null; };
|
||||
roleName = mkOption { type = types.str; default = "rustguac"; };
|
||||
roleIdFile = mkOption { type = types.str; default = "${stateDir}/role-id"; };
|
||||
secretIdFile = mkOption { type = types.str; default = "${stateDir}/secret-id"; };
|
||||
};
|
||||
};
|
||||
default = { address = "http://127.0.0.1:8200"; mountPath = "secret"; connectionsPath = "connections"; };
|
||||
});
|
||||
default = null;
|
||||
description = "Address book Vault (AppRole). null = pas d'address book.";
|
||||
};
|
||||
|
||||
oidc = mkOption {
|
||||
type = types.nullOr (types.submodule {
|
||||
options = {
|
||||
issuerUrl = mkOption { type = types.str; description = "OIDC issuer URL (e.g. https://auth.example.com)"; };
|
||||
issuerUrl = mkOption { type = types.str; };
|
||||
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"; };
|
||||
# Valeur LITTERALE. Pour un secret qui vit dans un fichier sops,
|
||||
# utiliser clientSecretFile : il est exporte en OIDC_CLIENT_SECRET et
|
||||
# n'apparait jamais dans la config (le chemin, lui, ne marcherait pas).
|
||||
clientSecret = mkOption { type = types.nullOr types.str; default = null; };
|
||||
clientSecretFile = mkOption { type = types.nullOr types.path; default = null; };
|
||||
redirectUri = mkOption {
|
||||
type = types.str;
|
||||
description = "URL publique + /auth/callback (chemin rustguac)";
|
||||
};
|
||||
defaultRole = mkOption {
|
||||
type = types.enum [ "admin" "poweruser" "operator" "viewer" ];
|
||||
default = "operator";
|
||||
};
|
||||
groupsClaim = mkOption { type = types.str; default = "groups"; };
|
||||
extraScopes = mkOption { type = types.listOf types.str; default = [ ]; };
|
||||
};
|
||||
});
|
||||
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 = [ ];
|
||||
description = "OIDC (null = desactive, auth par cle API seulement)";
|
||||
};
|
||||
|
||||
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"; };
|
||||
path = mkOption { type = types.str; default = "${stateDir}/recordings"; };
|
||||
maxDiskPercent = mkOption { type = types.int; default = 80; };
|
||||
maxRecordings = mkOption { type = types.int; default = 0; };
|
||||
};
|
||||
};
|
||||
default = { enable = true; storagePath = "/var/lib/rustguac/recordings"; };
|
||||
default = { };
|
||||
};
|
||||
|
||||
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; };
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Limitation de debit cote rustguac (inutile derriere un reverse proxy)";
|
||||
};
|
||||
|
||||
trustedProxies = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ "127.0.0.1/32" ];
|
||||
description = "CIDR des proxys dont X-Forwarded-For est digne de confiance";
|
||||
};
|
||||
|
||||
user = mkOption { type = types.str; default = "rustguac"; };
|
||||
group = mkOption { type = types.str; default = "rustguac"; };
|
||||
|
||||
extraConfig = mkOption { type = types.str; default = ""; };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = !cfg.guacd.tls || (cfg.guacd.certFile != null && cfg.guacd.keyFile != null);
|
||||
message = "services.rustguac.guacd.tls = true exige guacd.certFile et guacd.keyFile (guacd n'a pas de drapeau TLS sans certificat).";
|
||||
}
|
||||
{
|
||||
assertion = cfg.vault == null || (cfg.vault.roleIdFile == "${stateDir}/role-id" && cfg.vault.secretIdFile == "${stateDir}/secret-id");
|
||||
message = "services.rustguac.vault.roleIdFile/secretIdFile doivent rester sous ${stateDir} (le service les relit au demarrage).";
|
||||
}
|
||||
];
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
home = "/var/lib/rustguac";
|
||||
home = stateDir;
|
||||
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} - -"
|
||||
"d ${stateDir} 0750 ${cfg.user} ${cfg.group} - -"
|
||||
"d ${cfg.recording.path} 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}
|
||||
'';
|
||||
environment.etc."rustguac/config.toml".source = configFile;
|
||||
|
||||
systemd.services.rustguac = {
|
||||
description = "rustguac - Lightweight Rust replacement for Apache Guacamole";
|
||||
after = [ "network.target" "vault.service" ];
|
||||
wants = [ "vault.service" ];
|
||||
after = [ "network.target" ] ++ optional (cfg.vault != null) "vault.service";
|
||||
wants = optional (cfg.vault != null) "vault.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${rustguacPackage}/bin/rustguac --config /etc/rustguac/config.toml";
|
||||
WorkingDirectory = stateDir;
|
||||
# le bootstrap tourne en root (+) : il lit le token root et ecrit
|
||||
# role-id/secret-id dans ${stateDir}, que le service relit ensuite.
|
||||
ExecStartPre = optional (cfg.vault != null) "+${vaultBootstrap}/bin/rustguac-vault-bootstrap";
|
||||
Environment = [
|
||||
"RUST_LOG=info"
|
||||
"RUSTGUAC_RUNTIME_DIR=/run/rustguac"
|
||||
];
|
||||
ExecStart = "${runScript}/bin/rustguac-run";
|
||||
# systemd cree /run/rustguac en tant que service user : un tmpfiles
|
||||
# « d /run/rustguac ... rustguac » ne suffisait pas (le repertoire existait
|
||||
# deja en root, d'ou « Permission denied » sur le cp du config au demarrage).
|
||||
RuntimeDirectory = "rustguac";
|
||||
RuntimeDirectoryMode = "0750";
|
||||
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" ];
|
||||
wantedBy = [ "multi-user.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"}";
|
||||
# CLI reelle de guacd : -b ADRESSE, -l PORT, -p PIDFILE, -L NIVEAU_DE_LOG,
|
||||
# -C certificat, -K cle, -f (premier plan). L'ancienne ligne
|
||||
# « -L 127.0.0.1 -p 4822 -t » etait fausse sur les trois drapeaux
|
||||
# (-L attend un niveau de log, -p un pidfile, -t n'existe pas) : guacd
|
||||
# sortait immediatement en « Invalid log level ».
|
||||
ExecStart = "${guacdPackage}/sbin/guacd -f -b ${cfg.guacd.host} -l ${toString cfg.guacd.port}"
|
||||
+ optionalString cfg.guacd.tls " -C ${cfg.guacd.certFile} -K ${cfg.guacd.keyFile}";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
};
|
||||
};
|
||||
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" ];
|
||||
# rustguac et guacd ecoutent en loopback (host par defaut) et sont atteints
|
||||
# via le vhost nginx : rien a ouvrir au firewall.
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,20 @@ with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.vault-rustguac;
|
||||
|
||||
# Le mode dev de Vault est pilote par les DRAPEAUX CLI (`vault server -dev`),
|
||||
# pas par le fichier de config : `dev_root_token_id` / `dev_listen_address`
|
||||
# poses dans un HCL ne declenchent PAS le mode dev. Consequence vecue : vault
|
||||
# demarrait avec storage inmem mais restait SEALED et non initialise
|
||||
# (`/v1/sys/health` -> initialized:false, sealed:true), donc rustguac ne
|
||||
# pouvait rien lire ni ecrire.
|
||||
devScript = pkgs.writeShellScript "vault-dev" ''
|
||||
set -euo pipefail
|
||||
token=$(cat ${cfg.devRootToken})
|
||||
exec ${pkgs.vault}/bin/vault server -dev \
|
||||
-dev-root-token-id="$token" \
|
||||
-dev-listen-address=${cfg.address}:${toString cfg.port}
|
||||
'';
|
||||
in {
|
||||
|
||||
options.services.vault-rustguac = {
|
||||
@@ -85,43 +99,30 @@ in {
|
||||
"d /etc/vault 0750 ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
# Config
|
||||
environment.etc."vault/config.hcl".text = ''
|
||||
${if cfg.mode == "dev" then ''
|
||||
storage "inmem" {}
|
||||
# Config : uniquement utile en mode "ha" (le mode dev ignore un HCL).
|
||||
environment.etc = lib.mkIf (cfg.mode == "ha") {
|
||||
"vault/config.hcl".text = ''
|
||||
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"}
|
||||
}
|
||||
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)}
|
||||
}
|
||||
cluster_addr = "${cfg.ha.clusterAddr}"
|
||||
api_addr = "${cfg.ha.apiAddr}"
|
||||
|
||||
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
|
||||
'';
|
||||
ui = ${boolToString cfg.ui}
|
||||
disable_mlock = true
|
||||
'';
|
||||
};
|
||||
|
||||
# Systemd service
|
||||
systemd.services.vault = {
|
||||
@@ -130,7 +131,10 @@ in {
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${pkgs.vault}/bin/vault server -config /etc/vault/config.hcl";
|
||||
ExecStart = if cfg.mode == "dev" then
|
||||
"${devScript}"
|
||||
else
|
||||
"${pkgs.vault}/bin/vault server -config /etc/vault/config.hcl";
|
||||
Restart = "on-failure";
|
||||
RestartSec = 5;
|
||||
LimitNOFILE = 65536;
|
||||
@@ -140,8 +144,9 @@ in {
|
||||
};
|
||||
};
|
||||
|
||||
# Firewall
|
||||
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
||||
# Le listener est en loopback (cfg.address par defaut) : rien a ouvrir au
|
||||
# firewall, et surtout pas pour un Vault en mode dev qui detient un root
|
||||
# token.
|
||||
|
||||
# Auto-unseal for dev mode (already unsealed)
|
||||
# For HA, you'd need to run: vault operator init && vault operator unseal
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
config,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
@@ -53,35 +54,54 @@
|
||||
services.hermes.enableDiscordToken = true;
|
||||
|
||||
# ========================================================================
|
||||
# ACME/Let's Encrypt : PLUS UTILISE sur tower. Le DNS public
|
||||
# (*.marcpartensky.com) est un wildcard vers le VPS 104.129.12.158 (Pangolin),
|
||||
# donc un challenge http-01 ne peut jamais atteindre tower (derriere NAT) :
|
||||
# chaque vhost ACME finissait en « order-renew ... failed », et un contact
|
||||
# admin@example.com est de toute facon refuse par Let's Encrypt
|
||||
# (« contact email has forbidden domain example.com »).
|
||||
# Les vhosts nginx locaux ci-dessous sont donc en HTTP loopback, destines a etre
|
||||
# exposes par une ressource Pangolin/newt qui porte, elle, le TLS.
|
||||
# Si un jour l'ACME revient ici : security.acme.defaults.email = "<vrai email>";
|
||||
# (l'ancienne option security.acme.email est depreciee) + acceptTerms = true.
|
||||
|
||||
# RUSTGUAC - Gateway Rust (remplace Guacamole Java)
|
||||
# ========================================================================
|
||||
services.rustguac = {
|
||||
enable = true;
|
||||
host = "127.0.0.1";
|
||||
port = 8089;
|
||||
guacd = { host = "127.0.0.1"; port = 4822; tls = true; };
|
||||
# guacd n'a pas de TLS ici : il ecoute en loopback, et rustguac doit parler
|
||||
# le meme protocole que lui (tls = true des deux cotes, ou false des deux
|
||||
# cotes, sinon la connexion guacamole ne s'etablit pas).
|
||||
guacd = { host = "127.0.0.1"; port = 4822; tls = false; };
|
||||
vault = {
|
||||
address = "http://127.0.0.1:8200";
|
||||
token = "/run/secrets/vault-rustguac-root-token";
|
||||
rootTokenFile = "/run/secrets/vault-rustguac-root-token";
|
||||
mountPath = "secret";
|
||||
connectionsPath = "rustguac/connections";
|
||||
};
|
||||
oidc = {
|
||||
issuerUrl = "https://auth.example.com"; # TODO: remplacer par ton OIDC
|
||||
clientId = "rustguac";
|
||||
clientSecret = "/run/secrets/rustguac-oidc-client-secret";
|
||||
redirectUrl = "https://guac.example.com/api/oidc/callback";
|
||||
scopes = [ "openid" "profile" "email" "groups" ];
|
||||
usernameClaim = "email";
|
||||
groupsClaim = "groups";
|
||||
basePath = "rustguac";
|
||||
};
|
||||
recording = {
|
||||
enable = true;
|
||||
storagePath = "/var/lib/rustguac/recordings";
|
||||
maxSize = "10G";
|
||||
maxAge = "30d";
|
||||
path = "/var/lib/rustguac/recordings";
|
||||
maxDiskPercent = 80;
|
||||
};
|
||||
rateLimit = { enabled = true; requestsPerMinute = 120; };
|
||||
# OIDC DESACTIVE (29/09/2026) : rustguac 1.10.0 attend redirect_uri (pas
|
||||
# redirect_url), default_role, extra_scopes, et un client_secret LITTERAL ou
|
||||
# la variable OIDC_CLIENT_SECRET (pas un chemin de fichier). Surtout, il faut
|
||||
# d'abord creer l'app OIDC dans Zitadel : client "rustguac", redirect
|
||||
# https://guac.marcpartensky.com/auth/callback, puis exposer le vhost via une
|
||||
# ressource Pangolin. Pour l'activer :
|
||||
# oidc = { issuerUrl = "https://auth.marcpartensky.com"; clientId = "rustguac";
|
||||
# clientSecretFile = "/run/secrets/rustguac-oidc-client-secret";
|
||||
# redirectUri = "https://guac.marcpartensky.com/auth/callback";
|
||||
# defaultRole = "operator"; extraScopes = [ "groups" ]; };
|
||||
# En attendant, l'auth est par cle API (`rustguac add-admin`).
|
||||
oidc = null;
|
||||
# Pas de rate limiting ici : nginx est devant, et rustguac le recommande
|
||||
# desactive derriere un reverse proxy.
|
||||
rateLimit = false;
|
||||
trustedProxies = [ "127.0.0.1/32" ];
|
||||
};
|
||||
|
||||
# ========================================================================
|
||||
@@ -90,7 +110,10 @@
|
||||
services.kasmvnc = {
|
||||
enable = true;
|
||||
user = "marc";
|
||||
display = ":1";
|
||||
# :1 est DEJA pris par la session graphique de marc (Xwayland :1) :
|
||||
# kasmvncserver refuse alors de demarrer (« tower:1 is taken because of
|
||||
# /tmp/.X1-lock ») et l'unite boucle jusqu'au start-limit-hit.
|
||||
display = ":20";
|
||||
geometry = "1920x1080";
|
||||
port = 8443;
|
||||
tls = {
|
||||
@@ -98,7 +121,7 @@
|
||||
keyFile = "/run/secrets/kasmvnc-key-pem";
|
||||
};
|
||||
passwordFile = "/home/marc/.kasmpasswd";
|
||||
desktop = "lxqt-wayland";
|
||||
desktop = "lxqt";
|
||||
};
|
||||
|
||||
# ========================================================================
|
||||
@@ -107,6 +130,48 @@
|
||||
services.vault-rustguac = {
|
||||
enable = true;
|
||||
mode = "dev";
|
||||
# /home/marc/.kasmpasswd : fichier au format kasmvncpasswd. Attention,
|
||||
# kasmvncpasswd n'a PAS d'option -f et refuse de tourner sans terminal
|
||||
# (« getpassword error: Inappropriate ioctl for device ») ; il lit le mot de
|
||||
# passe sur stdin quand on le lui donne deux fois. L'activation tourne donc en
|
||||
# root, cree le fichier une fois, puis le passe a marc. Le mot de passe vit
|
||||
# dans secrets/kasmvnc.yml (`sops -d secrets/kasmvnc.yml` pour le lire).
|
||||
# -w -o (write + owner) donnent a marc une session utilisable : sans champ de
|
||||
# permission kasmvncserver annonce « Users configured: marc (can only view) »,
|
||||
# soit une session en lecture seule. (Mesure : l'authentification HTTP passe
|
||||
# quand meme sans ces flags, 200 avec le bon mot de passe et 401 avec un
|
||||
# mauvais ; un 401 sur un mot de passe correct signifie que l'utilisateur
|
||||
# n'existe pas dans le fichier, pas que les flags manquent.)
|
||||
# La regeneration est pilotee par une empreinte du secret : le fichier n'est
|
||||
# reecrit que si le mot de passe a change, sinon chaque switch relancerait la
|
||||
# session en cours pour rien (kasmvncserver ne lit ce fichier qu'au demarrage,
|
||||
# donc un changement de secret impose un restart). Le test de fumee final ne
|
||||
# journalise QUE le code HTTP, jamais le mot de passe.
|
||||
system.activationScripts.kasmvncPasswd = ''
|
||||
if [ -r /run/secrets/kasmvnc-password ]; then
|
||||
pw="$(cat /run/secrets/kasmvnc-password)"
|
||||
want="$(printf %s "$pw" | ${pkgs.coreutils}/bin/sha256sum | ${pkgs.coreutils}/bin/cut -d' ' -f1)"
|
||||
have="$(cat /var/lib/kasmvnc/password.sha256 2>/dev/null || true)"
|
||||
if [ "$want" != "$have" ]; then
|
||||
printf '%s\n%s\n' "$pw" "$pw" | ${config.services.kasmvnc.package}/bin/kasmvncpasswd -u marc -w -o /home/marc/.kasmpasswd || true
|
||||
chmod 600 /home/marc/.kasmpasswd 2>/dev/null || true
|
||||
chown marc:users /home/marc/.kasmpasswd 2>/dev/null || true
|
||||
install -d -m 0755 /var/lib/kasmvnc
|
||||
printf '%s' "$want" > /var/lib/kasmvnc/password.sha256
|
||||
${pkgs.systemd}/bin/systemctl --user -M marc@.host reset-failed kasmvnc.service 2>/dev/null || true
|
||||
${pkgs.systemd}/bin/systemctl --user -M marc@.host restart kasmvnc.service 2>/dev/null || true
|
||||
code="000"
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
code="$(${pkgs.curl}/bin/curl -sk -o /dev/null -w '%{http_code}' -u "marc:$pw" https://127.0.0.1:8443/ 2>/dev/null || true)"
|
||||
if [ "$code" = "200" ]; then break; fi
|
||||
sleep 2
|
||||
done
|
||||
echo "kasmvnc: mot de passe (re)genere, authentification locale -> HTTP $code" >&2
|
||||
fi
|
||||
unset pw want have
|
||||
fi
|
||||
'';
|
||||
|
||||
address = "127.0.0.1";
|
||||
port = 8200;
|
||||
devRootToken = "/run/secrets/vault-rustguac-root-token";
|
||||
@@ -118,26 +183,58 @@
|
||||
# ========================================================================
|
||||
services.rustdesk = {
|
||||
enable = true;
|
||||
hbbs = { enable = true; port = 21115; };
|
||||
hbbr = { enable = true; port = 21116; apiPort = 21118; };
|
||||
hbbs = { enable = true; idPort = 21116; };
|
||||
hbbr = { enable = true; port = 21117; };
|
||||
openFirewall = true;
|
||||
};
|
||||
# Ports reels de la 1.1.16 : hbbs 21115 (test NAT) / 21116 (ID, TCP+UDP) /
|
||||
# 21118 (websocket), hbbr 21117 (relais) / 21119 (websocket). Sans -k, hbbs
|
||||
# genere sa paire de cles dans /var/lib/rustdesk (id_ed25519*).
|
||||
# NOTE : tower est derriere NAT residentiel ; ces ports ne sont utiles qu'en
|
||||
# LAN tant que rien n'est redirige cote box (ou via le VPS).
|
||||
|
||||
# ========================================================================
|
||||
# NGINX - Reverse proxy avec ACME pour les deux services
|
||||
# NGINX - vhosts locaux (TLS porte par Pangolin/newt, pas par nginx ici)
|
||||
# ========================================================================
|
||||
services.nginx.rustguac = {
|
||||
enable = true;
|
||||
domain = "guac.example.com"; # TODO: ton domaine
|
||||
domain = "guac.marcpartensky.com";
|
||||
upstream = "http://127.0.0.1:8089";
|
||||
acme = true;
|
||||
acme = false;
|
||||
listenAddress = "127.0.0.1";
|
||||
listenPort = 8288;
|
||||
};
|
||||
|
||||
# Exposition publique via Pangolin (newt, site "tower") : kasm.marcpartensky.com
|
||||
# -> 127.0.0.1:8288, le vhost nginx local qui repart en https vers KasmVNC.
|
||||
# HTTP + SSO, comme noVNC (vnc.marcpartensky.com) : newt 1.12.4 suffit, le mode
|
||||
# VNC natif de Pangolin exigerait un connecteur > 1.13. Meme contrainte de
|
||||
# schema que services/newt : ce Pangolin attend `proxy-resources` + `protocol`
|
||||
# (`public-resources`/`mode` sont ignores en silence -> domaine en 404).
|
||||
# Pas de healthcheck : KasmVNC repond 401 sur / sans authentification, donc un
|
||||
# healthcheck HTTP marquerait la cible unhealthy et Pangolin la sortirait du
|
||||
# load-balancer (lecon du healthcheck noVNC dans services/newt).
|
||||
services.newt.blueprint.proxy-resources.kasm = {
|
||||
name = "kasm";
|
||||
protocol = "http";
|
||||
full-domain = "kasm.marcpartensky.com";
|
||||
auth.sso-enabled = true;
|
||||
targets = [
|
||||
{
|
||||
hostname = "127.0.0.1";
|
||||
port = 8288;
|
||||
method = "http";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
services.nginx.kasmvnc = {
|
||||
enable = true;
|
||||
domain = "kasm.example.com"; # TODO: ton domaine
|
||||
domain = "kasm.marcpartensky.com";
|
||||
upstream = "https://127.0.0.1:8443";
|
||||
acme = true;
|
||||
acme = false;
|
||||
listenAddress = "127.0.0.1";
|
||||
listenPort = 8289;
|
||||
};
|
||||
|
||||
# ========================================================================
|
||||
@@ -146,29 +243,42 @@
|
||||
sops.secrets = {
|
||||
vault-rustguac-root-token = {
|
||||
sopsFile = ../../secrets/tower.yml;
|
||||
owner = "root";
|
||||
owner = "marc";
|
||||
mode = "0400";
|
||||
};
|
||||
rustguac-oidc-client-secret = {
|
||||
# Lu par le service vault (User=vault) : owner root + 0400 rendait le
|
||||
# fichier illisible pour lui, donc `vault server -dev` sortait en EACCES.
|
||||
sopsFile = ../../secrets/tower.yml;
|
||||
owner = "root";
|
||||
owner = "vault";
|
||||
mode = "0400";
|
||||
};
|
||||
kasmvnc-cert-pem = {
|
||||
# Lu par le service rustguac lui-meme (User=rustguac).
|
||||
sopsFile = ../../secrets/tower.yml;
|
||||
owner = "root";
|
||||
owner = "rustguac";
|
||||
mode = "0400";
|
||||
};
|
||||
kasmvnc-key-pem = {
|
||||
sopsFile = ../../secrets/tower.yml;
|
||||
owner = "root";
|
||||
owner = "marc";
|
||||
mode = "0400";
|
||||
};
|
||||
"pangolin/api_key" = {
|
||||
sopsFile = ../../secrets/common.yml;
|
||||
# Mot de passe KasmVNC (web login), lu par le script d'activation cote root.
|
||||
kasmvnc-password = {
|
||||
sopsFile = ../../secrets/kasmvnc.yml;
|
||||
owner = "root";
|
||||
mode = "0400";
|
||||
};
|
||||
};
|
||||
|
||||
# User lingering pour KasmVNC systemd user service
|
||||
users.users.marc.linger = true;
|
||||
}
|
||||
} # "pangolin/api_key" : volontairement absent, la cle n'existe pas dans
|
||||
# secrets/common.yml (la declarer fait echouer tout le switch en
|
||||
# « the key 'pangolin' cannot be found »). Voir services/hermes/default.nix.
|
||||
|
||||
# hermes doit pouvoir lire les journaux (y compris les unites utilisateur de
|
||||
# marc, ex. kasmvnc) : sans ca, aucun diagnostic possible sans root, et sudo
|
||||
# n'est autorise que pour nixos-rebuild.
|
||||
users.users.hermes.extraGroups = [ "systemd-journal" ];
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
kasmvnc-password: ENC[AES256_GCM,data:nWz603CFpDptabxC1NOeWLitIjCzPa4onOvIxhJ+asU=,iv:rWp7H10pfPbN8a/ti+6/ZnEOqp2p6M1TzXyQa9nA6yQ=,tag:wp8XCDNAanZ0PW0wipty/g==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3QjYyQWVheTNJYjNLaHNz
|
||||
b1laZGlhdXRrM0o4RXUrMTVXUEFsVWpyYVNFCmtlWWk3OGNkWW5qaktIZkgzNVl4
|
||||
QXpWMnZqdjluTVp4SDQxeUZCOURTQmMKLS0tIGdUczM1ZUJhM0hrdTNpVzF3T3lV
|
||||
OGYxNXAvdjE4S3BKcVJUaStUbVZ1VlkKu+/dDDpIrxji+3D2GBh7gjNPqC80v+En
|
||||
zB1x5KuxDnCAHFg6me1O+1/U/exnHf8vajdsKMDskk43bph4iz8c+A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
||||
lastmodified: "2026-09-30T23:17:44Z"
|
||||
mac: ENC[AES256_GCM,data:sVxMPrPooqDN1TufskrKZSo7X2myuwmQiVdn0ZpmuIAZCDI7wdlgsBbEq8nkr1H+D9ni0sE51b6bkCtY9gGHNOW51wq5ZxpES9Lvyjn1BZDK/9C1BqLAIYlbDDnAAVS4xUlWjVpLQpCWLvknCNqexW+Yy9YF8HOcTCn4e1LsirY=,iv:fVtM3XmycJBDEu+k2MyCCMNuvYdNTk/9zhouUI1qOg0=,tag:YghF+Z2o5vkk6450pRgBXA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
+6
-2
@@ -26,6 +26,10 @@ vaultwarden_mcp_password: ENC[AES256_GCM,data:753uKZ5hw+XnzcY6,iv:zqvRoHpCGhNmHv
|
||||
discord_bot_env: ENC[AES256_GCM,data:wASaieXxptPIzVPPFzU4FIsw6S6ODwaPip8K/9+YUqU48oXY6LEdkTH73J/Fjigco5UNi7jdbrAXyBmd9xo+3cvWtdaOGyTvGzppsihf+04808Xe5+LvzI2cKEEbZQOk5oZGBVYV+55xbkeMsn/kFoZSjffpfsB9H+R9+p9nDVkrNxsfJLImx9/wSmVvaeVLZy+UphL6f3e1G7WIVFpI9Yxha8oPnEOH/nKTHdDS4QZglaCt7Q043V+6P2Lr8Jm1PhxIR+C8dfvjJOh1lRqACl5DUpdToMKS+Go1nLqUkHNic1V/2ojyfhw6Hs9fahTmJPdQ0zA1I8wLg+pANi8eKA9tHLnROwt5l/EULSeATCDoY8j0U4K3M2KWGLsyB/QpBlbt72mvXr0/6hNZxMBhsvppPRRkuYrKH+mlvYDWlu75YWFwBeSLwNxdji9817fi1s7itPbftZqHBamevYgU7bzhAGf1Q8ulmbJNmZyaAWUwK+aF0Q==,iv:Sfp3MtR0zZgvJIo+jTh+FaZ2sPD8WeOEbretFqYUvHw=,tag:GRjqToMEgh0PRKvtBQpP+Q==,type:str]
|
||||
nextcloud_mcp_env: ENC[AES256_GCM,data:rDplpbQdM79shZbLOUN5ugckTSzqGTL8ydwuyCD4nHYiBzLSFylnl3MAd48vdgUryEtd9pfJ,iv:gBk/iNXXIbatCeP5Xo6ORVeikwnnObaJyong1/h3iH0=,tag:HicAgZMH7ZxsnlvNOrNneg==,type:str]
|
||||
#ENC[AES256_GCM,data:oYy+tD9xS8HNlEJNgLEb4NfXURCY+4W3KndFgua3s3CW+rmuQMu2eeoi8qdXU/MAOG3pdwLGJPuEreF/nQg1OR4HNbEtFA==,iv:Ui4rXnnPNbwIqaeIeVN4CopaoNs+o0LwviYr9ye/YM0=,tag:cah42cgfR7cXOWw13oBotg==,type:comment]
|
||||
kasmvnc-cert-pem: ENC[AES256_GCM,data:uszmQk7rxtAh3Gzap7p7a8SjcXdN7H9aU8vpG1t/VgTsjG8nUOoZhgrPTtNGztFkU2WZawZ4eDiT3fiIQtv7r0l67PURYAQOXO3r+Gj+Jg1XOD+oHTvhmGuduOPW4Ns0ocgal/svr9j81Co0ZGziA20fsCvFniQcyMFDrfrgRrxj99AC+O2r1RN4Fz0C6Hqp1NZ8IPp0Qjyh2LzRn/FdDqNwE3TU9cIj231hFc6MMN2TTmmRZxT9gtbaC6V0UGqxw0ua4r15E9tULaQ8m9pMvx40sYw8Pk/EJ/b8coif9CWONtD11gmYscgJJ6jrCXMO//EP14/TmW/wjtmSFek+4/MN5SBjUJOCkrd1uBItji5nHyImQBT8dn+EnM8s4BOq+aF+moBMke3tUmv7XqarpkDEMygLiA6Jkm6nSVo970VhdmV57cPC8rAH+SvjD7cgbSHwIBUuHz2Z66yVxxhRV7ywvQebXAQ32Arye76cPEyiNSHWLTRq6iRdjFVELbu8te+1jJ2xqAR48mEfF99d8BAeFRUn1dgbX9nz1I4KBhhER4L3gGFqtByuvRIrKfCvVRcWvvJSpkfpKiET+eY9MxqlnXOgJBZUiLuoVZGRizK5Dv67xh2c1894UKHqZyT/RS3tC3QLiheD39vyUFtSc+N6wJz2Il1vLb4scXS1VC0Kpo46IHJVFQ6s7q+l4nuMxV6oSvpRWNQdL5BmhMUw8Wt9JFu/uiOUA1ZFg3Lm2HiB4fVr1et+RqayaNMOyHGE1OYxXZfhEfetUagvjinwYM0pL/XcMHlH8Eh3l7MG1GUyY7SilzslMELTMxeuT988ua9RPgBikt9AdW7HgvqwJkSt91Av9GVQaRLMv2m6ffRdesLGZpsefKM0IFFO1e06ohfiw9V1u8QQDJ49iQcSKDugxgrSGiVuEqG11JigxwOb4izqnJB4TGMfMsjzDst4bVpwR5lWS4JWdF1EFVYT4ivGesl+hIPbVmzKM9p5pro/DIBp1LyVRNMXj5kxmrSHeYv8uFrtyVIVfMzJr3vUiztVWjeKR6xUi31iXDH6E9vBBqRWbf+gtRIiZzqWfvdOFZYCZa9QcIpqjoH/yN7queg0MsEodGuipXaOVnHISsRZfFFzV4ex4q7W/Z6hqjadvys+eP1p7xwaRqdM6g1eUmThmYtsF0bJLPrUNxSsP+/3xi4mca0gK9FKVDM07WpWGDLZ3rJ1KShVM4zTy7Yha617vGhNb+G4A4NoOllRluJ+7mibrk3ylnvSv6NVNhyYVZSA6+7eL93uN5JekQ7QpoMA+iQqanLV1cxBfFP2KkXC77poSHJ4zD6OXjDqvUTyYEiq+Zc8ryotWa+kW+og8gCxH5vjnNhDJQoZJZ8b0oEFp07M8uDFwujQzeaOB9XPgjUbzoifGsfJn8ysZcytLSf7GesJS/qapYZZx0ZGGeGYGLnKBOcf2/F0IJiAX1hHCddtSF1GtkRUCcUQAAq1lOlQiuuqGxIw1zDqxVgjCTOSGxKtZUiQbmoEaxLvNiKEpmiKM/Q6WriOqijFEaPWyzu0XtY=,iv:mtBCKKnTw2pWi98q5DwjH3tFgEPX/Jvl9Ygs9BPyy38=,tag:+ecKTmPsgsuOkfk+RKn0Rw==,type:str]
|
||||
kasmvnc-key-pem: ENC[AES256_GCM,data:GtJWdK1pmGUlkPGkFD7CTogHkBJpHbs7o6tr11m0oaKeqTx49BkLliYP5q+Qe7DMvyglLzVC1urvBHBkLvnWyr+GZAFibQe/so8mEDMk8qreA+Khe7d4ARUTfj3QsIt9MWrq77lIBmJTs2/6y3fqG1wA59dJbbpxq/ZiQJlDFy3/uSYljh/SLvzVOmJsoWqw/j0ZLz2aomCqd1+D0p1hkdgtEzB6Xxh24qK+jsleTfNEc3Y/eXWKtdV1uiJsv9eQX7UlcTpUdEkIP+HWB+SUCIsTBsDffCf894y140AfzIj0ETwRNj1Bxxf3K2IGjmIGz/QdxPtJ96D1PK1+E2Fu5dWVAEQnTwTitwoDGNTtreofcJ3jUq0vG66qpMc3enAGPUinQElMw9IBtng3gkTV7oiP9yytLL9Rau/n4ao6/hjN7AfF59fgJqWB5SDZfn2CEV2vTGoJcuWcoXCPKgvvYM+Z+/wdPFV4Qwa9NBs1OGHHCWr448zuUHjLu0JiFgHrmdlxQAN1TYYKNBAk8c0F7N+BT5UBMzdmQuxwa4LVM11HOToO2FG3ZD/vzcBfRKgOvaNnA02LsMs2Re0342sIKAk4HpGHOaPYQ6T/MqF5t9zGvbjlj5RG7zyIXeZ6IQ1yEEWz7dmJTPc3lYF2C3tS5mHD1F6l3ugFn/x4a6FjYQxX6bVwwTAJT82PM+UzuAqpi0j1spas5oUFEE5MgmDRPNDiNULuhFgjoTpx7TPdlGsMaRGdZmGx2tOoN01PYcTwIBd5tkt8GFnalcrFurKSWlBSl0m9RmLsTtrAcBJNVGLMSMIJuTN4k4S1ADn/Mz4A85SLQkiQ8G2+Bswm5kQ3bOWrtCr/+0VU7lGT+3VCJTjyGqTirZlwjIGsnSNDa19rhN2aBhfb3y5xCCwjlgYJepAsRx2v5TruSPvokRkvj8K1vO6wnB13ExtGIXORsZqlH+nriO33EwpiG7RlvjG+bG8q8aYw/0gPl/lPgkSPwKVpNABOaLd6W4jQ+K/ZJfStlDMEfyryBJ1PtE9KKjxVK7yCfvGAz9GyjV+cLtDfj3O77ImC8w8G5cwo1rX9kVDmjXp2mRltzoT8xuvIRgJYKfx43nYT5kp6u3ZvZbAYzXCMd4dMxNMxkLr2t3HDXHR+2Rlqds/6zteUl/OZy9bqdP/97PCxglYjtCnzbg16XfLsR29zLIVMO4ZjFAozSYOZt/HfLiotFFjGUuOwekDhrRP0EFkoYBnqqxzO/NbIclvaTjIig5ddr7cGHERuGBf7BH3xHN9+d6YXFgZA5kOvEEzn5SjcDBW2T/M3U+bb0y/tOVqj3qq8NOxIb71sHIrMSTcKPHjb5R158lxn03tYNyymTMuGYv0dAt1an3Pva0quW+ZR7O6JDeffUcwv8uPkDkIguXEJfR5AaN79LotZIWb52OTxJ9+mldB6Pk4ZCr3sMkzDA1JfTJyza4OuLphRUL7IWT2/F7bYWJPSJXDUzsj1h08sfNvme3rSj87H4njqDlYb4AYMW6LvxtufiLRCC7WnCDTFlnXS0M3uGg5SxjLc5q1B72UzZhIUo9L2T845ge2OA+FssGohDi/HQ0SmIAb4Ohbzv8AL/4CY80n+klrdpXwtdRBTLc3HjoGEip/I2Axd4vkPvJLa3/PiwdFsjb1NkW5otJWIOiYwUaVZegfLNHq1kX+Ek9GzqFIIdSo/U7LiLdIay5fxLbeswb6tFaRmk3WJX+6ZLxHVxJrEpviONWenKPC8xeMPahDEhBGV43mTe1fqFqfSX8eVkO6Z3Em84mWmuoDHtKWHdBmmjD6+HYSa8T7xDvBlg2YyFSYNdV8K07z7kYZIQObrL+bzrpBvNJQSuQsaJZTpayO3E5soihTyzNA5+XRTkioc1hHUL7DEZ9Qvhz52VsfuU+DU4qmhL6GSuBMdYMh+mcw9VOt8pGbkdVWmwx305zfeaEWwQZi1/Bh5dFzIDEGpoWPrdOwazUPdnPaA7Rrt7309oZkRECRB4MG3vZmdRuQKNQcAbk1hfXcbG2HUfkAFfXurb82ZF6Vgb/wWFPRsJdLlx6NjY9rmjqL4XO+vP5KBum5J6sAZWwNs53Yc/ZzrWl/t1vuQUXHXjaNx4JSlErTSBhP1jGcwCEZrNNYid2sEW5uXZ3FNSTNVkfd/Z4q0xU0+6+n2yBVP5etn3pPQV7T9b2p9CXpK/bxaliLM6xGfTXOr5W4kpsJCGO8yVHI6ucKdARKf+MZyeYEuy/4i/0kagszPJ5rX+3oo,iv:fiVq04n/1h61OVuYENDGkGw4zA7a/Ad9JjZ3nOhX9FY=,tag:nGLLjEhM6MqBwolZIEk4DA==,type:str]
|
||||
vault-rustguac-root-token: ENC[AES256_GCM,data:dg7+P/rGjGotPPHpM/ZNZKakjMMTKAJWwg8FoWwXdZlbkkDAXaC6VK6lEG2m/cgcQw7xu2heAszt2VO0WSOtrw==,iv:W5ypyAD8bEaQuIkSV7UoiBuvhx/f7kbmJ6e/8j53sbc=,tag:sF04/AD370LbSHj4BBX+yg==,type:str]
|
||||
rustguac-oidc-client-secret: ENC[AES256_GCM,data:4IhFtSxzzZgTRM00OS386W6koSsO/Bs8vy0zctAVGI9HRfkViPk8nJ8SIrxyZ4n43bHyB9N6412dNwD1lJR47w==,iv:KdEHU4q4JABoxgdxqVOD/w9d453p5KAOJ427HGc0ugM=,tag:3GTDX+pR4MBzR+d8Ki1HJg==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
@@ -37,7 +41,7 @@ sops:
|
||||
Sg5uogPIZseNJou3o6R4uy8Mn6LvfmU9/NU3m75+PQHyyGkGL8k78g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
||||
lastmodified: "2026-09-27T20:53:34Z"
|
||||
mac: ENC[AES256_GCM,data:YgfIag2jQdwuWoAdishFhKXeEFDGc9wg76cLNjB84Yf76RUffomVCzskhyhip30ryZ3kQKiFi6CzhuLjl8U+Olcd0lBJxHzp9Lh83JCCGy17RnCSMrK3UXXhwYHruGoaTHHIlEbyKCdJLXR4VAhsABt23F/c1U9ZUhHmqsb4NxU=,iv:Pb5VifKtJpX71bTa4f6zBmOSY0uJSOUdQLaV0wk3sjs=,tag:KY4ZVFJBfpM2QqTXeV0l7w==,type:str]
|
||||
lastmodified: "2026-09-30T03:03:15Z"
|
||||
mac: ENC[AES256_GCM,data:ngI0KPObZadsE/6UtPKkN/xvBGWUemvwwOra3WtNg3i/ekPdySGZrPo1K2v3vvLaf0HqngctxxpvAxQIQ2b4x0kGRp7KslaPT8h1JCj6dNCFg06VLK+vEPIrr2J1ExVzbSsFtVGEYjgs76TjohbcRchYMU+wLR2TgP6sU48jnZc=,iv:xfqrVf2knxwDaS6sO0ADX07vS1kBkEwz3jCxIre4agI=,tag:NT/TzwJR+9nqL1UtV9qDOg==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
|
||||
Reference in New Issue
Block a user