Watch
0
0
Fork
You've already forked hyperhive
0

grafana: mint the admin password into the store and apply it on every start

Grafana's built-in admin kept the nixpkgs default password `admin`, and
Grafana's API takes basic auth whatever `auth.disable_login_form` says, so
the public vhost gave server-admin to `admin:admin`.

`swarm-bao-grafana-admin` creates a random password in the store under
Grafana's existing leaf when none is stored (`create` without `update`, so
the first one is kept), then copies it into the container.
`security.admin_password` reads it for a new database, and a `grafana cli
admin reset-admin-password` in `grafana.service`'s `preStart` applies it to
an existing one on every start.

Closes #5138
This commit is contained in:
atlas 2026-10-12 11:22:59 +02:00 • committed by mara
commit 993f5fd99d
7 changed files with 251 additions and 48 deletions

View file

@ -557,13 +557,28 @@ let
# for this one is the OTHER service's: `services/*` would have granted
# both, and the two are separate principals precisely because a
# dashboard is not entitled to a collector's credential.
name = "the Grafana OIDC reader's grant is Grafana's own client secret and nothing else";
#
# The admin password is the one leaf it may create, and without `update`:
# a grant that could replace it would let a second run rotate the
# password Grafana's database already holds.
name = "the Grafana reader reads its own client secret and creates its admin password, and nothing else";
ok =
let
s = baoGrantHere.systemd.services.swarm-bao-grafana-oidc-policy.script;
in
lib.hasInfix "path \"secret/data/swarm/services/swarm-grafana/oidc/client\" {" s
&& lib.hasInfix "capabilities = [\"read\"]" s
grantsIn s == [
{
path = "secret/data/swarm/services/swarm-grafana/oidc/client";
caps = [ "read" ];
}
{
path = "secret/data/swarm/services/swarm-grafana/admin/password";
caps = [
"create"
"read"
];
}
]
&& !(lib.hasInfix "secret/data/swarm/agents" s)
&& !(lib.hasInfix "secret/data/swarm/hives" s)
&& !(lib.hasInfix "secret/data/swarm/services/*" s)
@ -827,6 +842,20 @@ let
&& !(noReaders ? swarm-bao-matrix-oidc)
&& !(noReaders ? swarm-bao-nats-callout-xkey);
}
{
# The admin password is created under Grafana's leaf, so its unit waits
# for the unit writing that leaf's role and policy; before it, the create
# is refused. The second arm is the absence control.
name = "grafana's admin password unit is ordered after grafana's policy unit";
ok =
let
u = baoGrantWithConsumers.systemd.services.swarm-bao-grafana-admin;
in
lib.elem "swarm-bao-grafana-oidc-policy.service" u.after
&& lib.elem "swarm-bao-grafana-oidc-policy.service" u.wants
&& !(lib.elem "swarm-bao-grafana-oidc-policy.service" u.requires)
&& !(baoGrantNoReaders.systemd.services ? swarm-bao-grafana-admin);
}
{
# The ordering is set apart from each reader's own definition, so it can
# define a reader by itself: `after` on a unit nothing else declares is a
@ -959,6 +988,7 @@ let
[
"swarm-services-cert"
"swarm-bao-forwarder-oidc"
"swarm-bao-grafana-admin"
]
++ policyReaders
);

View file

@ -123,9 +123,8 @@ let
{
# This fixture enables grafana and NOT authelia, so the login-form toggle
# has to follow "grafana requires SSO" rather than "both services are on
# this host". Grafana ships an
# `admin`/`admin` account and its vhost is on the public gateway, so a
# password box there is a way in whatever the topology.
# this host". Its vhost is on the public gateway, so a password box there
# is a way in whatever the topology.
name = "grafana disables its local login form even where authelia is not on this host";
ok =
grafanaOldPath.containers.swarm-grafana.config.services.grafana.settings.auth.disable_login_form;
@ -196,6 +195,36 @@ let
&& !(grafanaRefusedFor grafanaRemoteAuthelia "services.hyperhive.swarm.authelia.url")
&& !(grafanaRefusedFor grafanaRemoteAuthelia "deploy.bao.grafanaOidcClientCertFile");
}
{
# Grafana's API takes basic auth whatever the login form says, so the
# built-in admin's password is the only thing between the public vhost
# and server-admin. Upstream sets it only when it creates the database,
# so both halves are pinned: the setting a new database reads, and the
# reset every start applies to whichever database exists. Without the
# reset, a database created before the setting keeps `admin`.
#
# The reset's `--config` must be the file the server reads, or it resets
# a different database: compared by store path against the server's own
# command line.
name = "grafana's admin password is minted into the store and applied to new and existing databases";
ok =
let
unit = grafanaRemoteAuthelia.systemd.services.swarm-bao-grafana-admin;
grafana = grafanaRemoteAuthelia.containers.swarm-grafana.config.services.grafana;
service = grafanaRemoteAuthelia.containers.swarm-grafana.config.systemd.services.grafana;
resetConfig = lib.head (
lib.head (lib.filter lib.isList (builtins.split "--config ([^ ]+)" service.preStart))
);
in
lib.hasInfix "bao kv put -cas=0 secret/swarm/services/swarm-grafana/admin/password value=-" unit.script
&& lib.hasInfix "openssl rand -hex 32" unit.script
&& lib.hasInfix "atomic_write_secret 0400 196:0 /var/lib/nixos-containers/swarm-grafana/var/lib/grafana-admin/password" unit.script
&& lib.elem "container@swarm-grafana.service" unit.before
&& unit.environment.BAO_CLIENT_CERT == "/etc/pki/bao-grafana-oidc.pem"
&& grafana.settings.security.admin_password == "$__file{/var/lib/grafana-admin/password}"
&& lib.hasInfix "admin reset-admin-password --password-from-stdin < /var/lib/grafana-admin/password" service.preStart
&& lib.hasInfix "-config ${resetConfig}" service.script;
}
{
# Same 403-not-a-miss reason as the matrix and queue arms below: the
# reader's grant covers the `services` prefix, so a path outside it is