From 942f9d2eebefe11c740ca6e72ecd6a0e587b42c9 Mon Sep 17 00:00:00 2001 From: fsagbuya Date: Mon, 20 Jul 2026 14:25:05 +0800 Subject: [PATCH] nixos/flarum: add initialAdminPasswordFile and databasePasswordFile options --- nixos/modules/services/web-apps/flarum.nix | 68 ++++++++++++++++++++-- nixos/tests/flarum.nix | 27 ++++++++- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/nixos/modules/services/web-apps/flarum.nix b/nixos/modules/services/web-apps/flarum.nix index c4c05826d8cd..1c7065c910bc 100644 --- a/nixos/modules/services/web-apps/flarum.nix +++ b/nixos/modules/services/web-apps/flarum.nix @@ -10,16 +10,23 @@ with lib; let cfg = config.services.flarum; + # Only placeholders reach the world-readable Nix store; the install + # script substitutes the real secrets at runtime. flarumInstallConfig = pkgs.writeText "config.json" ( builtins.toJSON { debug = false; offline = false; baseUrl = cfg.baseUrl; - databaseConfiguration = cfg.database; + databaseConfiguration = + cfg.database + // optionalAttrs (cfg.databasePasswordFile != null) { + password = "@databasePassword@"; + }; adminUser = { username = cfg.adminUser; - password = cfg.initialAdminPassword; + password = + if cfg.initialAdminPasswordFile != null then "@adminPassword@" else cfg.initialAdminPassword; email = cfg.adminEmail; }; settings = { @@ -69,7 +76,26 @@ in initialAdminPassword = mkOption { type = types.str; default = "flarum"; - description = "Initial password for the adminUser"; + description = '' + Initial password for the adminUser. + + WARNING: This is stored world-readable in the Nix store. + Use {option}`initialAdminPasswordFile` instead. + ''; + }; + + initialAdminPasswordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/flarum-admin-password"; + description = '' + File containing the initial password for adminUser. + Must be readable by the flarum user. + Takes precedence over {option}`initialAdminPassword`. + + The password must not contain `"` or `\` characters, as it is + substituted into a JSON installation config verbatim. + ''; }; user = mkOption { @@ -98,7 +124,12 @@ in bool int ]); - description = "MySQL database parameters"; + description = '' + MySQL database parameters. + + WARNING: A `password` set here is stored world-readable in the + Nix store. Use {option}`databasePasswordFile` instead. + ''; default = { # the database driver; i.e. MySQL; MariaDB... driver = "mysql"; @@ -118,6 +149,20 @@ in }; }; + databasePasswordFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/flarum-db-password"; + description = '' + File containing the database password. + Must be readable by the flarum user. + Takes precedence over `database.password`. + + The password must not contain `"` or `\` characters, as it is + substituted into a JSON installation config verbatim. + ''; + }; + createDatabaseLocally = mkOption { type = types.bool; default = false; @@ -210,6 +255,8 @@ in Type = "oneshot"; User = cfg.user; Group = cfg.group; + # The secret-filled install config is staged in /tmp + PrivateTmp = true; }; path = [ config.services.phpfpm.phpPackage ]; script = '' @@ -222,7 +269,18 @@ in '' + optionalString (cfg.createDatabaseLocally && cfg.database.driver == "mysql") '' if [ ! -f config.php ]; then - php flarum install --file=${flarumInstallConfig} + install -m 0600 ${flarumInstallConfig} /tmp/flarum-install.json + ${optionalString (cfg.initialAdminPasswordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@adminPassword@' \ + ${escapeShellArg cfg.initialAdminPasswordFile} /tmp/flarum-install.json + ''} + ${optionalString (cfg.databasePasswordFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '@databasePassword@' \ + ${escapeShellArg cfg.databasePasswordFile} /tmp/flarum-install.json + ''} + php flarum install --file=/tmp/flarum-install.json + # config.php contains the database password; stateDir is world-readable + chmod 600 config.php fi '' + '' diff --git a/nixos/tests/flarum.nix b/nixos/tests/flarum.nix index 17c7df02f44c..cbebe29c0c3c 100644 --- a/nixos/tests/flarum.nix +++ b/nixos/tests/flarum.nix @@ -10,7 +10,7 @@ }; nodes.machine = - { ... }: + { pkgs, ... }: { # Flarum installs and migrates the database on first boot and runs a # MariaDB server alongside PHP-FPM and nginx, so give the VM some headroom. @@ -28,8 +28,11 @@ adminUser = "admin"; adminEmail = "admin@example.com"; - # Flarum rejects admin passwords shorter than 8 characters. - initialAdminPassword = "flarum-admin-password"; + # The trailing newline matches how secret managers typically write files. + initialAdminPasswordFile = "${pkgs.writeText "admin-pass" "flarum-admin-password\n"}"; + # MariaDB authenticates via unix socket and never checks this password; + # setting it still exercises the substitution path. + databasePasswordFile = "${pkgs.writeText "db-pass" "flarum-db-password\n"}"; }; }; @@ -48,5 +51,23 @@ # The admin API endpoint should respond, confirming the app booted cleanly. machine.succeed("curl -sf http://localhost/api -o /dev/null") + + # Only the placeholders may appear in the install config in the Nix store. + machine.succeed("grep -q '@adminPassword@' /nix/store/*-config.json") + machine.succeed("grep -q '@databasePassword@' /nix/store/*-config.json") + machine.fail("grep -qe 'flarum-admin-password' -e 'flarum-db-password' /nix/store/*-config.json") + + # A successful login proves the admin password was substituted intact. + machine.succeed( + "curl -sf http://localhost/api/token " + + "-H 'Content-Type: application/json' " + + "-d '{\"identification\": \"admin\", \"password\": \"flarum-admin-password\"}' " + + "| grep -F token" + ) + + # The database password must arrive intact in config.php, which must + # not be world-readable. + machine.succeed("grep -q 'flarum-db-password' /var/lib/flarum/config.php") + machine.succeed("[ $(stat -c %a /var/lib/flarum/config.php) = 600 ]") ''; }