From 89f2c328fc1b113a9e9b862841e0c7d0391b3d09 Mon Sep 17 00:00:00 2001 From: Tim Keller Date: Thu, 27 Aug 2026 23:07:02 -0500 Subject: generate Grafana's secret key by default Grafana's upstream default for security.secret_key is a constant compiled into every release, which is why nixpkgs 26.05 stopped passing it through and started asserting. Requiring secretKeyFile made that assertion the first thing anyone hit, and forced a secret manager plus a container bindMount on a deployment whose Grafana database holds no credentials at all -- both datasources are provisioned without any. generateSecretKey defaults to true (and to false as soon as secretKeyFile is set), writing 32 random bytes to Grafana's data directory on first start. The unit runs as the grafana user into a directory users.users.grafana.createHome already owns, so there is no chown and nothing fights Grafana for the directory. UMask=0077 makes the file 0600 on creation and an unconditional chmod repairs one left readable by a previous version or a manual copy. The tradeoff is stated in the option description rather than hidden: the key lives beside the database it encrypts, so it defends against a stolen grafana.db, not against filesystem access. secretKeyFile remains for when the database holds a datasource password or a contact-point token. Verified both paths build: with no secret wiring the generator unit is present and LoadCredential points into the data directory; with secretKeyFile set the generator is absent and LoadCredential points at the supplied path. Both emit secret_key=$__file{...} into config.ini. The script is idempotent, leaves an existing key untouched, and restores 0600 on a file chmodded to 0644. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JKczN1f66kmLxNBLX37voQ --- modules/server.nix | 89 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 11 deletions(-) (limited to 'modules/server.nix') diff --git a/modules/server.nix b/modules/server.nix index 1996f5e..d9449a8 100644 --- a/modules/server.nix +++ b/modules/server.nix @@ -34,6 +34,13 @@ let replacement = "$1"; }]; + # Where Grafana's secret key actually comes from: a file you supply, or one this + # module generates into Grafana's data directory on first start. + secretKeyPath = + if cfg.grafana.generateSecretKey + then "${config.services.grafana.dataDir}/secret-key" + else cfg.grafana.secretKeyFile; + builtinDashboards = pkgs.callPackage ./dashboards.nix { inherit dsUid; }; selected = filterAttrs (name: _: cfg.grafana.dashboards.${name} or false) builtinDashboards; @@ -185,6 +192,31 @@ in description = "Run Grafana, provisioned with datasources and dashboards."; }; + generateSecretKey = mkOption { + type = types.bool; + default = cfg.grafana.secretKeyFile == null; + defaultText = lib.literalMD "`true` unless {option}`secretKeyFile` is set"; + description = '' + Generate a random `security.secret_key` on first start and keep it in + Grafana's data directory, instead of supplying one yourself. + + Grafana needs this key to be *stable* -- it encrypts credentials stored in + Grafana's database, and they become undecryptable if it changes. It has an + upstream default, but that default is a constant compiled into every Grafana + release, so leaving it unset means encrypting with a publicly known key; + nixpkgs asserts rather than let that happen silently. + + The generated file is written by the grafana user with mode 0600, so only + Grafana and root can read it. Note it then lives beside the database it + protects: anyone who can read `grafana.db` can read the key too. That is a + real limitation, and it is why {option}`secretKeyFile` exists -- but the key + is still per-install and secret from anyone without filesystem access, which + is the property that actually matters here. Prefer `secretKeyFile` once + Grafana's database holds credentials worth protecting, such as a datasource + password or a contact-point API token. + ''; + }; + secretKeyFile = mkOption { type = types.nullOr types.path; default = null; @@ -245,14 +277,24 @@ in # Grafana uses this key to encrypt datasource credentials in its database, and # nixpkgs stopped shipping a default in 26.05. Caught here rather than left to # Grafana's own assertion, which names an option this module manages for you. - assertions = [{ - assertion = !cfg.grafana.enable || cfg.grafana.secretKeyFile != null; - message = '' - services.telemetry.server.grafana.secretKeyFile must be set when Grafana is - enabled. Generate a key (`openssl rand -base64 32`), store it with whatever - secret manager you use, and point this option at the resulting file. - ''; - }]; + assertions = [ + { + assertion = !(cfg.grafana.generateSecretKey && cfg.grafana.secretKeyFile != null); + message = '' + services.telemetry.server.grafana.generateSecretKey and .secretKeyFile are + mutually exclusive. Setting secretKeyFile turns generation off by default, so + this only fires if you asked for both explicitly. + ''; + } + { + assertion = !cfg.grafana.enable || secretKeyPath != null; + message = '' + Grafana needs a secret key. Either leave + services.telemetry.server.grafana.generateSecretKey at its default so one is + generated on first start, or point .secretKeyFile at a file you manage. + ''; + } + ]; # -------------------------------------------------------------------------------- # Loki @@ -338,7 +380,7 @@ in } # A file provider rather than a literal, so the key is never written into a # world-readable store path. The credential is mounted by LoadCredential below. - // lib.optionalAttrs (cfg.grafana.secretKeyFile != null) { + // lib.optionalAttrs (secretKeyPath != null) { security.secret_key = "$__file{/run/credentials/grafana.service/secret_key}"; }; @@ -389,10 +431,35 @@ in }; }; - systemd.services.grafana = mkIf (cfg.grafana.enable && cfg.grafana.secretKeyFile != null) { - serviceConfig.LoadCredential = [ "secret_key:${cfg.grafana.secretKeyFile}" ]; + systemd.services.grafana = mkIf (cfg.grafana.enable && secretKeyPath != null) { + serviceConfig.LoadCredential = [ "secret_key:${secretKeyPath}" ]; }; + # Runs as the grafana user, into a directory users.users.grafana.createHome already + # made and owns -- so there is no chown, and no fighting with Grafana over the + # directory's ownership. UMask makes the file 0600 on creation; the explicit chmod + # also repairs a file left too permissive by an earlier version or a manual copy. + systemd.services.grafana-secret-key = + mkIf (cfg.grafana.enable && cfg.grafana.generateSecretKey) { + description = "Generate Grafana's secret key if it does not exist yet"; + wantedBy = [ "grafana.service" ]; + before = [ "grafana.service" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = "grafana"; + Group = "grafana"; + UMask = "0077"; + }; + script = '' + key=${lib.escapeShellArg secretKeyPath} + if [ ! -s "$key" ]; then + ${pkgs.openssl}/bin/openssl rand -base64 32 > "$key" + fi + chmod 0600 "$key" + ''; + }; + environment.etc = mkIf cfg.grafana.enable ( mapAttrs' (name: src: nameValuePair "grafana-dashboards/${name}.json" { source = src; }) dashboards -- cgit v1.3.1