fix(#3370): give Grafana a secret_key, generated in-container on first boot
nixpkgs dropped the default for `services.grafana.settings.security.secret_key`
and asserts on null, so the module I merged an hour ago fails the build on any
host that enables it. That is a broken deploy, not a warning.
Generated in-container like authelia's own keys and for the same reason:
nothing outside the container ever reads it. Generated ONCE and kept — the
key signs Grafana's stored datasource secrets, and rotating it does not
re-encrypt what it already encrypted, so a fresh key per boot would leave
Grafana unable to read its own database. Delivered as `$__file{}`, so it
never enters the store.
The gate missed this because a container is a separate NixOS evaluation with
its own assertions: forcing the host's `config.assertions` never touched the
container's. `state/eval-3265.sh` now forces
`containers.swarm-grafana.config.assertions` and reports any that fail.
This commit is contained in:
parent
405ed85550
commit
859833b186
1 changed files with 56 additions and 0 deletions
|
|
@ -41,6 +41,11 @@ let
|
||||||
# intermittent one.
|
# intermittent one.
|
||||||
secretPath = "/var/lib/grafana-oidc/${cfg.oidc.clientId}.secret";
|
secretPath = "/var/lib/grafana-oidc/${cfg.oidc.clientId}.secret";
|
||||||
|
|
||||||
|
# Grafana's own datasource-encryption key. Generated in-container (see the
|
||||||
|
# unit below) because nothing outside the container ever reads it — unlike
|
||||||
|
# the OIDC secret above, whose other reader is authelia's container.
|
||||||
|
secretKeyPath = "/var/lib/grafana-secret/secret_key";
|
||||||
|
|
||||||
# Format-locked by Grafana: the generic OAuth callback is always
|
# Format-locked by Grafana: the generic OAuth callback is always
|
||||||
# `<root_url>/login/generic_oauth`. Declared once here and read by both
|
# `<root_url>/login/generic_oauth`. Declared once here and read by both
|
||||||
# the authelia client and Grafana itself.
|
# the authelia client and Grafana itself.
|
||||||
|
|
@ -344,6 +349,52 @@ in
|
||||||
|
|
||||||
systemd.services.grafana.environment.SSL_CERT_FILE = lib.mkIf useSelfSigned grafanaCaBundle;
|
systemd.services.grafana.environment.SSL_CERT_FILE = lib.mkIf useSelfSigned grafanaCaBundle;
|
||||||
|
|
||||||
|
# Grafana's `secret_key` has **no default in nixpkgs** and an
|
||||||
|
# assertion refuses the build without one — which is how the first
|
||||||
|
# version of this module broke a deploy. It signs the datasource
|
||||||
|
# secrets in Grafana's own database.
|
||||||
|
#
|
||||||
|
# Generated in-container on first boot, like authelia's own keys and
|
||||||
|
# for the same reason: nothing outside this container ever reads it,
|
||||||
|
# which makes in-container generation right rather than merely
|
||||||
|
# easier. (The OIDC client secret is delivered host-side precisely
|
||||||
|
# because it has a second reader.)
|
||||||
|
#
|
||||||
|
# ⚠️ Generated ONCE and kept — the `-s` guard is load-bearing.
|
||||||
|
# Rotating this key does not re-encrypt what it already encrypted, so
|
||||||
|
# a fresh key on every boot would leave Grafana unable to decrypt its
|
||||||
|
# own stored datasource secrets. Under /var/lib, never /run.
|
||||||
|
systemd.services.swarm-grafana-secret-key = {
|
||||||
|
description = "generate Grafana's datasource encryption key on first boot";
|
||||||
|
wantedBy = [ "grafana.service" ];
|
||||||
|
before = [ "grafana.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
# Same User/Group/StateDirectory as grafana itself, so systemd
|
||||||
|
# creates the directory owned by the account that has to read
|
||||||
|
# the file — no chown, no mode juggling.
|
||||||
|
User = "grafana";
|
||||||
|
Group = "grafana";
|
||||||
|
StateDirectory = "grafana-secret";
|
||||||
|
StateDirectoryMode = "0700";
|
||||||
|
SyslogIdentifier = "swarm-grafana-secret-key";
|
||||||
|
};
|
||||||
|
path = [
|
||||||
|
pkgs.openssl
|
||||||
|
pkgs.coreutils
|
||||||
|
];
|
||||||
|
script = ''
|
||||||
|
set -euo pipefail
|
||||||
|
key=${lib.escapeShellArg secretKeyPath}
|
||||||
|
if [ ! -s "$key" ]; then
|
||||||
|
umask 077
|
||||||
|
openssl rand -hex 32 > "$key"
|
||||||
|
echo "generated Grafana's datasource encryption key"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
services.grafana = {
|
services.grafana = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = cfg.package;
|
package = cfg.package;
|
||||||
|
|
@ -371,6 +422,11 @@ in
|
||||||
|
|
||||||
users.auto_assign_org_role = cfg.oidc.role;
|
users.auto_assign_org_role = cfg.oidc.role;
|
||||||
|
|
||||||
|
# ⚠️ `$__file{}`, and required rather than optional: nixpkgs
|
||||||
|
# dropped the default and asserts on null, so without this the
|
||||||
|
# whole host fails to build. The unit below generates it.
|
||||||
|
security.secret_key = "$__file{${secretKeyPath}}";
|
||||||
|
|
||||||
# No local password path at all when SSO is configured. This
|
# No local password path at all when SSO is configured. This
|
||||||
# is not tidiness: Grafana ships an `admin`/`admin` account,
|
# is not tidiness: Grafana ships an `admin`/`admin` account,
|
||||||
# and this vhost is on the public gateway.
|
# and this vhost is on the public gateway.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue