Closing the command-center security queue #70/#71/#72/#73/#74: rolling tags let a rebuild move under a running service with nobody approving it, and make a CVE audit ungradeable. 36 quadlet units across dankclaw, dankserver and frankbook-pro now carry tag@sha256; localhost/* builds stay exempt (their build is the pin). The pihole and copyparty pins pull the fixed builds; valkey 9.1.2 (already running) and redis 7.4.11 are pinned in place, vendor fixes TBD. New gate tests/quadlet_image_pins.bats: no unpinned registry image, no generator-rejected key anywhere. Three seams that pinned the old exact-tag law (frankbook readarr, open_webui, seerr) now require tag AND digest - strictly stronger. Quadlet Lifecycle docs: batch-abort law, entrypoint flags-only law, pin law.
95 lines
4.9 KiB
INI
95 lines
4.9 KiB
INI
# Quadlet unit: grafana (rootless, boot-persistent).
|
|
#
|
|
# Install path on dankclaw: ~/.config/containers/systemd/grafana.container
|
|
# Unit name: grafana.service (container name: grafana)
|
|
#
|
|
# Dashboards. This replaces the compose service of the same name in
|
|
# dankclaw/compose.yaml ("Observability" block).
|
|
#
|
|
# PUBLISHING. Bound to 127.0.0.1 for the same reason as prometheus: the browser
|
|
# path is https://grafana.franksdank.cloud (Caddy → Anubis → Authentik outpost →
|
|
# grafana:3000 over dankclaw_default) and the host-side homepage status timer
|
|
# probes 127.0.0.1:3002. Publishing on 0.0.0.0 as the compose service did left
|
|
# an off-host path into Grafana that skips the guard chain entirely.
|
|
#
|
|
# AUTH. Grafana is an OAuth2 relying party against Authentik (OAuth2Provider
|
|
# "grafana", confidential client). The guard chain fronts unauthenticated
|
|
# traffic; the login itself happens through the Authentik button inside
|
|
# Grafana. The client secret is NOT written here: it is expanded by systemd
|
|
# from the root .env (EnvironmentFile in [Service], never in [Container], so
|
|
# the container receives exactly this one secret and not the whole file).
|
|
# Without that .env entry the unit will not start — the same dependency every
|
|
# other rendered-secret deploy has.
|
|
#
|
|
# STATE. /opt/grafana/data holds grafana.db. /opt/grafana/provisioning is
|
|
# mounted read-only and was EMPTY as of 2026-09-11, and grafana.db reported
|
|
# zero datasources and zero dashboards: this Grafana is an empty install whose
|
|
# only configured account is the local admin. Nothing here queries Prometheus
|
|
# or Loki yet.
|
|
|
|
[Unit]
|
|
Description=Grafana dashboards (Dankclaw)
|
|
After=network-online.target prometheus.service
|
|
Wants=network-online.target prometheus.service
|
|
|
|
[Container]
|
|
ContainerName=grafana
|
|
Image=docker.io/grafana/grafana:latest@sha256:dbbc38ea21103cefb50396340cd88afec779c7e070b0a7bf5a75b4ce46651d3f
|
|
Network=dankclaw_default
|
|
User=0:0
|
|
PublishPort=127.0.0.1:3002:3000
|
|
Environment=GF_SERVER_ROOT_URL=https://grafana.franksdank.cloud
|
|
Environment=GF_AUTH_GENERIC_OAUTH_ENABLED=true
|
|
Environment=GF_AUTH_GENERIC_OAUTH_NAME=Authentik
|
|
Environment=GF_AUTH_GENERIC_OAUTH_CLIENT_ID=grafana-21ca190cde75d60f
|
|
Environment=GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=${GRAFANA_OAUTH_CLIENT_SECRET}
|
|
# Global authorize path: this Authentik build resolves the provider by the
|
|
# client_id query parameter — /application/o/<slug>/authorize/ does not exist.
|
|
Environment=GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.franksdank.cloud/application/o/authorize/
|
|
Environment=GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.franksdank.cloud/application/o/token/
|
|
# Standard OIDC userinfo. Grafana v13 fetches <api_url>/emails for the address
|
|
# ONLY when the token carries no `email` claim (authentik has no /emails path and
|
|
# 404s it, failing the login) — so the claim must actually be there: the fix is
|
|
# the grafana-email-scope mapping (see scripts/authentik-grafana-sso-fix.sh),
|
|
# not a Grafana flag. Do NOT append ?api=1 to this URL: Grafana then GETs
|
|
# <...>/userinfo/?api=1/emails, a 403.
|
|
Environment=GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.franksdank.cloud/application/o/userinfo/
|
|
# authentik's scopes are openid/email/profile. `user:email` (a GitLab-ism) is
|
|
# not a known scope here; requesting it makes the authorize view crash.
|
|
# Quoted: quadlet word-splits Environment=, so an unquoted spaced list
|
|
# truncates SCOPES to its first token.
|
|
Environment="GF_AUTH_GENERIC_OAUTH_SCOPES=openid email profile"
|
|
Environment=GF_AUTH_GENERIC_OAUTH_EMAIL_ATTRIBUTE_PATH=email
|
|
Environment=GF_AUTH_GENERIC_OAUTH_LOGIN_ATTRIBUTE_PATH=sub
|
|
Environment=GF_AUTH_GENERIC_OAUTH_NAME_ATTRIBUTE_PATH=preferred_username
|
|
# First SSO login auto-provisions the user. Safe: unauthenticated traffic never
|
|
# reaches this callback — the Anubis -> Authentik guard fronts every request.
|
|
Environment=GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true
|
|
Environment=GF_AUTH_GENERIC_OAUTH_AUTO_UPSERT=true
|
|
Environment=GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_ORG=true
|
|
Environment=GF_AUTH_GENERIC_OAUTH_ORG_ID=1
|
|
Volume=/opt/grafana/data:/var/lib/grafana
|
|
Volume=/opt/grafana/provisioning:/etc/grafana/provisioning:ro
|
|
HealthCmd=wget --spider -q http://localhost:3000/api/health
|
|
HealthInterval=30s
|
|
HealthTimeout=10s
|
|
HealthRetries=3
|
|
HealthStartPeriod=60s
|
|
|
|
[Service]
|
|
# 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
|
|
|
|
[Install]
|
|
WantedBy=default.target
|