fix(observability): seed grafana's OAuth secret into a per-service env file

The first deploy of the grafana unit failed: 'stat /root/.config: permission
denied', exit 1, restart loop. Cause — the repo-root .env that the compose
service used to pick up is a gluetun env file, and it carries HOME=/root (and
the WireGuard keys). Pointing the unit's [Service] EnvironmentFile at it sets
HOME for the service, so podman looks for its configuration under /root.

grafana now reads /home/dank/Containers/grafana/env, the per-service file the
deployer seeds from dankclaw/.env with a single-variable filter — the same
shape caddy, pihole, copyparty, romm and seerr use. The seed is validated, so
a missing secret fails the deploy loudly instead of starting a container whose
OAuth login cannot complete. The value was copied to dankclaw/.env, which is
where dankclaw/.env.example already documented it.
This commit is contained in:
Frank
2026-09-11 15:59:15 -07:00
parent dab81b91a3
commit fe4eeaa03a
3 changed files with 53 additions and 10 deletions
@@ -62,9 +62,16 @@ HealthRetries=3
HealthStartPeriod=60s
[Service]
# systemd-side only: supplies ${GRAFANA_OAUTH_CLIENT_SECRET} for expansion
# above. The container does not receive this file.
EnvironmentFile=/home/dank/serverAppConfigs/.env
# systemd-side only: supplies ${GRAFANA_OAUTH_CLIENT_SECRET} for the expansion
# above. The container never receives this file.
#
# It is the per-service env file the deployer seeds from dankclaw/.env — the
# fleet convention (caddy, pihole, copyparty, romm and seerr all read one).
# The repo-root .env that the compose service happened to pick up is a gluetun
# env file: it carries HOME=/root and the WireGuard keys, and importing it into
# this service makes podman look for its configuration under /root, which it
# cannot read, so the unit fails to start. Seed the value into dankclaw/.env.
EnvironmentFile=/home/dank/Containers/grafana/env
Restart=always
RestartSec=10
+22 -1
View File
@@ -378,6 +378,24 @@ deploy_compose_service() {
esac
}
# grafana is the one observability service that holds a credential: the
# Authentik OAuth2 client secret. It is seeded into a per-service env file (the
# fleet convention: caddy, pihole, copyparty, romm, seerr) instead of being
# imported from the repo .env wholesale. The repo-root .env that the compose
# service used to pick up is a gluetun env file — it also carries HOME=/root
# and the WireGuard keys, and an EnvironmentFile of that file sets HOME for the
# service, which makes podman look for its config under /root and fail.
seed_grafana_env() {
local dest=/home/dank/Containers/grafana/env
mkdir -p /home/dank/Containers/grafana
if [[ ! -f "$dest" ]]; then
awk -F= '/^GRAFANA_OAUTH_CLIENT_SECRET=/ { print }' dankclaw/.env > "$dest"
chmod 600 "$dest"
fi
grep -q '^GRAFANA_OAUTH_CLIENT_SECRET=.' "$dest" \
|| { echo "grafana env missing GRAFANA_OAUTH_CLIENT_SECRET; set it in dankclaw/.env" >&2; return 1; }
}
# The five observability containers left compose on 2026-09-11 (they were
# started by hand with podman-compose and never came back after a reboot).
# They are ordinary rootless Quadlet units now: same images, same mounts, same
@@ -399,7 +417,10 @@ deploy_observability() {
copy_alloy_config
build_compose_image alloy
;;
grafana|node-exporter)
grafana)
seed_grafana_env || return 1
;;
node-exporter)
;;
*)
echo "unknown observability service: $service" >&2
+21 -6
View File
@@ -146,17 +146,32 @@ service_section() {
done
}
@test "grafana receives the OAuth client secret without receiving the whole .env" {
# systemd expands ${GRAFANA_OAUTH_CLIENT_SECRET} in [Container] from the
# [Service] EnvironmentFile. Putting that file in [Container] would hand
# the container every fleet secret in it (Porkbun keys, Authentik keys,
# the sudo password file's neighbours) for the sake of one value.
@test "grafana gets its OAuth client secret without getting a whole env file" {
# systemd expands ${GRAFANA_OAUTH_CLIENT_SECRET} for the [Container]
# Environment= line from the [Service] EnvironmentFile, which keeps the
# file out of the container entirely. That file is the per-service one the
# deployer seeds (fleet convention: caddy, pihole, copyparty, romm, seerr)
# — NOT a repo .env. The repo-root .env the compose service happened to
# read is a gluetun env file carrying HOME=/root and the WireGuard keys;
# importing it sets HOME for this service and podman then looks for its
# configuration under /root and refuses to start.
local unit="$UNIT_DIR/grafana.container"
service_section "$unit" | grep -q '^EnvironmentFile=/home/dank/serverAppConfigs/.env$'
service_section "$unit" | grep -q '^EnvironmentFile=/home/dank/Containers/grafana/env$'
! service_section "$unit" | grep -q 'serverAppConfigs'
! container_section "$unit" | grep -q '^EnvironmentFile='
container_section "$unit" | grep -q '^Environment=GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=\${GRAFANA_OAUTH_CLIENT_SECRET}$'
}
@test "the deployer seeds exactly one value into the grafana env file" {
# The single-variable filter is the point: a whole-file copy is how the
# HOME=/root failure got in.
grep -q '^seed_grafana_env() {' "$REMOTE_DEPLOY"
grep -q '^ local dest=/home/dank/Containers/grafana/env$' "$REMOTE_DEPLOY"
grep -q "awk -F= '/^GRAFANA_OAUTH_CLIENT_SECRET=/ { print }' dankclaw/.env > \"\$dest\"" "$REMOTE_DEPLOY"
grep -q "grep -q '\^GRAFANA_OAUTH_CLIENT_SECRET=\.' \"\$dest\"" "$REMOTE_DEPLOY"
grep -q 'seed_grafana_env || return 1' "$REMOTE_DEPLOY"
}
@test "observability units hold no literal secret material" {
# A secret-named setting is allowed only when its value is a ${VAR}
# reference resolved from an EnvironmentFile systemd reads; a literal value