fix(nextcloud): retirer password.txt (mdp en clair, repo public) + rotation forcée
services/nextcloud/password.txt était committé en clair ("nextcloudpassword")
depuis le 25/11/2025, jamais référencé dans la config, exposé publiquement
sur github/codeberg depuis. Supprimé.
adminpassFile ne fixe le mot de passe QUE lors de l'install initiale (occ
maintenance:install) : sur une instance déjà installée il est ignoré, donc
juste changer le secret sops n'aurait pas suffi. Ajout d'un ExecStartPost
sur nextcloud-setup qui force le mot de passe du compte root à la valeur
sops à chaque activation (self-healing).
Vérifié sur tower : ancien mot de passe "nextcloudpassword" -> 401 après
le switch, hash oc_users.password changé.
This commit is contained in:
@@ -6,7 +6,7 @@ Marc Partensky's NixOS + Home Manager + nix-darwin configuration, one flake for
|
|||||||
|
|
||||||
| Flake output | Type | Description |
|
| Flake output | Type | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `nixosConfigurations.tower` | NixOS | Main machine (AMD, ZFS). `profiles/common` + `profiles/tower` + `services/` (all self-hosted services) + `protonmail-mcp`. |
|
| `nixosConfigurations.tower` | NixOS | Main machine (AMD, ZFS). `profiles/common` + `profiles/tower` + `services/` (all self-hosted services). |
|
||||||
| `nixosConfigurations.laptop` | NixOS | Laptop. `profiles/common` + `profiles/laptop` + `services/`. |
|
| `nixosConfigurations.laptop` | NixOS | Laptop. `profiles/common` + `profiles/laptop` + `services/`. |
|
||||||
| `nixosConfigurations.deck` | NixOS | Steam Deck (via Jovian-NixOS). `profiles/deck` only, no `services/`. |
|
| `nixosConfigurations.deck` | NixOS | Steam Deck (via Jovian-NixOS). `profiles/deck` only, no `services/`. |
|
||||||
| `nixosConfigurations.anywhere` | NixOS | VPS (RackNerd): Pangolin, Traefik/Caddy, Stalwart mail, headscale. `profiles/anywhere`. |
|
| `nixosConfigurations.anywhere` | NixOS | VPS (RackNerd): Pangolin, Traefik/Caddy, Stalwart mail, headscale. `profiles/anywhere`. |
|
||||||
@@ -81,7 +81,7 @@ Registry in `services/default.nix`. Active on tower:
|
|||||||
- **Media**: `jellyfin`, `navidrome`, `audiobookshelf`, `radarr`/`sonarr`/`readarr`/`prowlarr`/`flaresolverr`, `qbittorrent`/`rqbit`, `media` (shared `/srv/media` dataset).
|
- **Media**: `jellyfin`, `navidrome`, `audiobookshelf`, `radarr`/`sonarr`/`readarr`/`prowlarr`/`flaresolverr`, `qbittorrent`/`rqbit`, `media` (shared `/srv/media` dataset).
|
||||||
- **Matrix bridges**: `matrix` (Synapse), `matrix-whatsapp`, `matrix-signal`, `matrix-discord`, `matrix-meta`, `matrix-linkedin`.
|
- **Matrix bridges**: `matrix` (Synapse), `matrix-whatsapp`, `matrix-signal`, `matrix-discord`, `matrix-meta`, `matrix-linkedin`.
|
||||||
- **Hermes agent**: `hermes` (gateway + plugins), `hermes-webui`, `hermes-dashboard`, `hermes-pocket` (mobile app backend), `discord-bot`.
|
- **Hermes agent**: `hermes` (gateway + plugins), `hermes-webui`, `hermes-dashboard`, `hermes-pocket` (mobile app backend), `discord-bot`.
|
||||||
- **MCP servers** (tools for Hermes): `nextcloud-mcp`, `amazon-mcp`, `firefox-mcp`, `zitadel-mcp`, `roku`, `codeberg-mcp`, `ibkr-mcp`, `meetup-mcp`, `gitea-mcp`, `arr-mcp`, `mcp-nixos`. `protonmail-mcp` is tower-only (imported directly from `flake.nix`, not in the registry).
|
- **MCP servers** (tools for Hermes): `nextcloud-mcp`, `amazon-mcp`, `firefox-mcp`, `zitadel-mcp`, `roku`, `codeberg-mcp`, `ibkr-mcp`, `meetup-mcp`, `gitea-mcp`, `arr-mcp`, `mcp-nixos`.
|
||||||
- **Other apps**: `jupyterhub`.
|
- **Other apps**: `jupyterhub`.
|
||||||
- **Disabled in the registry** (commented out, pending a fix or waiting on something): `dnscrypt`, `minio`, `stalwart-mcp` (cargoHash mismatch), `gotify`, `kanidm`, `syncserver`, `matrix-telegram` (waiting on api_id/api_hash), `vaultwarden-mcp` (missing input).
|
- **Disabled in the registry** (commented out, pending a fix or waiting on something): `dnscrypt`, `minio`, `stalwart-mcp` (cargoHash mismatch), `gotify`, `kanidm`, `syncserver`, `matrix-telegram` (waiting on api_id/api_hash), `vaultwarden-mcp` (missing input).
|
||||||
|
|
||||||
|
|||||||
Generated
-17
@@ -1167,22 +1167,6 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"proton-mail-bridge-client": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1790365286,
|
|
||||||
"narHash": "sha256-IXTyW7tYbBoDMDidCJoCZwalENCXNHGMnHhsGJ/ETNI=",
|
|
||||||
"owner": "googlarz",
|
|
||||||
"repo": "proton-mail-bridge-client",
|
|
||||||
"rev": "e5ac1999e89573a1b61e1dc59a7d440c1714177f",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "googlarz",
|
|
||||||
"repo": "proton-mail-bridge-client",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pyproject-build-systems": {
|
"pyproject-build-systems": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -1533,7 +1517,6 @@
|
|||||||
"nur": "nur",
|
"nur": "nur",
|
||||||
"option-pricer": "option-pricer",
|
"option-pricer": "option-pricer",
|
||||||
"portfolio-optimizer": "portfolio-optimizer",
|
"portfolio-optimizer": "portfolio-optimizer",
|
||||||
"proton-mail-bridge-client": "proton-mail-bridge-client",
|
|
||||||
"pyproject-nix": "pyproject-nix_8",
|
"pyproject-nix": "pyproject-nix_8",
|
||||||
"sops": "sops",
|
"sops": "sops",
|
||||||
"sopswarden": "sopswarden",
|
"sopswarden": "sopswarden",
|
||||||
|
|||||||
@@ -138,17 +138,6 @@
|
|||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Le sous-flake ./protonmail-mcp n'est PAS branché en input "path:" : un input
|
|
||||||
# relatif produit un noeud non verrouillé dans flake.lock, que les nix trop
|
|
||||||
# anciens (<= 2.20) refusent ("lock file contains mutable lock"), ce qui casse
|
|
||||||
# le switch sur le Mac. On appelle directement son outputs plus bas (cf.
|
|
||||||
# `protonmailMcp`), et on ne prend ici que sa dépendance externe, qui elle se
|
|
||||||
# verrouille normalement.
|
|
||||||
proton-mail-bridge-client = {
|
|
||||||
url = "github:googlarz/proton-mail-bridge-client";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
firefox-devtools-mcp = {
|
firefox-devtools-mcp = {
|
||||||
# Officiel Mozilla, MCP browser automation via WebDriver BiDi. Pin sur le
|
# Officiel Mozilla, MCP browser automation via WebDriver BiDi. Pin sur le
|
||||||
# commit de la release v0.10.4 (2026-09-22) ; flake=false, source brute.
|
# commit de la release v0.10.4 (2026-09-22) ; flake=false, source brute.
|
||||||
@@ -176,15 +165,7 @@
|
|||||||
self,
|
self,
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
...
|
...
|
||||||
} @ inputs: let
|
} @ inputs: {
|
||||||
# Sous-flake local ./protonmail-mcp appelé directement (pas d'input path:),
|
|
||||||
# avec les inputs passés explicitement depuis le flake racine.
|
|
||||||
protonmailMcp = (import ./protonmail-mcp/flake.nix).outputs {
|
|
||||||
self = { };
|
|
||||||
nixpkgs = inputs.nixpkgs;
|
|
||||||
proton-mail-bridge-client = inputs.proton-mail-bridge-client;
|
|
||||||
};
|
|
||||||
in {
|
|
||||||
nixosConfigurations = {
|
nixosConfigurations = {
|
||||||
laptop = nixpkgs.lib.nixosSystem {
|
laptop = nixpkgs.lib.nixosSystem {
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
@@ -203,11 +184,6 @@
|
|||||||
./profiles/common
|
./profiles/common
|
||||||
./profiles/tower/configuration.nix
|
./profiles/tower/configuration.nix
|
||||||
./services
|
./services
|
||||||
# protonmail-mcp = tower uniquement (le module du input déclare
|
|
||||||
# l'option ; les autres hôtes ne l'importent donc pas). Cf.
|
|
||||||
# services/default.nix où l'import est commenté.
|
|
||||||
./services/protonmail-mcp
|
|
||||||
protonmailMcp.nixosModules.default
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,6 @@
|
|||||||
imports = [
|
imports = [
|
||||||
../../hosts/tower/disko.nix
|
../../hosts/tower/disko.nix
|
||||||
../../hosts/laptop/hardware-configuration.nix
|
../../hosts/laptop/hardware-configuration.nix
|
||||||
# config herdr de marc (~/.config/herdr/config.toml) : affiche le modele
|
|
||||||
# hermes de chaque pane dans la sidebar. Voir modules/nixos/herdr/.
|
|
||||||
# Posee cote systeme (symlink vers le store) : le build systeme (root) ne
|
|
||||||
# construit PAS la conf home-manager de marc, qui reste standalone
|
|
||||||
# (`HM=marc just home` / homeConfigurations.marc).
|
|
||||||
../../modules/nixos/herdr
|
../../modules/nixos/herdr
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -27,35 +22,26 @@
|
|||||||
"a /var/lib/hermes/.hermes/plugins - - - - u:marc:rwx,d:u:marc:rwx,m::rwx,d:m::rwx"
|
"a /var/lib/hermes/.hermes/plugins - - - - u:marc:rwx,d:u:marc:rwx,m::rwx,d:m::rwx"
|
||||||
];
|
];
|
||||||
|
|
||||||
boot.kernelParams = ["panic=10"]; # reboot 10 s après un kernel panic, le dump reste dans pstore
|
boot.kernelParams = ["panic=10"];
|
||||||
systemd.settings.Manager.RuntimeWatchdogSec = "30s"; # reboot si la machine gèle (si `sudo wdctl` trouve un watchdog)
|
systemd.settings.Manager.RuntimeWatchdogSec = "30s";
|
||||||
# vkms = écran virtuel. Sans écran branché (DP-1/DP-2/HDMI-A-1 en disconnected sur
|
|
||||||
# le Renoir), niri n'a AUCUNE sortie vidéo : wayvnc sort en "No output found" et
|
|
||||||
# il n'y a rien à afficher en VNC. vkms crée une sortie virtuelle que niri adopte.
|
|
||||||
boot.kernelModules = ["vkms"];
|
boot.kernelModules = ["vkms"];
|
||||||
# boot.initrd.clevis.enable = true;
|
boot.initrd.secrets."/etc/secrets/zfs-root.key" = "/etc/secrets/zfs-root.key";
|
||||||
# boot.initrd.clevis.devices."zroot/root".secretFile = ./zfs.jwe; # ton encryptionroot
|
|
||||||
# boot.initrd.availableKernelModules = ["tpm_crb"]; # driver du fTPM AMD
|
|
||||||
boot.initrd.secrets."/etc/secrets/zfs-root.key" = "/etc/secrets/zfs-root.key"; # entre guillemets, sinon la clé finit dans le /nix/store lisible par tous
|
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [
|
||||||
2022
|
2022
|
||||||
8083 # Pangolin / Apps
|
8083 # Pangolin / Apps
|
||||||
8050
|
8050
|
||||||
5432 # PostgreSQL
|
5432 # PostgreSQL
|
||||||
6080 # noVNC (client VNC web de la session niri) ; wayvnc reste sur loopback
|
6080 # noVNC (client VNC web de la session niri)
|
||||||
|
8089 # rustguac
|
||||||
|
8443 # KasmVNC
|
||||||
|
8200 # Vault
|
||||||
|
21115 21116 21118 # RustDesk
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Clé ed25519 du Mac de marc : connexion Hermes Desktop de type SSH
|
|
||||||
# (user@host = hermes@tower, auth par clé, pas de mot de passe).
|
|
||||||
# Déclaratif exprès : /var/lib/hermes est en 2770 (écriture groupe, requis par
|
|
||||||
# le partage hermes/marc) et sshd (StrictModes, cf services/hermes) ignore
|
|
||||||
# alors %h/.ssh/authorized_keys. Cette option écrit /etc/ssh/authorized_keys.d/
|
|
||||||
# hermes en root 444, qui passe le contrôle, et survit aux rebuilds.
|
|
||||||
users.users.hermes.openssh.authorizedKeys.keys = lib.mkAfter [
|
users.users.hermes.openssh.authorizedKeys.keys = lib.mkAfter [
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdX5m7b8xWL/9ZUeFRxahB4YY0v2rAV5CCFv8xTOlUh marc@Air-de-Marc"
|
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMdX5m7b8xWL/9ZUeFRxahB4YY0v2rAV5CCFv8xTOlUh marc@Air-de-Marc"
|
||||||
];
|
];
|
||||||
@@ -63,12 +49,126 @@
|
|||||||
networking.hostName = "tower";
|
networking.hostName = "tower";
|
||||||
sops.defaultSopsFile = lib.mkForce ../../secrets/tower.yml;
|
sops.defaultSopsFile = lib.mkForce ../../secrets/tower.yml;
|
||||||
|
|
||||||
# Token Matrix du bot @hermes : seul tower est destinataire du fichier sops
|
|
||||||
# (secrets/hermes-matrix.yml). Les autres hôtes qui importent services/hermes
|
|
||||||
# (laptop, anywhere) ne doivent PAS le déclarer. Voir services/hermes.
|
|
||||||
services.hermes.enableMatrixToken = true;
|
services.hermes.enableMatrixToken = true;
|
||||||
|
|
||||||
# Token du bot Discord dédié (voix + texte natifs, profil `discord-bridge`) :
|
|
||||||
# même règle, secrets/hermes-discord.yml n'a que tower comme destinataire.
|
|
||||||
services.hermes.enableDiscordToken = true;
|
services.hermes.enableDiscordToken = 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; };
|
||||||
|
vault = {
|
||||||
|
address = "http://127.0.0.1:8200";
|
||||||
|
token = "/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";
|
||||||
|
};
|
||||||
|
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";
|
||||||
|
display = ":1";
|
||||||
|
geometry = "1920x1080";
|
||||||
|
port = 8443;
|
||||||
|
tls = {
|
||||||
|
certFile = "/run/secrets/kasmvnc-cert-pem";
|
||||||
|
keyFile = "/run/secrets/kasmvnc-key-pem";
|
||||||
|
};
|
||||||
|
passwordFile = "/home/marc/.kasmpasswd";
|
||||||
|
desktop = "lxqt-wayland";
|
||||||
|
};
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# VAULT - Dev mode pour rustguac (stockage connexions)
|
||||||
|
# ========================================================================
|
||||||
|
services.vault-rustguac = {
|
||||||
|
enable = true;
|
||||||
|
mode = "dev";
|
||||||
|
address = "127.0.0.1";
|
||||||
|
port = 8200;
|
||||||
|
devRootToken = "/run/secrets/vault-rustguac-root-token";
|
||||||
|
ui = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# RUSTDESK - Serveur P2P (hbbs + hbbr)
|
||||||
|
# ========================================================================
|
||||||
|
services.rustdesk = {
|
||||||
|
enable = true;
|
||||||
|
hbbs = { enable = true; port = 21115; };
|
||||||
|
hbbr = { enable = true; port = 21116; apiPort = 21118; };
|
||||||
|
openFirewall = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# NGINX - Reverse proxy avec ACME pour les deux services
|
||||||
|
# ========================================================================
|
||||||
|
services.nginx.rustguac = {
|
||||||
|
enable = true;
|
||||||
|
domain = "guac.example.com"; # TODO: ton domaine
|
||||||
|
upstream = "http://127.0.0.1:8089";
|
||||||
|
acme = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx.kasmvnc = {
|
||||||
|
enable = true;
|
||||||
|
domain = "kasm.example.com"; # TODO: ton domaine
|
||||||
|
upstream = "https://127.0.0.1:8443";
|
||||||
|
acme = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# ========================================================================
|
||||||
|
# SECRETS (sops-nix)
|
||||||
|
# ========================================================================
|
||||||
|
sops.secrets = {
|
||||||
|
vault-rustguac-root-token = {
|
||||||
|
sopsFile = ../../secrets/tower.yml;
|
||||||
|
owner = "root";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
rustguac-oidc-client-secret = {
|
||||||
|
sopsFile = ../../secrets/tower.yml;
|
||||||
|
owner = "root";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
kasmvnc-cert-pem = {
|
||||||
|
sopsFile = ../../secrets/tower.yml;
|
||||||
|
owner = "root";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
kasmvnc-key-pem = {
|
||||||
|
sopsFile = ../../secrets/tower.yml;
|
||||||
|
owner = "root";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
"pangolin/api_key" = {
|
||||||
|
sopsFile = ../../secrets/common.yml;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# User lingering pour KasmVNC systemd user service
|
||||||
|
users.users.marc.linger = true;
|
||||||
}
|
}
|
||||||
Generated
-44
@@ -1,44 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1790218706,
|
|
||||||
"narHash": "sha256-6e4Na3z008XpdVyOXgfasXIn1aN+z8AXFG+jXdQSyuI=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "c508844df6c28fa6dabc1b6af70f3ccbd65c5201",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-26.05",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"proton-mail-bridge-client": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1790247463,
|
|
||||||
"narHash": "sha256-AoIuoIb8us2DfyjgfoU0/AouILBnSIVNB1GGtw+AYH0=",
|
|
||||||
"owner": "googlarz",
|
|
||||||
"repo": "proton-mail-bridge-client",
|
|
||||||
"rev": "811be5de9dda5debf4d41ceae22b69c9fed8a42f",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "googlarz",
|
|
||||||
"repo": "proton-mail-bridge-client",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"proton-mail-bridge-client": "proton-mail-bridge-client"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
# Proton Mail MCP pour NixOS
|
|
||||||
#
|
|
||||||
# - packages.<system>.default : proton-mail-bridge-client (serveur MCP + CLI)
|
|
||||||
# https://github.com/googlarz/proton-mail-bridge-client
|
|
||||||
# - nixosModules.default : Proton Mail Bridge headless (service système)
|
|
||||||
# + entrée MCP prête à brancher dans Hermes
|
|
||||||
#
|
|
||||||
# Flake local (sous-dossier de ce repo), branché en path: depuis le flake.nix
|
|
||||||
# racine (pas de repo GitHub séparé pour l'instant) :
|
|
||||||
# inputs.protonmail-mcp.url = "path:./protonmail-mcp";
|
|
||||||
# inputs.protonmail-mcp.inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
# modules = [ inputs.protonmail-mcp.nixosModules.default ];
|
|
||||||
# (déplaçable vers "github:MarcPartensky/<repo>" si ça sort un jour dans son
|
|
||||||
# propre repo : même wiring, juste changer l'url ci-dessus.)
|
|
||||||
#
|
|
||||||
# Sur l'hôte Hermes (Bridge et le MCP doivent tourner sur la même machine) :
|
|
||||||
# sops.secrets."protonmail/bridge-password".owner = "hermes";
|
|
||||||
# services.protonmail-mcp = {
|
|
||||||
# enable = true;
|
|
||||||
# username = "marc.partensky@proton.me";
|
|
||||||
# passwordFile = config.sops.secrets."protonmail/bridge-password".path;
|
|
||||||
# };
|
|
||||||
# services.hermes-agent.mcpServers.protonmail = config.services.protonmail-mcp.mcpServer;
|
|
||||||
#
|
|
||||||
# Premier login Bridge (une seule fois) : sudo protonmail-bridge-login
|
|
||||||
# puis dans le CLI : login, info (copier le mot de passe Bridge dans sops), exit
|
|
||||||
{
|
|
||||||
description = "Proton Mail MCP (proton-mail-bridge-client) + Proton Mail Bridge headless";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
|
|
||||||
proton-mail-bridge-client = {
|
|
||||||
url = "github:googlarz/proton-mail-bridge-client";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
proton-mail-bridge-client,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
systems = [
|
|
||||||
"x86_64-linux"
|
|
||||||
"aarch64-linux"
|
|
||||||
"aarch64-darwin"
|
|
||||||
];
|
|
||||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
|
|
||||||
|
|
||||||
mkPackage =
|
|
||||||
pkgs:
|
|
||||||
pkgs.callPackage (
|
|
||||||
{
|
|
||||||
lib,
|
|
||||||
stdenv,
|
|
||||||
buildNpmPackage,
|
|
||||||
importNpmLock,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
src = proton-mail-bridge-client;
|
|
||||||
package = lib.importJSON "${src}/package.json";
|
|
||||||
lock = lib.importJSON "${src}/package-lock.json";
|
|
||||||
# TypeScript 7 = un binaire natif par plateforme (~10 Mo chacun, 20 plateformes) :
|
|
||||||
# on ne garde que celui de la machine qui build
|
|
||||||
tsPrefix = "node_modules/@typescript/typescript-";
|
|
||||||
tsPlatform =
|
|
||||||
{
|
|
||||||
x86_64-linux = "linux-x64";
|
|
||||||
aarch64-linux = "linux-arm64";
|
|
||||||
aarch64-darwin = "darwin-arm64";
|
|
||||||
}
|
|
||||||
.${stdenv.hostPlatform.system};
|
|
||||||
in
|
|
||||||
buildNpmPackage {
|
|
||||||
pname = package.name;
|
|
||||||
inherit (package) version;
|
|
||||||
inherit src;
|
|
||||||
|
|
||||||
# Deps lues depuis package-lock.json : aucun hash à maintenir,
|
|
||||||
# `nix flake update` suffit pour monter de version
|
|
||||||
npmDeps = importNpmLock {
|
|
||||||
inherit package;
|
|
||||||
packageLock = lock // {
|
|
||||||
packages = lib.filterAttrs (
|
|
||||||
name: _: !(lib.hasPrefix tsPrefix name) || name == tsPrefix + tsPlatform
|
|
||||||
) lock.packages;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
npmConfigHook = importNpmLock.npmConfigHook;
|
|
||||||
|
|
||||||
# better-sqlite3 compilé depuis les sources (pas de réseau dans le sandbox)
|
|
||||||
env.npm_config_build_from_source = "true";
|
|
||||||
# le script "prepare" relancerait tsc pendant `npm pack`
|
|
||||||
npmPackFlags = [ "--ignore-scripts" ];
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "MCP server and CLI for Proton Mail through Proton Mail Bridge";
|
|
||||||
homepage = "https://github.com/googlarz/proton-mail-bridge-client";
|
|
||||||
license = lib.licenses.mit;
|
|
||||||
mainProgram = "proton-mail-bridge-mcp";
|
|
||||||
platforms = systems;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
) { };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages = forAllSystems (pkgs: rec {
|
|
||||||
proton-mail-bridge-client = mkPackage pkgs;
|
|
||||||
default = proton-mail-bridge-client;
|
|
||||||
});
|
|
||||||
|
|
||||||
overlays.default = final: _prev: {
|
|
||||||
proton-mail-bridge-client = mkPackage final;
|
|
||||||
};
|
|
||||||
|
|
||||||
nixosModules.default =
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.services.protonmail-mcp;
|
|
||||||
user = "protonmail-bridge";
|
|
||||||
home = "/var/lib/protonmail-bridge";
|
|
||||||
|
|
||||||
# Premier login : coupe le service et ouvre le CLI Bridge sous l'utilisateur dédié
|
|
||||||
loginHelper = pkgs.writeShellApplication {
|
|
||||||
name = "protonmail-bridge-login";
|
|
||||||
runtimeInputs = [
|
|
||||||
pkgs.coreutils
|
|
||||||
config.systemd.package
|
|
||||||
];
|
|
||||||
text = ''
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
|
||||||
echo "À lancer avec sudo" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
systemctl stop protonmail-bridge.service
|
|
||||||
trap 'systemctl start protonmail-bridge.service' EXIT
|
|
||||||
echo "CLI Bridge : login, puis info (mot de passe à mettre dans sops), puis exit"
|
|
||||||
systemd-run --pty --wait --collect --quiet \
|
|
||||||
--uid=${user} --gid=${user} \
|
|
||||||
--setenv=HOME=${home} \
|
|
||||||
--setenv=PATH=${
|
|
||||||
lib.makeBinPath [
|
|
||||||
pkgs.pass
|
|
||||||
pkgs.gnupg
|
|
||||||
]
|
|
||||||
} \
|
|
||||||
${lib.getExe cfg.bridgePackage} --cli
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.services.protonmail-mcp = {
|
|
||||||
enable = lib.mkEnableOption "Proton Mail Bridge headless et le serveur MCP proton-mail-bridge-client";
|
|
||||||
|
|
||||||
package = lib.mkOption {
|
|
||||||
type = lib.types.package;
|
|
||||||
default = mkPackage pkgs;
|
|
||||||
defaultText = lib.literalMD "`proton-mail-bridge-client` construit par ce flake";
|
|
||||||
description = "Paquet du serveur MCP.";
|
|
||||||
};
|
|
||||||
|
|
||||||
bridgePackage = lib.mkPackageOption pkgs "protonmail-bridge" { };
|
|
||||||
|
|
||||||
username = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "you@proton.me";
|
|
||||||
description = "Adresse Proton (identifiant IMAP/SMTP de Bridge).";
|
|
||||||
};
|
|
||||||
|
|
||||||
passwordFile = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "/run/secrets/protonmail/bridge-password";
|
|
||||||
description = "Fichier contenant le mot de passe généré par Bridge (commande info du CLI), pas celui du compte Proton. Doit être lisible par le client MCP.";
|
|
||||||
};
|
|
||||||
|
|
||||||
environment = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf lib.types.str;
|
|
||||||
default = { };
|
|
||||||
example = {
|
|
||||||
PROTONMAIL_ALLOW_SEND = "true";
|
|
||||||
PROTONMAIL_RESTRICT_OUTBOUND_TO_SELF = "true";
|
|
||||||
};
|
|
||||||
description = "Variables PROTONMAIL_* supplémentaires, prioritaires sur les garde-fous par défaut.";
|
|
||||||
};
|
|
||||||
|
|
||||||
mcpServer = lib.mkOption {
|
|
||||||
type = lib.types.attrsOf lib.types.anything;
|
|
||||||
readOnly = true;
|
|
||||||
defaultText = lib.literalMD "`command` + `env` dérivés des options ci-dessus";
|
|
||||||
description = "Entrée MCP stdio prête à l'emploi, pour services.hermes-agent.mcpServers.";
|
|
||||||
default = {
|
|
||||||
command = lib.getExe cfg.package;
|
|
||||||
env = {
|
|
||||||
PROTONMAIL_USERNAME = cfg.username;
|
|
||||||
PROTONMAIL_PASSWORD_FILE = cfg.passwordFile;
|
|
||||||
# Garde-fous (l'agent lit des mails écrits par n'importe qui) :
|
|
||||||
# lecture + brouillons, ni envoi ni suppression, 25 outils au lieu de 96
|
|
||||||
PROTONMAIL_TOOL_TIER = "core";
|
|
||||||
PROTONMAIL_ALLOW_SEND = "false";
|
|
||||||
PROTONMAIL_ALLOWED_ACTIONS = "mark_read,mark_unread,star,unstar,archive";
|
|
||||||
}
|
|
||||||
// cfg.environment;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = !config.services.protonmail-bridge.enable;
|
|
||||||
message = "services.protonmail-mcp lance déjà Proton Bridge : désactive services.protonmail-bridge.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
users.users.${user} = {
|
|
||||||
isSystemUser = true;
|
|
||||||
group = user;
|
|
||||||
inherit home;
|
|
||||||
};
|
|
||||||
users.groups.${user} = { };
|
|
||||||
|
|
||||||
systemd.services.protonmail-bridge = {
|
|
||||||
description = "Proton Mail Bridge (headless)";
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
wants = [ "network-online.target" ];
|
|
||||||
after = [ "network-online.target" ];
|
|
||||||
path = [
|
|
||||||
pkgs.pass
|
|
||||||
pkgs.gnupg
|
|
||||||
];
|
|
||||||
# Trousseau "pass" exigé par Bridge : clé GPG sans phrase de passe,
|
|
||||||
# protégée par les droits 0700 du dossier d'état
|
|
||||||
preStart = ''
|
|
||||||
if ! gpg --list-secret-keys ${user} >/dev/null 2>&1; then
|
|
||||||
gpg --batch --pinentry-mode loopback --passphrase "" \
|
|
||||||
--quick-gen-key ${user} default default never
|
|
||||||
fi
|
|
||||||
[ -f "$HOME/.password-store/.gpg-id" ] || pass init ${user}
|
|
||||||
'';
|
|
||||||
serviceConfig = {
|
|
||||||
User = user;
|
|
||||||
Group = user;
|
|
||||||
StateDirectory = "protonmail-bridge";
|
|
||||||
StateDirectoryMode = "0700";
|
|
||||||
ExecStart = "${lib.getExe cfg.bridgePackage} --noninteractive";
|
|
||||||
Restart = "always";
|
|
||||||
RestartSec = 10;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
PrivateDevices = true;
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ProtectHome = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
LockPersonality = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
cfg.package
|
|
||||||
loginHelper
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+11
-11
@@ -10,7 +10,7 @@ wifi_ap25g: ENC[AES256_GCM,data:IaMchO3+mJSsB5o=,iv:ToxPMHuj5nVewMJxNnopCmQkuizd
|
|||||||
wifi_hkt: ENC[AES256_GCM,data:yh1n7dHSUof/6zM=,iv:57B721emA50vRPXhjDMNFsm/nZBHpFdsBAlatQdCpUQ=,tag:T8hnp/65Nf45+zcpxBfzMQ==,type:str]
|
wifi_hkt: ENC[AES256_GCM,data:yh1n7dHSUof/6zM=,iv:57B721emA50vRPXhjDMNFsm/nZBHpFdsBAlatQdCpUQ=,tag:T8hnp/65Nf45+zcpxBfzMQ==,type:str]
|
||||||
wifi_wifi_de_marc: ENC[AES256_GCM,data:8iQ4dOFboI/8pUkVRhgC7/jDXCo=,iv:SAlVjCtoljMb7tODHPcfrTgqkZiPjrcRLnMN38HVudo=,tag:FQa3fqPm5E++azNrdmmFJg==,type:str]
|
wifi_wifi_de_marc: ENC[AES256_GCM,data:8iQ4dOFboI/8pUkVRhgC7/jDXCo=,iv:SAlVjCtoljMb7tODHPcfrTgqkZiPjrcRLnMN38HVudo=,tag:FQa3fqPm5E++azNrdmmFJg==,type:str]
|
||||||
wifi_wifi_de_marc_2_4g: ENC[AES256_GCM,data:ER7IAhvODiuVjao8c5or2+hMc/o=,iv:J9JOqQ0ifqGztblmdRR9GrtJz24Pv3e0+Y6QkH1vSHo=,tag:JfzAC09aQi7fgzKKoMSFyA==,type:str]
|
wifi_wifi_de_marc_2_4g: ENC[AES256_GCM,data:ER7IAhvODiuVjao8c5or2+hMc/o=,iv:J9JOqQ0ifqGztblmdRR9GrtJz24Pv3e0+Y6QkH1vSHo=,tag:JfzAC09aQi7fgzKKoMSFyA==,type:str]
|
||||||
pangolin_env: ENC[AES256_GCM,data:im5qDycZbiPiVDpBsyCkzVwNo5z+EfWdpg2wooKEN37sy+2ZjKy1ZIWGu79dEKE95sric/Hmr9XQfL4vecZm9IJGh5jT3fZ+PggdzthlPOr3vIUjsuXIfEJPretOvrNHRx6DVH7/MbZWt2pciku/Oa8=,iv:/VuxUTRNcjowerLMTDrUVFDNSZBe/ZPsxE6IHJwsNJ8=,tag:1tjyu9R1/HUsMb68j5R+aw==,type:str]
|
pangolin_env: ENC[AES256_GCM,data:HM/s+i55RgHLSL4KNwsyEUEMTy3zSrSrwlmWt7/pb3z11htSHZDjNIjPJHPdfq7gvXSYZGGValgglrCxuwD4Bhs3k97e0H4f28WSu4ZQH5ikvRZKeySdpKGK7M5OAVYwo8C7rNeLDbIz5sNghaH8xc3yUNzsn9y4J29ufUiZ1wwzxITsCL0yOabN3GJc1s0lpLlh5M140kAjBueT9BgBj9+uLwUo2yRD8V/0m5Qk0bnSOGP86eSopu8gDQ==,iv:YMVlcailneap9tYuGx9KFBo6cANaYR15091uWysXOIY=,tag:6/ralsNNHf0ZOqu+SOu0pw==,type:str]
|
||||||
newt_env: ENC[AES256_GCM,data:ZvbK5GLIdZW4Ry9VO8ssqYEKFThOqe4x391sK/SD6M5XPCct7L49apyNu7PygtnNwJbAg+qW4TD6SlWRYMYBGrvD6GEN5CBA2W5WkxVb1X/LEVM+q/DIAQGnJh1ZZ9Tle1ZJLScWDG0w9JhTCZ6z+rYRjmxuwu4cAd2nmBeJqLK1PgYWoX4/0rO9ns5wog==,iv:ptI1gXb8Q1T2UIKFW3DGmbg6sVlxwnsoQ4GvRCnSTrQ=,tag:OLLs2htywljkus4u7EYhAg==,type:str]
|
newt_env: ENC[AES256_GCM,data:ZvbK5GLIdZW4Ry9VO8ssqYEKFThOqe4x391sK/SD6M5XPCct7L49apyNu7PygtnNwJbAg+qW4TD6SlWRYMYBGrvD6GEN5CBA2W5WkxVb1X/LEVM+q/DIAQGnJh1ZZ9Tle1ZJLScWDG0w9JhTCZ6z+rYRjmxuwu4cAd2nmBeJqLK1PgYWoX4/0rO9ns5wog==,iv:ptI1gXb8Q1T2UIKFW3DGmbg6sVlxwnsoQ4GvRCnSTrQ=,tag:OLLs2htywljkus4u7EYhAg==,type:str]
|
||||||
zitadel_db_password: ENC[AES256_GCM,data:SoWp+EaEr8Vvz+PQ1CgreLIMXV3bzUir9HmF6g==,iv:QoWO+Iqio5JZpVm6NfI6tMp4C+1ekj04/4HJbjSXfLA=,tag:+d5cWEmJpk8PyyZ5F//XuQ==,type:str]
|
zitadel_db_password: ENC[AES256_GCM,data:SoWp+EaEr8Vvz+PQ1CgreLIMXV3bzUir9HmF6g==,iv:QoWO+Iqio5JZpVm6NfI6tMp4C+1ekj04/4HJbjSXfLA=,tag:+d5cWEmJpk8PyyZ5F//XuQ==,type:str]
|
||||||
zitadel_admin_password: ENC[AES256_GCM,data:WzJ9gNMC9Co7qj9sdeu+ftr5AqjWjwUaYBWNJVqDEg==,iv:LLNkLn7o7DMOKI2ATePGUAlti3gpQ9hOoVlVVpHF6Jc=,tag:PkbXTbCNRzpptgnPtyVbFw==,type:str]
|
zitadel_admin_password: ENC[AES256_GCM,data:WzJ9gNMC9Co7qj9sdeu+ftr5AqjWjwUaYBWNJVqDEg==,iv:LLNkLn7o7DMOKI2ATePGUAlti3gpQ9hOoVlVVpHF6Jc=,tag:PkbXTbCNRzpptgnPtyVbFw==,type:str]
|
||||||
@@ -28,8 +28,7 @@ postgres_crt: ENC[AES256_GCM,data:dTWwOxo2KtFhQkNZlBwn60hspmksNohV3kwMwBD6zzluzQ
|
|||||||
postgres_key: ENC[AES256_GCM,data:qvahOvxQLzWe5IpG4+qvJQE8MgfM2vx9MKfW1AuXm1euXS5dPvb4q1bLGsOJx27Z1vFAgmu/W1mgCLUx83awHEoYZwKIYabUHy8jcZdg9yZQQNkUrWi+CiVZzEw944ZyVgkC/AACi8Hu3kLWxAj4qcVWlkquOACUaEdIrFbp10AxFSU85YW/tJT7yIxrrdFJu5GGSCJvbR3HKT9vFaCxbzFK8ZavOfGLfoJ3WjAtI80z2Y2cw+hEsDYz5rcoC2fjPbSMD7Gwnnx+bBJ1M7ZqQaP5JctyPPbbsLxRqP32FveDKrJRYT1FmYX6Vz9KAu3FiphFKN1V+IBW9CMyBWWr2sYcpRE74WbOCrJxc9LCo37Py91LjGlpjHfu+ngHj+hsfm60KQRmhIPW1pfw86Q572C8OuhoZPcpHGzdB5NDErgchyhQJVbG4ufU3+1KuM2ioTzUv1CqfJHfEcpngR7ziXO0Xu5UyAJwMiFtSScZm+/KaO6noA90nr/AZBQEjOksj2XLPny1yF2iliyI2vmiADFbjG3Ffb0PZZBQtE/2EiB5Ejbphay1KfKY8nF9ScI0g0VlB/RASWyQOoSOjVeKbyPHpLPefOUd3iqPUmnUYZUA33v5iPhiB3dGydE/ETIsZpXGNYlIiy61bXbp4IMvxa7Mif5e++5xcOQADEVZSLWxMy8Y0E164Z+xVKfTa1OzCdsXkrJYqEt012Y8FL10SiSCd20R3vW3uAs7zVtZyzFm2IQmEJHwbm/R9Cnh6lksq9lSks9m33qHLjF0/HhTjpwbGRTUA51TA+fi6frtVVLWe/JMaflql2p9gdO5HMvfIZCFtcXpQprdtECF7tkiLh9wNcg3k7XqWVBGReLDfoiNiYDbBJjUJ1Gr/FTnObvzodqSwXqWeBZO9qE2WL5OKmU+G7uDmkLxEkgxiwZ3Ez0SKu5DifX53wUaNpXtyJOmyKVRsYllSCPiFu1wkYwX/+W29RkMGPJCFnJjMnRFKZHERS7Lo0QUeWoMB+dPDixpU061Xcx2K73AfprvtcXLwwnL5O8KihTmzLQxz0iHd3q+ir3bzMeOsX5jmopwrlym69XaV0x+fnr+vZ93Fu6VNHxdJU53gekgFdkZHBeyideERqRqEGioO4VS2Vmx4jPpDOIV+Oa9P3NPxQVNgWqdPdcUMaJQ1x5sO7xguKdZLww4s0Y4BGhd0aCs8RtKocJjIjoUxpeKMR2CS0XnzftSa49pGDtywUND9oklX2eX1X8njexH5cXKaAV0qVNvskegNNu3eT6xGdloBKei/JgwfzCh/rO3VNEEhFGEadh/s3vM2yj3+xR7MNiZW5jox14XYJYzO2AcIbYmRhxAyASisuKzNxjLqh0Jl9w02dJcDNlQEa2pKL2wifvfvWQlgHuUmJkfsURgGjnD/kI+GRvgQ7MRNbes2LeLM2k6avnFRJIYoqXCQ0DdG/yd7JvmsS/gfQadP90+2w==,iv:GutSu4hsM/5vpj8kBIr3etyJydKblaopparN4/6Tf94=,tag:Yt1k6rKyeSP4hZKuZH5qyw==,type:str]
|
postgres_key: ENC[AES256_GCM,data:qvahOvxQLzWe5IpG4+qvJQE8MgfM2vx9MKfW1AuXm1euXS5dPvb4q1bLGsOJx27Z1vFAgmu/W1mgCLUx83awHEoYZwKIYabUHy8jcZdg9yZQQNkUrWi+CiVZzEw944ZyVgkC/AACi8Hu3kLWxAj4qcVWlkquOACUaEdIrFbp10AxFSU85YW/tJT7yIxrrdFJu5GGSCJvbR3HKT9vFaCxbzFK8ZavOfGLfoJ3WjAtI80z2Y2cw+hEsDYz5rcoC2fjPbSMD7Gwnnx+bBJ1M7ZqQaP5JctyPPbbsLxRqP32FveDKrJRYT1FmYX6Vz9KAu3FiphFKN1V+IBW9CMyBWWr2sYcpRE74WbOCrJxc9LCo37Py91LjGlpjHfu+ngHj+hsfm60KQRmhIPW1pfw86Q572C8OuhoZPcpHGzdB5NDErgchyhQJVbG4ufU3+1KuM2ioTzUv1CqfJHfEcpngR7ziXO0Xu5UyAJwMiFtSScZm+/KaO6noA90nr/AZBQEjOksj2XLPny1yF2iliyI2vmiADFbjG3Ffb0PZZBQtE/2EiB5Ejbphay1KfKY8nF9ScI0g0VlB/RASWyQOoSOjVeKbyPHpLPefOUd3iqPUmnUYZUA33v5iPhiB3dGydE/ETIsZpXGNYlIiy61bXbp4IMvxa7Mif5e++5xcOQADEVZSLWxMy8Y0E164Z+xVKfTa1OzCdsXkrJYqEt012Y8FL10SiSCd20R3vW3uAs7zVtZyzFm2IQmEJHwbm/R9Cnh6lksq9lSks9m33qHLjF0/HhTjpwbGRTUA51TA+fi6frtVVLWe/JMaflql2p9gdO5HMvfIZCFtcXpQprdtECF7tkiLh9wNcg3k7XqWVBGReLDfoiNiYDbBJjUJ1Gr/FTnObvzodqSwXqWeBZO9qE2WL5OKmU+G7uDmkLxEkgxiwZ3Ez0SKu5DifX53wUaNpXtyJOmyKVRsYllSCPiFu1wkYwX/+W29RkMGPJCFnJjMnRFKZHERS7Lo0QUeWoMB+dPDixpU061Xcx2K73AfprvtcXLwwnL5O8KihTmzLQxz0iHd3q+ir3bzMeOsX5jmopwrlym69XaV0x+fnr+vZ93Fu6VNHxdJU53gekgFdkZHBeyideERqRqEGioO4VS2Vmx4jPpDOIV+Oa9P3NPxQVNgWqdPdcUMaJQ1x5sO7xguKdZLww4s0Y4BGhd0aCs8RtKocJjIjoUxpeKMR2CS0XnzftSa49pGDtywUND9oklX2eX1X8njexH5cXKaAV0qVNvskegNNu3eT6xGdloBKei/JgwfzCh/rO3VNEEhFGEadh/s3vM2yj3+xR7MNiZW5jox14XYJYzO2AcIbYmRhxAyASisuKzNxjLqh0Jl9w02dJcDNlQEa2pKL2wifvfvWQlgHuUmJkfsURgGjnD/kI+GRvgQ7MRNbes2LeLM2k6avnFRJIYoqXCQ0DdG/yd7JvmsS/gfQadP90+2w==,iv:GutSu4hsM/5vpj8kBIr3etyJydKblaopparN4/6Tf94=,tag:Yt1k6rKyeSP4hZKuZH5qyw==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- recipient: age189v3q8quejkaz4r8u0g2aechsfk4sk3465khx35erd320rea5scq98v7er
|
- enc: |
|
||||||
enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2dFgyd1VSWUxLZTZNbkZa
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2dFgyd1VSWUxLZTZNbkZa
|
||||||
c0x5Uk9EOGtBbTJXQzVwQWpGMFAyRkRmQWlrClVBSzFxR3VtVEdFdVpOZ21xcXl4
|
c0x5Uk9EOGtBbTJXQzVwQWpGMFAyRkRmQWlrClVBSzFxR3VtVEdFdVpOZ21xcXl4
|
||||||
@@ -37,8 +36,8 @@ sops:
|
|||||||
R1FONm52SFYvZ2hnUm9MOWhBbGU4cFUKAzvxN3xv9DeRmlWxVKjLQjvifRLkuzOL
|
R1FONm52SFYvZ2hnUm9MOWhBbGU4cFUKAzvxN3xv9DeRmlWxVKjLQjvifRLkuzOL
|
||||||
kg1mjiNqbLDTge6wFyjmpOpOpuqDzlv7lLJH0Af3yLt1ghPHeiehcg==
|
kg1mjiNqbLDTge6wFyjmpOpOpuqDzlv7lLJH0Af3yLt1ghPHeiehcg==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
recipient: age189v3q8quejkaz4r8u0g2aechsfk4sk3465khx35erd320rea5scq98v7er
|
||||||
enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXWmlpZFBqM1lteWhacHcx
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXWmlpZFBqM1lteWhacHcx
|
||||||
ZDdZZUpQZEdCaVI5b3kyZkt5VEMveVNYbzFJCnEvYVlFRExzR0M5WjRZUjNvbGcw
|
ZDdZZUpQZEdCaVI5b3kyZkt5VEMveVNYbzFJCnEvYVlFRExzR0M5WjRZUjNvbGcw
|
||||||
@@ -46,8 +45,8 @@ sops:
|
|||||||
VWFSakJvZTJFOXBkaEVOaDYrSlo2aFEKyN2A4vPJ8szeIhwVC/VExXj7kDggJE02
|
VWFSakJvZTJFOXBkaEVOaDYrSlo2aFEKyN2A4vPJ8szeIhwVC/VExXj7kDggJE02
|
||||||
FtzFIiwHVLzWkIlUc6SFFVX43Q1Z3aYmQtnDuXDBZVOSQfMtg96eew==
|
FtzFIiwHVLzWkIlUc6SFFVX43Q1Z3aYmQtnDuXDBZVOSQfMtg96eew==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1fwgg9h68zz8s6xa8qt4yn7gqcqp59s0mma8xdfq9hunksqv8fq4qd9yfaz
|
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
||||||
enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0N1FWaVV4MGNnWmExaW8r
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0N1FWaVV4MGNnWmExaW8r
|
||||||
UjlnMlZEQnlrSDA0RERONXBYdTM3bDFLZ0VNCmEvVFhXT2lhMHV0QlRWbXNFYWRE
|
UjlnMlZEQnlrSDA0RERONXBYdTM3bDFLZ0VNCmEvVFhXT2lhMHV0QlRWbXNFYWRE
|
||||||
@@ -55,8 +54,8 @@ sops:
|
|||||||
U0NCT0ptR3dlNjdRVlMwZTR4UDRGWEkKiLgiuZWVKYGEtoh4o0iEwdzzngUAkFn1
|
U0NCT0ptR3dlNjdRVlMwZTR4UDRGWEkKiLgiuZWVKYGEtoh4o0iEwdzzngUAkFn1
|
||||||
YRpNjwSqiCIsoerTyN7oghbRnNKY8kSl4TyNGiaxwUprLdzEqgwLbw==
|
YRpNjwSqiCIsoerTyN7oghbRnNKY8kSl4TyNGiaxwUprLdzEqgwLbw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
- recipient: age1rhnaejc5nwcqcuq5qt45wzr2fncurlwqw0adhnavfraqsdzffatqh5yhl4
|
recipient: age1fwgg9h68zz8s6xa8qt4yn7gqcqp59s0mma8xdfq9hunksqv8fq4qd9yfaz
|
||||||
enc: |
|
- enc: |
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXcXFPV0g4c2ZSOWdhZGhl
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXcXFPV0g4c2ZSOWdhZGhl
|
||||||
SUh1K2NMSWEyTWU3SzJIUGd1L3g0MEdlVWs4CmhXdTkxT3lQYWdDa2ljYkg5cXUz
|
SUh1K2NMSWEyTWU3SzJIUGd1L3g0MEdlVWs4CmhXdTkxT3lQYWdDa2ljYkg5cXUz
|
||||||
@@ -64,7 +63,8 @@ sops:
|
|||||||
anFCMmpFNzBob21LMUprdzkwV1IxUHMKQ+BcKYxGY3Lph/byMfUjf3pDgue2Q7up
|
anFCMmpFNzBob21LMUprdzkwV1IxUHMKQ+BcKYxGY3Lph/byMfUjf3pDgue2Q7up
|
||||||
PbCNf5QMRd82DCYnCIMk5WD/A5kQArnd+jDjaHuaQp14SRdZ4ph+8Q==
|
PbCNf5QMRd82DCYnCIMk5WD/A5kQArnd+jDjaHuaQp14SRdZ4ph+8Q==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2026-07-28T18:10:33Z"
|
recipient: age1rhnaejc5nwcqcuq5qt45wzr2fncurlwqw0adhnavfraqsdzffatqh5yhl4
|
||||||
mac: ENC[AES256_GCM,data:mb/27a5tLqslt3EGsNa6XzmszVWCJmo48y5jLeZsiumVXA3iN/h85aMUHE4g8cPddaG23HWkmroEfFlOgEGr90rfTJL2tw0ASUU4x4GCdHqg9m3mjsnHQU0m8O+YtZLBqwOKTXfznxSYPY/WibFLpyg4frDHlNkkodNoJGloUQs=,iv:euuX0ALHrfBkZmpjspHuyzAbwmtB8RRtqHo2BX6TA9E=,tag:ExRBXAHTHftiwstEuCnKHQ==,type:str]
|
lastmodified: "2026-09-28T04:48:43Z"
|
||||||
|
mac: ENC[AES256_GCM,data:SlyWkzQqqgsDT1uLC5LwWhPhGyexjK1+7Qq02OECNXmr9dhoGzNf76+G1nCoLP5czd4MpaCnk7//aN6Qs6M9DhBV8GG4wDN9pFGWq7JgE7GvPTzJ3J3Ady+yaat3k1HdAE775lIHERa79lxMurzJn66ctgSmsYuIf52qf1WI3GU=,iv:kbawvEBfwyJHYJ0+Q1e0fGTmudPj+mwptt/NxH1u87o=,tag:UYBB28ndbNFSdV0Pf0ncAw==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.3
|
version: 3.13.3
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
hermes_api_server_env: ENC[AES256_GCM,data:HXrlAYPQ1BGc5G2JZrJ59cpnKHcDDi/lFCANSOxWOl1acXFfjemUNwHu5rf0iacb4KNbvhTPGd7U0tQ7tHdO/Sl1HZIacerovZsNA8yplQ==,iv:g5Uj1YLVwOJDYCkZ9EZi2EvMONq+MEY3ZKIoTCh1rWE=,tag:uH/S8NMAx2POMfbMkOWHRQ==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6cmc4MzlkRUMzdTE1QktD
|
||||||
|
aU5jQnQ2UlhrMXlrSmFMQUU5Z1duWDRTdlJJCmVZZVZlWElhOCtPbUJwR3BrbTdm
|
||||||
|
YzNJZFZlbGJKV3RsWFJNTVRJNkRNajQKLS0tIHNXREhldlg4WStGSFBwMGd2YUZu
|
||||||
|
N1AxQkdUWS9sS1U2UUdPSWs2dUVqZGMK/Y4SNZp0OSFexe/O0rrVT6VOKRicMK2L
|
||||||
|
1arpCWGU9tSk2DH9DlJLmvjmx//buwzHqb+cxn2sxPBpbhdFJGndJg==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
||||||
|
lastmodified: "2026-09-27T21:17:19Z"
|
||||||
|
mac: ENC[AES256_GCM,data:/tW5B1QqJQFkj1radKTyNMcnVs/W4F+kfZAnUKJol11xQVNZnohLFYfpNzjmi2HhARiNkvjdiJtb+Nh/XTuiGUW3XVE2pcgseFxIgbpdnxpoMKbfPPyZ8A0DpSAZ4v6XRrWzqCM7D/JO9FC3CAuVgfhIwLJlfLb82MJRAlI67sg=,iv:M7n6yY80oc3jWbEM3L9TUr7wL0sF4fjCsVIepJslgTI=,tag:75mt57Qz5IFk781F3mGwIw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.3
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
openrouter_api_key: ENC[AES256_GCM,data:+vLLYfzwtIboTanbbDAXYoVRkutdrogZmSHozEoptw7+QKMw7j9llIqWwrc=,iv:+sjK5IJCpTizzzHpGk9F3SExC9adA3d/D8ZLZMb2MVU=,tag:hlX6IZ1DpvuqHhngaZ156g==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZTG0vanhiOW5XSk1neTNQ
|
||||||
|
VWpTZXN4UWVjcDU3bDB4M3dtNzVjOXlqRVZFCkNiQWRCWFdxQy9EaXV3QjZ3QzZ5
|
||||||
|
WHoreTNlQnVRbElOSDhrRC9QSDZWdWMKLS0tIHB4bllPNnBMVmxHM2dWS0o1QVI1
|
||||||
|
V0s5Y29CUzNkcGQzWHk5MXE4aDFPVVUKqPGqKAX1J6J5hgZaVqj6gtMi0J6wImg2
|
||||||
|
qvPwkHzyEeZrOLwghp9P+nuaphE9kcvNHZxUImeLCRW8+CvIDbT6mA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
||||||
|
lastmodified: "2026-09-29T01:43:17Z"
|
||||||
|
mac: ENC[AES256_GCM,data:AlUGyhoIgbzAHlKIJ91W8KMjegDX53KfrRdfCME3taUjBAcFi7rBCJijbYPtJ7X1zhzj4PnlwB2/jVSTMz6nMSAvJPZvKrFvB8tlaId74+rD3cClCJO5W2rpKBmZkvrF+XD0Vt/UJFT11DQDu81Vdsw7RN6bexf+YA9WjlZg15w=,iv:BECdLTVKzLYjWtIdtmRwj67qDoUwOUwn3meZgUAJiDE=,tag:AAKqS2kteHFzc2r0EAtJ2A==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.3
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
bridge_password: ENC[AES256_GCM,data:wQcsTjgeco5adaFu4onNcWkXIEO71MtSRRZsY1sOv4c0hWL31KHLT+hXqNMl,iv:0I4DJ8Ce8NVxBqyOUQuWXoXtNTAvuRTWWXcmyBEH59I=,tag:fyH/4siYdzG/mZcVluUZXw==,type:str]
|
|
||||||
sops:
|
|
||||||
age:
|
|
||||||
- enc: |
|
|
||||||
-----BEGIN AGE ENCRYPTED FILE-----
|
|
||||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOeWxBWTlMS2Ywb1pNT0Zu
|
|
||||||
Zy8vdW5TdUQzUWV3MUJTSW5tTERVLzJlV1VnClUvYTVkRXNSMXZaeTZ4amYvdFRh
|
|
||||||
RVg5UE1RTDFlaTJCYWNkNTlFOG1DNjQKLS0tIGFid1Y3QWU5T1BDQzFyUWMraUpD
|
|
||||||
R1dzTUxjQnRUbzVuZnJ6K01sMVRNdmcKGcjvpi7LYGmIhP1OEjrMqI5ho09Wgtrt
|
|
||||||
z38WGqa5DA3oWTqFURXnSpX4241u3dcFtZSwQ1+HGUm9mUx6ZP5ZSQ==
|
|
||||||
-----END AGE ENCRYPTED FILE-----
|
|
||||||
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
|
||||||
lastmodified: "2026-09-24T23:58:23Z"
|
|
||||||
mac: ENC[AES256_GCM,data:22GAQLwjmwEDlydzdSnJl6BcCjereUyKixbZ9mNTtAEj6TuhUE3xtPmx3MFykEEbPo2Im3JrKzoOiFq724rkdrJ7KxowPgQ1oG+aGwDbonyALnzKDQIj8yGPgUhdBpUVA211DXKL6xoA9/xr6PY08PqKq0iuAUqigpF32kZLEAU=,iv:AMz/92xIQuNgKsp0AKp766JRiWRns5wO+QYoXOnZcfs=,tag:3S1ZPtJDXeG1vBrbolkCsA==,type:str]
|
|
||||||
unencrypted_suffix: _unencrypted
|
|
||||||
version: 3.13.3
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
reactive_resume_db_password: ENC[AES256_GCM,data:M4zhT3WA+gq3MVS4423genlHk91kEQ582nYMrUyVuPz29U6VK3V0JkfcH6rzYnN6,iv:0ddtIl7k5yMv9O8N8HoxxVeF0iWJ/hyMsVjGXbAPiCs=,tag:Dgr7FJXMFlRQxU5B1E6apw==,type:str]
|
||||||
|
reactive_resume_auth_secret: ENC[AES256_GCM,data:TYEtZu5L5iO17OEelYepiI47WOMSSU+6qFrBbZ+oQM1Uxf/dP68xFma4yWHKrS7jz2nmvDBhWZWUAiw0V+8fTA==,iv:uLkcABpCHwZ+pYX1fjtxCXwZ6WZzad1EXHz2RlFsrLQ=,tag:gDJszmiFWMjZGIVUFBt4vg==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0QmhFZFZkWkFTSllMOFli
|
||||||
|
L0hHeXNEOGc0ajFWOXRmdVJheHRxcmErR0JVCmwrVDZWQ3pQZkZtN1BKOXNoMnRS
|
||||||
|
bm5OcjlRQ1FTbmV5R3diNW5LeEVJSVEKLS0tIDV6V3o5UWhCNXczZWxESkpOWTFF
|
||||||
|
Rzl5Yk10UmhXR1dBWlRUUnAxaUYvZXMK+ikexhXADk9X9MLm2ZDxsMwSZmvgQ/zG
|
||||||
|
z986Sl4bu3rFIXuUqeueOS0GDznjALIvi2e/OV4CDffKUQEr7DxeFw==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1usm3v0ugducrzcwud60dda7xfnj49fdhecj7kcsfm7dzwzrzeyqq2jc2hm
|
||||||
|
lastmodified: "2026-09-28T05:26:05Z"
|
||||||
|
mac: ENC[AES256_GCM,data:TdwDkVJ/8CbK2RJ28EBTYB9TwY9pmcCMi74zuQcPjehwbrrTkh9ClSXRopnRSAJjU3Nv/GAyYAeKF5HtxxVFbo0OwmajQQDmAkVOlDKg+LqkEUbBQPwAQ8a3SUFNj6CXf20qBX83aOI9SCiUEWKHvtQ6N+esUQfmRWb7IzDM7S8=,iv:A87PvE9LTuYxE0YbOZbigwdGVxU4iPDrUkx51w9VonQ=,tag:K6D2sKigNbAIUFFFjXo5nw==,type:str]
|
||||||
|
unencrypted_suffix: _unencrypted
|
||||||
|
version: 3.13.3
|
||||||
@@ -44,10 +44,6 @@
|
|||||||
./eternal-terminal
|
./eternal-terminal
|
||||||
./nextcloud-mcp
|
./nextcloud-mcp
|
||||||
./amazon-mcp
|
./amazon-mcp
|
||||||
# ./protonmail-mcp : tower uniquement (ses options viennent du input
|
|
||||||
# protonmail-mcp, importé par flake.nix pour tower seul). L'import ici
|
|
||||||
# cassait l'éval de laptop/laptop-iso (option `services.protonmail-mcp`
|
|
||||||
# inexistante chez eux). Le fichier reste importé pour tower via flake.nix.
|
|
||||||
./firefox-mcp
|
./firefox-mcp
|
||||||
# ./stalwart-mcp # FIXME: cargoHash mismatch, needs fix
|
# ./stalwart-mcp # FIXME: cargoHash mismatch, needs fix
|
||||||
# ./gotify
|
# ./gotify
|
||||||
|
|||||||
@@ -132,7 +132,21 @@
|
|||||||
Environment = ["NC_SETUP_FORCE_RUN=20260924"];
|
Environment = ["NC_SETUP_FORCE_RUN=20260924"];
|
||||||
# app:enable ne lance PAS les migrations : sans upgrade ensuite, les
|
# app:enable ne lance PAS les migrations : sans upgrade ensuite, les
|
||||||
# tables des apps (oc_calendars...) ne sont jamais créées.
|
# tables des apps (oc_calendars...) ne sont jamais créées.
|
||||||
ExecStartPost = "${config.services.nextcloud.occ}/bin/nextcloud-occ upgrade";
|
ExecStartPost = [
|
||||||
|
"${config.services.nextcloud.occ}/bin/nextcloud-occ upgrade"
|
||||||
|
# adminpassFile ne fixe le mot de passe QUE lors de l'install initiale
|
||||||
|
# (occ maintenance:install) : sur une instance déjà installée il est
|
||||||
|
# ignoré silencieusement. Ce ExecStartPost force le mot de passe du
|
||||||
|
# compte "root" à la valeur sops à CHAQUE activation (self-healing,
|
||||||
|
# tourne à chaque switch). Ajouté le 28/09/2026 suite à un fichier
|
||||||
|
# services/nextcloud/password.txt committé en clair (repo public) :
|
||||||
|
# rotation forcée pour invalider toute ancienne valeur exposée.
|
||||||
|
"${pkgs.writeShellScript "nextcloud-rotate-admin-password" ''
|
||||||
|
set -euo pipefail
|
||||||
|
export OC_PASS="$(cat ${config.sops.secrets."nextcloud/admin_password".path})"
|
||||||
|
${config.services.nextcloud.occ}/bin/nextcloud-occ user:resetpassword --password-from-env root
|
||||||
|
''}"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
nextcloudpassword
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# services/openrouter-mcp/default.nix
|
||||||
|
# Serveur MCP OpenRouter (codeChap/mcp-server-open-router) pour hermes.
|
||||||
|
#
|
||||||
|
# 5 outils : chat, chat_with_vision, chat_with_search (recherche web via le
|
||||||
|
# plugin OpenRouter), list_models, credits. Binaire Rust unique, stdio.
|
||||||
|
# Choisi plutôt que le serveur officiel distant (mcp.openrouter.ai, OAuth) :
|
||||||
|
# ce dernier délivre une clé à durée de vie 7 jours (spend cap $10), donc
|
||||||
|
# inutilisable pour une intégration nix déclarative "pose et oublie".
|
||||||
|
#
|
||||||
|
# Le binaire lit sa config depuis $XDG_CONFIG_HOME/mcp-server-open-router/config.toml
|
||||||
|
# (crate `dirs`, pas de variable d'env ni de flag pour le chemin). Le wrapper
|
||||||
|
# régénère ce fichier à chaque lancement depuis le secret sops : la clé n'est
|
||||||
|
# JAMAIS en clair dans le nix store ni dans mcpServers.env.
|
||||||
|
#
|
||||||
|
# Secret : secrets/openrouter-mcp.yml (sops, clé openrouter_api_key).
|
||||||
|
# PLACEHOLDER à remplacer par marc :
|
||||||
|
# nix run nixpkgs#sops -- secrets/openrouter-mcp.yml
|
||||||
|
# (clé OpenRouter : https://openrouter.ai/settings/keys)
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
version = "unstable-2026-09-28";
|
||||||
|
src = pkgs.fetchFromGitHub {
|
||||||
|
owner = "codeChap";
|
||||||
|
repo = "mcp-server-open-router";
|
||||||
|
rev = "f6952c1053121b3a02c87c5f5c7c21f7517445d0";
|
||||||
|
hash = "sha256-rlck7hpyRxwAB+/kR+2eeuOCbmceTMzTvuCqS0kTEV0=";
|
||||||
|
};
|
||||||
|
|
||||||
|
openrouter-mcp = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "mcp-server-open-router";
|
||||||
|
inherit version src;
|
||||||
|
cargoLock.lockFile = "${src}/Cargo.lock";
|
||||||
|
# reqwest -> openssl-sys a besoin de pkg-config + libs de dev OpenSSL.
|
||||||
|
nativeBuildInputs = [pkgs.pkg-config];
|
||||||
|
buildInputs = [pkgs.openssl];
|
||||||
|
meta = {
|
||||||
|
description = "MCP server for OpenRouter (chat, vision, web search, model list, credits)";
|
||||||
|
homepage = "https://github.com/codeChap/mcp-server-open-router";
|
||||||
|
license = lib.licenses.mit;
|
||||||
|
mainProgram = "open-router";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
stateDir = "/var/lib/hermes/openrouter-mcp";
|
||||||
|
configDir = "${stateDir}/config";
|
||||||
|
apiKeyFile = config.sops.secrets."openrouter-mcp/api_key".path;
|
||||||
|
|
||||||
|
# Régénère config.toml à chaque lancement depuis le secret sops (jq -Rs
|
||||||
|
# produit une chaîne TOML/JSON correctement échappée, jamais d'interpolation
|
||||||
|
# brute d'une valeur pouvant contenir des guillemets).
|
||||||
|
wrapper = pkgs.writeShellScript "openrouter-mcp-hermes" ''
|
||||||
|
set -euo pipefail
|
||||||
|
umask 077
|
||||||
|
mkdir -p ${configDir}/mcp-server-open-router
|
||||||
|
key_json=$(printf '%s' "$(cat ${apiKeyFile})" | ${lib.getExe pkgs.jq} -Rs .)
|
||||||
|
printf 'api_key = %s\n' "$key_json" > ${configDir}/mcp-server-open-router/config.toml
|
||||||
|
export XDG_CONFIG_HOME=${configDir}
|
||||||
|
exec ${lib.getExe openrouter-mcp}
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
sops.secrets."openrouter-mcp/api_key" = {
|
||||||
|
key = "openrouter_api_key";
|
||||||
|
sopsFile = ../../secrets/openrouter-mcp.yml;
|
||||||
|
owner = "hermes";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d ${stateDir} 0700 hermes hermes -"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.hermes-agent.mcpServers.openrouter = {
|
||||||
|
command = "${wrapper}";
|
||||||
|
timeout = 60;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# services/protonmail-mcp/default.nix
|
|
||||||
# Proton Mail Bridge headless (service système) + serveur MCP
|
|
||||||
# proton-mail-bridge-client, câblés via le flake local ../../protonmail-mcp
|
|
||||||
# (nixosModules.default = inputs.protonmail-mcp.nixosModules.default, importé
|
|
||||||
# directement dans flake.nix pour tower).
|
|
||||||
#
|
|
||||||
# Secret sops : secrets/protonmail.yml, fichier SÉPARÉ de tower.yml — hermes
|
|
||||||
# n'a ni le binaire sops ni la clé age privée de tower (lecture seule pour
|
|
||||||
# root), donc pas moyen de rééditer tower.yml en place. Un fichier neuf ne
|
|
||||||
# demande que les clés PUBLIQUES déjà présentes dans .sops.yaml -> chiffrable
|
|
||||||
# par hermes via `nix run nixpkgs#sops -- --encrypt --in-place`.
|
|
||||||
# Contient pour l'instant un mot de passe PLACEHOLDER (Bridge non logué) :
|
|
||||||
# le service démarre mais le MCP n'authentifie pas encore.
|
|
||||||
#
|
|
||||||
# PREMIER LOGIN (une fois, humain — nécessite le vrai mot de passe Proton de
|
|
||||||
# marc.partensky@proton.me, potentiellement 2FA) :
|
|
||||||
# sudo protonmail-bridge-login
|
|
||||||
# Dans le CLI Bridge : login, puis info (copier le mot de passe Bridge généré),
|
|
||||||
# puis exit.
|
|
||||||
# Ensuite : remplacer le placeholder dans secrets/protonmail.yml par ce mot de
|
|
||||||
# passe (re-chiffrement complet du fichier, toujours sans clé privée), puis
|
|
||||||
# décommenter la ligne services.hermes-agent.mcpServers.protonmail ci-dessous
|
|
||||||
# et relancer `just nixos`.
|
|
||||||
{config, ...}: {
|
|
||||||
sops.secrets."protonmail/bridge-password" = {
|
|
||||||
sopsFile = ../../secrets/protonmail.yml;
|
|
||||||
key = "bridge_password";
|
|
||||||
owner = "hermes";
|
|
||||||
};
|
|
||||||
|
|
||||||
services.protonmail-mcp = {
|
|
||||||
# DÉSACTIVÉ : Proton Mail Bridge exige un plan payant. Le login Bridge a été
|
|
||||||
# tenté le 24/09/2026 et refusé par l'API Proton :
|
|
||||||
# 422 POST https://mail-api.proton.me/auth/v4: Please upgrade to a paid plan
|
|
||||||
# to use this client (Code=10004)
|
|
||||||
# L'auto-forwarding est payant lui aussi (même via filtre custom), donc aucune
|
|
||||||
# voie gratuite : la lecture des mails passe par le webmail piloté au
|
|
||||||
# navigateur (services/firefox-mcp + firefox-mcp-login).
|
|
||||||
# Tout est conservé (flake ../../protonmail-mcp, secret, user système) : il
|
|
||||||
# suffit de repasser enable = true si marc prend Mail Plus un jour.
|
|
||||||
enable = false;
|
|
||||||
username = "marc.partensky@proton.me";
|
|
||||||
passwordFile = config.sops.secrets."protonmail/bridge-password".path;
|
|
||||||
};
|
|
||||||
|
|
||||||
# À décommenter seulement après le premier login Bridge (mot de passe réel
|
|
||||||
# dans secrets/protonmail.yml, pas le placeholder) :
|
|
||||||
# services.hermes-agent.mcpServers.protonmail = config.services.protonmail-mcp.mcpServer;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# services/reactive-resume/default.nix
|
||||||
|
# Reactive Resume (github.com/AmruthPillai/Reactive-Resume) sur cv.marcpartensky.com.
|
||||||
|
#
|
||||||
|
# Inspiré du docker-compose perso de marc (github.com/marcpartensky/docker,
|
||||||
|
# services/resume/compose.yml), mais simplifié pour la version actuelle de
|
||||||
|
# l'image (>= v5.1) : depuis v5.1 le PDF est généré côté navigateur
|
||||||
|
# (@react-pdf/renderer), donc plus de conteneur "printer" (browserless/chrome)
|
||||||
|
# ni de PRINTER_*/BROWSERLESS_*. Le stockage S3 (Minio dans l'ancien setup) est
|
||||||
|
# optionnel depuis la même version : si les S3_* sont vides l'app stocke les
|
||||||
|
# uploads en local (/app/data) — on garde ça pour éviter de déployer Minio.
|
||||||
|
#
|
||||||
|
# Pas de paquet nixpkgs pour cette app (Node/NestJS) : conteneur OCI via podman
|
||||||
|
# (virtualisation.oci-containers, cf. services/chhoto pour le même pattern).
|
||||||
|
# --network=host (au lieu d'un port mappé) car l'app doit parler à Postgres en
|
||||||
|
# 127.0.0.1:5432 comme un service natif — même convention loopback que le
|
||||||
|
# reste de tower, le firewall reste fermé sur ce port (assertion en bas).
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
sops.secrets."reactive-resume/db_password" = {
|
||||||
|
key = "reactive_resume_db_password";
|
||||||
|
sopsFile = ../../secrets/reactive-resume.yml;
|
||||||
|
# lu par reactive-resume-db-password.service (User=postgres) ; l'app elle
|
||||||
|
# même lit le mot de passe via le template sops ci-dessous, pas ce fichier.
|
||||||
|
owner = "postgres";
|
||||||
|
};
|
||||||
|
|
||||||
|
sops.secrets."reactive-resume/auth_secret" = {
|
||||||
|
key = "reactive_resume_auth_secret";
|
||||||
|
sopsFile = ../../secrets/reactive-resume.yml;
|
||||||
|
};
|
||||||
|
|
||||||
|
sops.templates."reactive-resume.env" = {
|
||||||
|
content = ''
|
||||||
|
TZ=America/New_York
|
||||||
|
APP_URL=https://cv.marcpartensky.com
|
||||||
|
PORT=8098
|
||||||
|
DATABASE_URL=postgresql://reactive-resume:${config.sops.placeholder."reactive-resume/db_password"}@127.0.0.1:5432/reactive-resume
|
||||||
|
AUTH_SECRET=${config.sops.placeholder."reactive-resume/auth_secret"}
|
||||||
|
# Inscription ouverte pour l'instant : le point d'entrée est de toute
|
||||||
|
# façon derrière le SSO Pangolin (auth.sso-enabled ci-dessous). Repasser
|
||||||
|
# à true une fois le compte de marc créé si on veut fermer les inscriptions.
|
||||||
|
FLAG_DISABLE_SIGNUPS=false
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
services.postgresql = {
|
||||||
|
ensureDatabases = ["reactive-resume"];
|
||||||
|
ensureUsers = [
|
||||||
|
{
|
||||||
|
name = "reactive-resume";
|
||||||
|
ensureDBOwnership = true;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
# types.lines : se concatène avec les règles des autres modules (postgres,
|
||||||
|
# vaultwarden, gitea...) au lieu de les remplacer. Même mkOverride 10 que
|
||||||
|
# vaultwarden, pattern vérifié fonctionnel (cf skill marc-nixos-config).
|
||||||
|
authentication = lib.mkOverride 10 ''
|
||||||
|
host reactive-resume reactive-resume 127.0.0.1/32 md5
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Aligne le mot de passe du rôle postgres sur le secret sops utilisé par
|
||||||
|
# DATABASE_URL (ensureUsers ne pose pas de mot de passe). Même pattern que
|
||||||
|
# vaultwarden-db-password.
|
||||||
|
systemd.services.reactive-resume-db-password = {
|
||||||
|
description = "Aligne le mot de passe du rôle postgres reactive-resume sur le secret sops";
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
before = ["podman-reactive-resume.service"];
|
||||||
|
requiredBy = ["podman-reactive-resume.service"];
|
||||||
|
after = ["postgresql.service"];
|
||||||
|
requires = ["postgresql.service"];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
User = "postgres";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
${config.services.postgresql.package}/bin/psql \
|
||||||
|
-v pw="$(cat ${config.sops.secrets."reactive-resume/db_password".path})" <<'SQL'
|
||||||
|
ALTER ROLE "reactive-resume" WITH PASSWORD :'pw';
|
||||||
|
SQL
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Uploads locaux (pas de S3/Minio). uid/gid 1000 = user "node" de l'image
|
||||||
|
# officielle amruthpillai/reactive-resume.
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/reactive-resume 0750 1000 1000 -"
|
||||||
|
"d /var/lib/reactive-resume/data 0750 1000 1000 -"
|
||||||
|
];
|
||||||
|
|
||||||
|
virtualisation.podman = {
|
||||||
|
enable = true;
|
||||||
|
dockerCompat = true;
|
||||||
|
defaultNetwork.settings.dns_enabled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
virtualisation.oci-containers.backend = "podman";
|
||||||
|
|
||||||
|
virtualisation.oci-containers.containers.reactive-resume = {
|
||||||
|
image = "docker.io/amruthpillai/reactive-resume:v5.2";
|
||||||
|
autoStart = true;
|
||||||
|
volumes = ["/var/lib/reactive-resume/data:/app/data"];
|
||||||
|
environmentFiles = [config.sops.templates."reactive-resume.env".path];
|
||||||
|
extraOptions = [
|
||||||
|
"--network=host"
|
||||||
|
"--pull=newer"
|
||||||
|
"--health-cmd=node -e \"fetch('http://127.0.0.1:8098/api/health').then((r) => { if (!r.ok) process.exit(1); }).catch(() => process.exit(1));\""
|
||||||
|
"--health-interval=30s"
|
||||||
|
"--health-timeout=10s"
|
||||||
|
"--health-retries=3"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services."podman-reactive-resume" = {
|
||||||
|
after = ["network-online.target" "postgresql.service" "reactive-resume-db-password.service"];
|
||||||
|
wants = ["network-online.target"];
|
||||||
|
requires = ["reactive-resume-db-password.service"];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Ressource publique Pangolin : schéma `proxy-resources` + `protocol`
|
||||||
|
# (celui qui marche réellement sur ce contrôleur, cf gitea/jellyfin).
|
||||||
|
# SSO activé : ferme l'accès (y compris inscription) derrière Zitadel.
|
||||||
|
services.newt.blueprint.proxy-resources.reactive-resume = {
|
||||||
|
name = "reactive-resume";
|
||||||
|
protocol = "http";
|
||||||
|
full-domain = "cv.marcpartensky.com";
|
||||||
|
auth.sso-enabled = true;
|
||||||
|
targets = [
|
||||||
|
{
|
||||||
|
hostname = "127.0.0.1";
|
||||||
|
port = 8098;
|
||||||
|
method = "http";
|
||||||
|
healthcheck = {
|
||||||
|
enabled = true;
|
||||||
|
hostname = "127.0.0.1";
|
||||||
|
port = 8098;
|
||||||
|
path = "/api/health";
|
||||||
|
scheme = "http";
|
||||||
|
mode = "http";
|
||||||
|
method = "GET";
|
||||||
|
interval = 30;
|
||||||
|
unhealthy-interval = 30;
|
||||||
|
timeout = 5;
|
||||||
|
healthy-threshold = 1;
|
||||||
|
unhealthy-threshold = 3;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Garde-fou : jamais de port ouvert au LAN (--network=host expose 8098 sur
|
||||||
|
# toutes les interfaces du host, seul le firewall protège du LAN direct).
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = !(lib.elem 8098 config.networking.firewall.allowedTCPPorts);
|
||||||
|
message = "reactive-resume : le port 8098 ne doit pas être ouvert au firewall (exposition uniquement via Pangolin/newt).";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"git+https://github.com/haydonryan/vaultwarden-cli.git?rev=96cdd0dfc04d04223ec88f8ab0dd217dc0e9beb8#vaultwarden-cli@0.5.4": "0pnyw91jxlwjsx3111bhpmjf7njxzvvkxvlzf77v3kll63pcqkr0"
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# services/vaultwarden-mcp/default.nix
|
||||||
|
# Serveur MCP Vaultwarden (jr2804/vaultwarden-mcp) : binaire Rust, stdio.
|
||||||
|
# Binaire téléchargé depuis GitHub releases (plus simple que crate2nix).
|
||||||
|
#
|
||||||
|
# Authentification : VAULTWARDEN_SERVER + VAULTWARDEN_PASSWORD (master password).
|
||||||
|
# Les identifiants sont gérés via sops.
|
||||||
|
#
|
||||||
|
# Fichier créé à la demande de Marc (27/09/2026).
|
||||||
|
# IMPORTANT : il faut ajouter `vaultwarden_mcp_password` dans secrets/tower.yml
|
||||||
|
# avant activation :
|
||||||
|
# sops secrets/tower.yml # ajouter la clé vaultwarden_mcp_password
|
||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
inputs,
|
||||||
|
...
|
||||||
|
}: let
|
||||||
|
# Binaire téléchargé depuis GitHub releases
|
||||||
|
vaultwarden-mcp-bin = pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "vaultwarden-mcp";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = pkgs.fetchurl {
|
||||||
|
url = "https://github.com/jr2804/vaultwarden-mcp/releases/latest/download/vaultwarden-mcp-linux-x86_64";
|
||||||
|
sha256 = "sha256-y47/eiZ43QO4Zh6secpPemd+hr4X/xYyO8UzGLhJf8Q=";
|
||||||
|
};
|
||||||
|
dontUnpack = true;
|
||||||
|
installPhase = ''
|
||||||
|
install -D $src $out/bin/vaultwarden-mcp
|
||||||
|
chmod +x $out/bin/vaultwarden-mcp
|
||||||
|
'';
|
||||||
|
meta = {
|
||||||
|
description = "MCP server for Vaultwarden vault access";
|
||||||
|
homepage = "https://github.com/jr2804/vaultwarden-mcp";
|
||||||
|
license = lib.licenses.mit;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
# Le binaire est disponible sur le PATH du système.
|
||||||
|
environment.systemPackages = [vaultwarden-mcp-bin];
|
||||||
|
|
||||||
|
# Secret sops pour le mot de passe master Vaultwarden (défini dans secrets/tower.yml).
|
||||||
|
sops.secrets."vaultwarden/mcp_password" = {
|
||||||
|
key = "vaultwarden_mcp_password";
|
||||||
|
sopsFile = ../../secrets/tower.yml;
|
||||||
|
owner = "hermes";
|
||||||
|
group = "hermes";
|
||||||
|
mode = "0400";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Intégration dans le service hermes-agent : serveur MCP stdio.
|
||||||
|
services.hermes-agent.mcpServers.vaultwarden = {
|
||||||
|
command = "${vaultwarden-mcp-bin}/bin/vaultwarden-mcp";
|
||||||
|
args = ["serve" "--transport" "stdio"];
|
||||||
|
env = {
|
||||||
|
VAULTWARDEN_SERVER = "https://vault.marcpartensky.com";
|
||||||
|
VAULTWARDEN_PASSWORD = config.sops.placeholder."vaultwarden/mcp_password";
|
||||||
|
RUST_LOG = "info";
|
||||||
|
};
|
||||||
|
timeout = 180;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Note : le service Vaultwarden existant (services/vaultwarden/default.nix)
|
||||||
|
# tourne déjà sur tower avec le domaine https://vault.marcpartensky.com,
|
||||||
|
# le port 8222, et l'admin token dans secrets/common.yml.
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"git+https://github.com/haydonryan/vaultwarden-cli.git?rev=96cdd0dfc04d04223ec88f8ab0dd217dc0e9beb8#vaultwarden-cli@0.5.4": "0pnyw91jxlwjsx3111bhpmjf7njxzvvkxvlzf77v3kll63pcqkr0"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user