feat(hermes-dashboard): terminal web hermes (Pangolin SSO)
Backend hermes-agent en mode dashboard (9120) + nginx host-rewrite (9121), ressource newt terminal.marcpartensky.com. Ports jamais ouverts au firewall : /api/pty est un shell.
This commit is contained in:
@@ -36,3 +36,7 @@ creation_rules:
|
||||
key_groups:
|
||||
- age:
|
||||
- *tower
|
||||
- path_regex: secrets/hermes-dashboard\.yml$
|
||||
key_groups:
|
||||
- age:
|
||||
- *tower
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
hermes_dashboard_token: ENC[AES256_GCM,data:GwVYozAwOZshLyCeDmi3vPQuztguEkWJG3crRjxGc+S7kk/f3kzx6A==,iv:Kw2j5BW1UtrrSRquF0F3IrrRQDivZa6Sx/rzFQJlI1w=,tag:KYheaNQWU+eyPY+ndSSFNw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0RVdNM2VOOVRBQkhwM3Va
|
||||
dmpueXduYmxTeGlhU0lrVWdBOCtjYlhhNmtZCm9GNmRiUVQ2a2I3OU5qeUNlR2wr
|
||||
SWg4ZHpJTGVOV3dPME9lWFpCYk0zekEKLS0tIGZ1ckJmd1VaZkR2a3duOHBHZkx3
|
||||
dUh1dmY4ZGhQc2tERVF5Vm44SUd0OTQKX75Br9KWCG3FfHAprPqGRrAkHeAyg96x
|
||||
lSKGHHD6cehSJ+0r1UwzaPGROCM38VNCap2nzOoV/owMku0I/Z2DUA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
||||
lastmodified: "2026-09-25T21:03:10Z"
|
||||
mac: ENC[AES256_GCM,data:UnrAaaphA8XiJdQ22udJq0ucjWDJ5ToaZMVP1CTxyIFvR+l+zzQo0dmzA/onPX7NFpc1nRTvPR3vuFlbTrU1kzZib5+BOCT5bqCF9cQ4hSa9OPfXiyEIX/zWCbYVffntNzT93sESaKPsdW8MwbzEAvQ0fsugC7OBF0/+xsUZAGc=,iv:7H2t5wcLoiZLEY58GpNucg7RCsiMCId5zN69Z4UvgCM=,tag:JAB8B3uDPIxIFb5Gy88d3Q==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.3
|
||||
@@ -0,0 +1,104 @@
|
||||
# services/hermes-dashboard/default.nix
|
||||
# Terminal web + panneau d'admin de Hermes sur tower.
|
||||
#
|
||||
# Rien à packager : le backend est déjà dans l'input `hermes-agent` du flake.
|
||||
# `hermes serve` et `hermes dashboard` sont le MÊME point d'entrée, à un flag
|
||||
# près, et les deux exposent les sockets /api/ws et /api/pty. C'est /api/pty
|
||||
# qui est le terminal web ; `dashboard` ajoute en plus l'interface navigateur
|
||||
# sur le même port. Un seul des deux modes peut tourner par instance.
|
||||
#
|
||||
# Chaîne : Pangolin (terminal.marcpartensky.com, SSO)
|
||||
# -> newt (site tower)
|
||||
# -> 127.0.0.1:9121 (nginx, réécrit Host: 127.0.0.1:9120)
|
||||
# -> 127.0.0.1:9120 (hermes-backend.service, mode dashboard).
|
||||
#
|
||||
# Le backend bindé en loopback ne déclenche PAS la garde d'authentification du
|
||||
# dashboard (elle ne s'active que sur une adresse non-loopback) : c'est le SSO
|
||||
# Pangolin qui protège l'accès, exactement comme la ressource noVNC de tower.
|
||||
# Ne JAMAIS exposer 9120 ou 9121 au firewall : /api/pty est un shell.
|
||||
#
|
||||
# ⚠ Le PTY hérite du durcissement de l'unité hermes-backend (User=hermes).
|
||||
# Sans le `NoNewPrivileges = false` posé dans services/hermes/default.nix, le
|
||||
# shell obtenu n'a aucun sudo, pas même le nixos-rebuild NOPASSWD.
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
backendPort = 9120;
|
||||
proxyPort = 9121;
|
||||
backendUrl = "http://127.0.0.1:${toString backendPort}";
|
||||
backendHost = "127.0.0.1:${toString backendPort}";
|
||||
in {
|
||||
# Token de session stable : sans lui le backend en régénère un à chaque
|
||||
# démarrage, que personne d'autre ne peut connaître (donc aucun client
|
||||
# externe ne peut se rattacher à CE backend).
|
||||
sops.secrets."hermes_dashboard_token" = {
|
||||
sopsFile = ../../secrets/hermes-dashboard.yml;
|
||||
key = "hermes_dashboard_token";
|
||||
owner = config.services.hermes-agent.user;
|
||||
group = config.services.hermes-agent.group;
|
||||
mode = "0400";
|
||||
};
|
||||
|
||||
services.hermes-agent.backend = {
|
||||
mode = "dashboard";
|
||||
host = "127.0.0.1";
|
||||
port = backendPort;
|
||||
sessionTokenFile = config.sops.secrets."hermes_dashboard_token".path;
|
||||
};
|
||||
|
||||
# Proxy local : le backend refuse toute requête dont le Host diffère de
|
||||
# l'adresse sur laquelle il a bindé (défense anti-DNS-rebinding), d'où la
|
||||
# réécriture. Même mécanique que services/hermes-pocket.
|
||||
services.nginx.virtualHosts."hermes-dashboard-proxy" = {
|
||||
listen = [
|
||||
{
|
||||
addr = "127.0.0.1";
|
||||
port = proxyPort;
|
||||
}
|
||||
];
|
||||
# `/` couvre l'app web, /api/ws et /api/pty ; proxyWebsockets pose les
|
||||
# en-têtes Upgrade/Connection dont le PTY a besoin.
|
||||
locations."/" = {
|
||||
proxyPass = backendUrl;
|
||||
proxyWebsockets = true;
|
||||
extraConfig = ''
|
||||
proxy_set_header Host ${backendHost};
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_buffering off;
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# Ressource publique Pangolin, appliquée par le site qui porte le blueprint
|
||||
# (donc tower) : `site` est omis volontairement, cf. services/newt.
|
||||
# NB : cle `proxy-resources` + `protocol` (schema attendu par le Pangolin du
|
||||
# VPS), cf. le commentaire detaille dans services/newt.
|
||||
services.newt.blueprint.proxy-resources.hermes-terminal = {
|
||||
name = "Terminal web Hermes";
|
||||
protocol = "http";
|
||||
full-domain = "terminal.marcpartensky.com";
|
||||
auth.sso-enabled = true;
|
||||
targets = [
|
||||
{
|
||||
hostname = "127.0.0.1";
|
||||
port = proxyPort;
|
||||
method = "http";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Garde-fou : ces ports ne doivent jamais être ouverts sur le LAN.
|
||||
assertions = [
|
||||
{
|
||||
assertion = !(lib.elem backendPort config.networking.firewall.allowedTCPPorts);
|
||||
message = "hermes-dashboard : le port ${toString backendPort} (/api/pty = shell) ne doit pas être ouvert au firewall.";
|
||||
}
|
||||
{
|
||||
assertion = !(lib.elem proxyPort config.networking.firewall.allowedTCPPorts);
|
||||
message = "hermes-dashboard : le port ${toString proxyPort} (proxy du /api/pty) ne doit pas être ouvert au firewall.";
|
||||
}
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user