aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md34
-rw-r--r--modules/server.nix89
2 files changed, 95 insertions, 28 deletions
diff --git a/README.md b/README.md
index 9014e1b..5c8e1dc 100644
--- a/README.md
+++ b/README.md
@@ -76,14 +76,15 @@ nixosConfigurations.poweredge = nixpkgs.lib.nixosSystem {
collectors.enable = true;
server = {
enable = true;
- grafana.secretKeyFile = "/run/secrets/grafana-secret-key";
proxy.enable = true; # Grafana on :80
};
};
}
```
-Grafana at `http://localhost`. `collectorHosts` defaults to `[ "localhost" ]`.
+Grafana at `http://localhost`. `collectorHosts` defaults to `[ "localhost" ]`, and
+Grafana's secret key is generated on first start into `/var/lib/grafana/secret-key`
+(mode 0600, owned by `grafana`) — see below if you would rather supply it yourself.
## Example: web services in a container
@@ -91,7 +92,7 @@ The host runs the collectors; the container runs everything else. This is the in
shape.
```nix
-{ config, inputs, ... }:
+{ inputs, ... }:
{
imports = [ inputs.telemetry.nixosModules.collectors ];
@@ -100,23 +101,12 @@ shape.
logs.lokiUrl = "http://metrics:3100"; # the container, by name
};
- sops.secrets.grafana-secret-key = {
- sopsFile = ./secrets/grafana.yaml;
- key = "secret_key";
- };
-
containers.metrics = {
autoStart = true;
privateNetwork = true;
hostBridge = "br-lan0";
localMacAddress = "02:00:00:00:00:06";
- # The module reads whatever path you point secretKeyFile at.
- bindMounts."/run/grafana-secret-key" = {
- hostPath = config.sops.secrets.grafana-secret-key.path;
- isReadOnly = true;
- };
-
config = { ... }: {
imports = [ inputs.telemetry.nixosModules.server ];
@@ -126,7 +116,6 @@ shape.
services.telemetry.server = {
enable = true;
collectorHosts = [ "poweredge" ]; # the host above
- grafana.secretKeyFile = "/run/grafana-secret-key";
proxy.enable = true;
};
@@ -262,8 +251,19 @@ inventing placeholder values.
render "No data". Don't configure the Grafana datasource by hand.
- **The datasource UIDs `prometheus` and `loki` are fixed, not options.** The generated
dashboards reference them literally.
-- **Grafana's secret key is mandatory** as of nixpkgs 26.05. `openssl rand -base64 32`,
- store it somewhere, point `grafana.secretKeyFile` at it.
+- **Grafana's secret key is mandatory** as of nixpkgs 26.05, because Grafana's upstream
+ default is a constant compiled into every release. By default this module generates one
+ on first start into `/var/lib/grafana/secret-key` (0600, owned by `grafana`), so there
+ is nothing to wire up. Two things to know: it must stay *stable*, or credentials
+ encrypted in Grafana's database stop decrypting — so keep the Grafana data directory on
+ persistent storage; and it sits beside the database it protects, so it defends against
+ a stolen `grafana.db` but not against filesystem access. Set
+ `grafana.secretKeyFile` to a path you manage instead once the database holds anything
+ worth protecting, such as a datasource password:
+ ```nix
+ services.telemetry.server.grafana.secretKeyFile =
+ config.sops.secrets.grafana-secret-key.path; # turns generation off
+ ```
- **Changing `collectorHosts` changes the label set.** Panels look empty for a minute
after first deploy while the recording rules re-evaluate; data from before the change
stays under the old labels.
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