Files
nixos/services/reactive-resume/default.nix
T
Hermes Agent 9180562410 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é.
2026-09-29 19:49:09 -04:00

165 lines
6.1 KiB
Nix

# 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).";
}
];
}