diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/server.nix | 89 |
1 files changed, 78 insertions, 11 deletions
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 |
