Compare commits

...

4 Commits

Author SHA1 Message Date
e012654915 OpenBAO-Server 2.6.2 -> 2.7.1 (Unsealer bleibt vorerst)
Die CLI auf pi5-1 ist seit dem 01.10. 2.7.1, der Server lief noch auf 2.6.2.
Der Hauptknoten entsiegelt sich per Transit ueber den Sidecar selbst, der
Austausch brauchte also keine Schluessel: Log zeigt "unsealed with stored key"
und "Recorded vault version: 2.7.1".

Der Sidecar openbao-unsealer bleibt bewusst auf 2.6.2. Er ist Shamir-versiegelt
mit Schwelle 1/1 und muesste nach einem Austausch von Hand entsiegelt werden -
und solange er versiegelt ist, kommt auch der Hauptknoten beim naechsten Start
nicht hoch. Die Transit-API ist versionsuebergreifend stabil, der Mischbetrieb
ist also unkritisch.

Geprueft nach dem Austausch: Siegelstatus, Lesezugriff ueber Traefik-TLS von
pi5-2 aus und der git-credential-Helfer gegen Gitea.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-24 09:39:32 +02:00
f3cfe777b6 mt-az: vollstaendige Zertifikatskette, plus Pruefung beim Erneuern
api-ssl auf mt-az lieferte nur das Blattzertifikat aus. Gegen die famfi-Root-CA
allein scheiterte die Pruefung deshalb mit "unable to get local issuer
certificate" - jeder strenge TLS-Client braucht die Zwischen-CA separat
zugesteckt. Der CrowdSec-Bouncer funktionierte nur, weil er mit
famfi-ca-bundle.pem (Root + Intermediate) konfiguriert ist.

Ursache war nicht das Skript, sondern der Zertifikatsspeicher des Routers: die
Zwischen-CA lag dort gar nicht. RouterOS verknuepft Blatt und CA beim IMPORT des
Blattes (Feld ca=), und ohne die CA im Speicher entsteht diese Verknuepfung nicht.
Nachtraeglich laesst sie sich auch nicht setzen - "bad parameter ca".

Behoben in zwei Schritten: die Zwischen-CA als famfi-int-ca-20261022.crt_0
importiert, danach ein --force-Renew, damit RouterOS das neue Blatt mit ihr
verknuepft. api-ssl liefert jetzt Blatt + Zwischen-CA, und die Pruefung gegen die
Root allein gibt "Verify return code: 0 (ok)".

Das Skript selbst brauchte dafuer keine Aenderung - es schreibt weiterhin nur
d["certificate"]. Es funktioniert aber nur, SOLANGE die Zwischen-CA im Speicher
liegt. Darum hier ergaenzt:

- Hinweis im Kopf auf diese Abhaengigkeit.
- Kettenpruefung am Ende: nach dem Austausch wird gegen $ROOT_CA verifiziert.
  Bewusst nur eine Warnung, kein die() - das Zertifikat waere ja in Ordnung, die
  fehlende Kette ist ein Mangel, kein Ausfall. Ein Abbruch wuerde einen
  erfolgreichen Austausch als Fehlschlag melden.

Die Pruefung laeuft nur bei einer tatsaechlichen Erneuerung, nicht beim taeglichen
Leerlauf - also genau dann, wenn die Verknuepfung verloren gehen koennte. Die Zeit
dazwischen deckt die Blackbox-Probe ab: deren Modul tcp_tls_famfi zeigt seit heute
auf die Root-CA allein, scheitert also, wenn die Kette je wieder wegfaellt.

Nachgewiesen: Bouncer nach dem Zertifikatswechsel neu gestartet, frischer
TLS-Handshake ohne x509-Fehler, arbeitet Entscheidungen ab; 16145 Eintraege in der
Router-Sperrliste, 82591 verworfene Pakete.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-22 13:28:47 +02:00
a9e51fbc8c MikroTik-Zertifikat erneuern und automatisieren
Das API-SSL-Zertifikat von mt-az.famfi.home (CN=mt-az.famfi.home, ausgestellt
von famfi.home Intermediate CA, 90 Tage) lief am 2026-09-26 ab. Folge: der
Container crowdsec-mikrotik-bouncer starb im Minutentakt mit
"x509: certificate has expired" - 967 Neustarts, letzter erfolgreicher
API-Pull 2026-09-27 02:29. Zehn Tage lang wurden CrowdSec-Sperren also nur
noch von Traefik durchgesetzt und nicht mehr in die Router-Firewall
geschoben.

Die eigentliche Ursache war nicht das Zertifikat, sondern dass niemand
hinsah: fuer openbao und gitea gibt es Erneuerungsskripte samt Timer (daher
sind die mit 58 bzw. 73 Tagen in Ordnung), fuer den Router gab es keins. Und
die Regeln TLSCertificateExpiringSoon/-Critical haengen am blackbox_exporter,
der nur openbao.famfi.home und www_mahle_com prueft - mt-az:8729 steht in
keiner Probe.

renew-mikrotik-cert.sh ist die Kopie von renew-gitea-cert.sh mit dem
Router-spezifischen Installationsschritt: Zertifikat und Schluessel per scp
hochladen, zweimal /certificate import mit leerem passphrase (RouterOS
protokolliert Kommandos mit, deshalb darf dort nichts Geheimes stehen),
api-ssl umschalten, hochgeladene Dateien entfernen und das alte Objekt erst
danach loeschen. Der Name traegt einen Datumsstempel, weil RouterOS den
Zertifikatsnamen vom Dateinamen ableitet und es sonst Kollisionen gibt.

Das Router-Passwort holt das Skript zur Laufzeit aus OpenBAO, damit es
nirgends auf Platte und in keiner Kommandozeile liegt. Die Policy cert-renew
bekommt dafuer read auf genau secret/data/mikrotik/MT-AZ/apics, nicht auf
secret/mikrotik/*.

Zwei Fehler des ersten Laufs sind behoben: die Namensauslese benutzte einen
gierigen Regex ".*name=", der in der terse-Ausgabe das spaetere
subject-alt-name= traf und "DNS:mt-az.famfi.home" zurueckgab; jetzt wird
feldweise zerlegt. Und /ip service set spricht den Dienst direkt beim Namen
an statt ueber [find ...], das auch den dynamischen Verbindungseintrag
erfasste und eine irritierende Meldung ueber telnet quittierte. Am Ende
prueft das Skript, dass api-ssl wirklich ein gueltiges Zertifikat ausliefert.

Ergebnis: neues Zertifikat gueltig bis 2027-01-04, der Bouncer laeuft und
arbeitet den Rueckstand ab ("Address ... blocked in mikrotik"). Die
IP-Dienste des Routers sind unveraendert, telnet/ftp/www-ssl bleiben
deaktiviert. Timer taeglich mit 21-Tage-Fenster, wie bei den anderen beiden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-06 20:45:38 +02:00
d3c7d2445f Audit-Log einrichten: deklarativ, mit Rotation und haltbarem Pfad
Bis jetzt gab es kein Audit-Device - also keinerlei Aufzeichnung, wer welches
Geheimnis gelesen hat. Bei der Fehlersuche an der Identitaet "claude" am
2026-10-03 haette ein Blick ins Audit-Log die Ursache sofort gezeigt, weil die
beim Login vergebenen Policies darin stehen.

Deklarativ in openbao.hcl statt per `bao audit enable`: OpenBAO 2.x lehnt den
API-Weg ausdruecklich ab ("cannot enable audit device via API; use declarative,
config-based audit device management instead") - anders als Vault. Der Block
wird bei Neustart und bei SIGHUP uebernommen, der Reload lief daher ohne
Ausfall und ohne Entsiegeln.

log_raw bleibt auf dem Standard false: sensible Werte werden gehasht. Im Log
steht nachgeprueft ein HMAC statt des client_token, dazu Operation, Pfad,
display_name, policies und entity_id.

Dazu zwei Dinge, die das betriebssicher machen:
- /openbao/logs war ein ANONYMES Volume aus dem Image, nicht in der compose
  deklariert. Es haette ein `docker compose down -v` nicht ueberlebt. Jetzt ein
  Bind-Mount auf ./logs (uid 100 = openbao im Container). Der Pfad im Container
  bleibt gleich, der audit-Block muss dafuer nicht angepasst werden. Greift beim
  naechsten `docker compose up -d openbao`.
- logrotate: taeglich, 30 Generationen, zusaetzlich maxsize 100M. Das ist
  Pflicht, nicht Komfort - Audit-Devices sind blockierend, bei voller Platte
  weist OpenBAO Anfragen ab. SIGHUP im postrotate statt copytruncate, damit
  keine Zeilen verloren gehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXUoNtUh1qEdZXSzQeSos4
2026-10-03 10:54:58 +02:00
7 changed files with 235 additions and 1 deletions

3
.gitignore vendored
View File

@@ -12,3 +12,6 @@ artifacts/
.claude/
*-credentials.txt
*.bak-*
# Audit-Logs gehoeren nicht ins Repo
logs/

View File

@@ -31,3 +31,39 @@ cluster_addr = "https://openbao:8201"
# NOTE: OpenBAO 2.x removed mlock support. Instead, disable or encrypt swap
# on the host so secrets can't leak to disk. See:
# https://openbao.org/docs/install/#post-installation-hardening
# ---------------------------------------------------------------------------
# Audit-Log, eingerichtet 2026-10-03.
#
# Warum deklarativ und nicht per `bao audit enable`: OpenBAO 2.x lehnt das ueber
# die API ab ("cannot enable audit device via API; use declarative, config-based
# audit device management instead") - anders als Vault. Der Block wird bei
# Neustart UND bei SIGHUP uebernommen, ein Reload genuegt also.
#
# Warum ueberhaupt: ohne Audit-Device gibt es keinerlei Aufzeichnung, wer welches
# Geheimnis gelesen hat. Bei der Fehlersuche am 2026-10-03 (Identitaet "claude"
# ohne Rechte) haette ein Blick in dieses Log die Ursache sofort gezeigt, weil
# die beim Login vergebenen Policies darin stehen.
#
# log_raw bleibt auf dem Standard false: sensible Werte werden gehasht (HMAC),
# nicht im Klartext geschrieben. Das Log zeigt also WER WAS WANN angefasst hat,
# nicht die Geheimnisse selbst.
#
# ACHTUNG, Betriebsrisiko: Audit-Devices sind BLOCKIEREND. Kann das Device nicht
# schreiben (Platte voll, Rechte weg), weist OpenBAO Anfragen ab. Deshalb ist
# logrotate Pflicht, siehe logrotate/openbao-audit im Repo.
#
# ACHTUNG, Haltbarkeit: /openbao/logs ist derzeit ein ANONYMES Docker-Volume aus
# dem Image, nicht in der compose deklariert. Es ueberlebt Neustarts und normale
# Recreates, aber nicht `docker compose down -v`. Die compose-Datei enthaelt
# deshalb jetzt einen Bind-Mount auf ./logs; er greift beim naechsten
# `docker compose up -d openbao`. Der Pfad IM Container bleibt gleich, dieser
# Block muss dafuer also nicht angepasst werden.
audit "file" "audit-file" {
description = "Audit-Log als JSON in /openbao/logs/audit.log"
options {
file_path = "/openbao/logs/audit.log"
mode = "0600"
format = "json"
}
}

View File

@@ -1,6 +1,10 @@
services:
openbao:
image: openbao/openbao:2.6.2
# 2026-10-24: 2.6.2 -> 2.7.1. Die CLI auf dem Host ist seit dem 01.10. 2.7.1,
# der Server hinkte hinterher. Der Unsealer-Sidecar unten bleibt vorerst auf
# 2.6.2: er ist Shamir-versiegelt (1/1) und muesste nach einem Austausch von
# Hand entsiegelt werden - die Transit-API ist versionsuebergreifend stabil.
image: openbao/openbao:2.7.1
container_name: openbao
restart: unless-stopped
# The image entrypoint already runs `bao server -config=/openbao/config`
@@ -18,6 +22,9 @@ services:
volumes:
- ./config:/openbao/config:ro
- openbao-data:/openbao/file
# Audit-Log auf den Host: /openbao/logs war vorher ein anonymes
# Volume aus dem Image und haette ein `down -v` nicht ueberlebt.
- ./logs:/openbao/logs
healthcheck:
# 200 = unsealed+active. Accept sealed (501) and standby (429) too,
# so the container is "healthy" once the API is responding at all.

View File

@@ -1,2 +1,9 @@
# cert-renew — materialized from live OpenBAO (policy-as-code).
path "pki_int/issue/famfi-home" { capabilities = ["update"] }
# Ergaenzt 2026-10-06 fuer renew-mikrotik-cert.sh: das Zertifikat von
# mt-az.famfi.home liegt nicht auf einem Host, sondern muss per SSH in den
# Router importiert werden. Damit das Skript dessen Passwort nicht auf Platte
# oder in einer Kommandozeile braucht, holt es sich das Geheimnis zur Laufzeit.
# Bewusst nur dieser eine Pfad, nicht secret/mikrotik/*.
path "secret/data/mikrotik/MT-AZ/apics" { capabilities = ["read"] }

159
scripts/renew-mikrotik-cert.sh Executable file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# Renew the mt-az.famfi.home API-SSL cert (Kopie von renew-gitea-cert.sh, 2026-10-06)
# from OpenBAO's pki_int engine and install it on the MikroTik.
#
# Warum es dieses Skript gibt: das Zertifikat lief am 2026-09-26 ab und hat damit
# den CrowdSec-MikroTik-Bouncer getoetet - 967 Neustartversuche mit
# "x509: certificate has expired", 10 Tage lang keine Sperren in der
# Router-Firewall. Niemand hat es bemerkt, weil mt-az:8729 in keiner
# Blackbox-Probe steht und es - anders als fuer openbao und gitea - kein
# Erneuerungsskript gab.
#
# Idempotent: erneuert nur, wenn das Zertifikat am Router innerhalb von
# $RENEW_WINDOW_DAYS ablaeuft. Als root laufen (liest das Token).
# sudo /home/lutz/Projects/OpenBAO/scripts/renew-mikrotik-cert.sh [--force]
set -euo pipefail
ADDR="${BAO_ADDR:-http://127.0.0.1:8200}"
TOKEN_FILE="/etc/openbao-cert-renew.token"
ROLE="pki_int/issue/famfi-home"
CN="mt-az.famfi.home"
TTL="2160h" # 90 Tage, wie bei openbao/gitea
RT_HOST="192.168.0.2"
RT_USER="apics" # group=full, address=192.168.0.0/24
RENEW_WINDOW_DAYS="${RENEW_WINDOW_DAYS:-21}"
# Fuer die vollstaendige Kette muss die Zwischen-CA im Zertifikatsspeicher des
# Routers liegen (2026-10-22 als famfi-int-ca-20261022.crt_0 importiert). RouterOS
# verknuepft Blatt und CA beim IMPORT des Blattes; fehlt die CA, liefert api-ssl nur
# das Blatt aus und strenge Clients scheitern mit "unable to get local issuer
# certificate". Wird die CA je entfernt, faellt das unten in der Kettenpruefung auf.
ROOT_CA="${ROOT_CA:-/home/lutz/.config/famfi-ca/famfi-home-root-ca.pem}"
FORCE="${1:-}"
log() { printf '%s [renew-mikrotik-cert] %s\n' "$(date '+%F %T')" "$*"; }
die() { log "ERROR: $*"; exit 1; }
[ -r "$TOKEN_FILE" ] || die "token file $TOKEN_FILE not readable (run as root?)"
TOKEN="$(cat "$TOKEN_FILE")"
# Passwort des Router-Benutzers: zur Laufzeit aus OpenBAO, damit es nirgends
# auf Platte liegt. Die Policy cert-renew braucht dafuer read auf
# secret/data/mikrotik/MT-AZ/apics. Solange das nicht erteilt ist, kann der
# Aufrufer MIKROTIK_PASS setzen (nur fuer manuelle Laeufe gedacht).
SSHPASS="${MIKROTIK_PASS:-}"
if [ -z "$SSHPASS" ]; then
SSHPASS="$(curl -sS --max-time 15 -H "X-Vault-Token: ${TOKEN}" \
"${ADDR}/v1/secret/data/mikrotik/MT-AZ/apics" 2>/dev/null \
| jq -r '.data.data.password // empty')" || true
fi
[ -n "$SSHPASS" ] || die "kein Router-Passwort (Policy cert-renew erweitern oder MIKROTIK_PASS setzen)"
export SSHPASS
rt() { sshpass -e ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no \
-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 "${RT_USER}@${RT_HOST}" "$@"; }
# Restlaufzeit direkt am Router messen - verlaesslicher als eine lokale Kopie,
# denn entscheidend ist, was api-ssl tatsaechlich vorzeigt.
if [ "$FORCE" != "--force" ]; then
end="$(echo | timeout 20 openssl s_client -connect "${RT_HOST}:8729" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || true)"
if [ -n "$end" ]; then
days_left=$(( ( $(date -d "$end" +%s) - $(date +%s) ) / 86400 ))
if [ "$days_left" -gt "$RENEW_WINDOW_DAYS" ]; then
log "cert valid ${days_left}d (> ${RENEW_WINDOW_DAYS}d) - nothing to do"; exit 0
fi
log "cert has ${days_left}d left (<= ${RENEW_WINDOW_DAYS}d) - renewing"
else
log "konnte die Restlaufzeit nicht lesen - erneuere vorsorglich"
fi
fi
resp="$(curl -sS --fail-with-body --max-time 15 -H "X-Vault-Token: ${TOKEN}" \
--data "{\"common_name\":\"${CN}\",\"ttl\":\"${TTL}\"}" "${ADDR}/v1/${ROLE}")" \
|| die "issue request failed"
tmp="$(mktemp -d)"; chmod 700 "$tmp"
cleanup() { rm -rf "$tmp"; }
trap cleanup EXIT
export RESP="$resp"
python3 - "$tmp" <<'PY'
import json, os, sys
d = json.loads(os.environ["RESP"])["data"]
t = sys.argv[1]
open(f"{t}/cert.pem", "w").write(d["certificate"] + "\n")
open(f"{t}/key.pem", "w").write(d["private_key"] + "\n")
PY
chmod 600 "$tmp/key.pem"
cmod="$(openssl x509 -in "$tmp/cert.pem" -noout -modulus | openssl md5)"
kmod="$(openssl rsa -in "$tmp/key.pem" -noout -modulus 2>/dev/null | openssl md5)"
[ "$cmod" = "$kmod" ] || die "cert/key modulus mismatch - refusing to install"
# Datumsstempel im Namen: RouterOS leitet den Zertifikatsnamen vom Dateinamen ab.
# Mit gleichem Namen gaebe es Kollisionen mit dem noch vorhandenen alten Objekt.
STAMP="$(date +%Y%m%d%H%M)"
BASE="mt-az-${STAMP}"
sshpass -e scp -o PreferredAuthentications=password -o PubkeyAuthentication=no \
-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 \
"$tmp/cert.pem" "${RT_USER}@${RT_HOST}:${BASE}.crt" >/dev/null
sshpass -e scp -o PreferredAuthentications=password -o PubkeyAuthentication=no \
-o StrictHostKeyChecking=accept-new -o ConnectTimeout=15 \
"$tmp/key.pem" "${RT_USER}@${RT_HOST}:${BASE}.key" >/dev/null
log "Dateien hochgeladen: ${BASE}.crt/.key"
# Leeres passphrase, damit nichts Geheimes in der Kommandozeile steht - RouterOS
# protokolliert Kommandos mit.
rt "/certificate import file-name=${BASE}.crt passphrase=\"\"" >/dev/null
rt "/certificate import file-name=${BASE}.key passphrase=\"\"" >/dev/null
# Feldweise zerlegen statt mit einem Regex ueber die ganze Zeile: in der
# terse-Ausgabe steht auch subject-alt-name=, und ein gieriges ".*name=" trifft
# genau das. Beim ersten Lauf kam dadurch "DNS:mt-az.famfi.home" heraus.
NEWNAME="$(rt "/certificate print terse where name~\"${BASE}\"" \
| tr ' ' '\n' | grep -m1 '^name=' | cut -d= -f2)"
[ -n "$NEWNAME" ] || die "importiertes Zertifikat nicht gefunden"
log "importiert als ${NEWNAME}"
OLD="$(rt '/ip service print terse where name="api-ssl"' \
| tr ' ' '\n' | grep -m1 '^certificate=' | cut -d= -f2)"
# Direkt beim Namen ansprechen statt ueber [find ...]: letzteres liefert bei
# /ip service auch den dynamischen Verbindungseintrag und quittierte das
# Setzen mit einer irritierenden Meldung ueber telnet.
rt "/ip service set api-ssl certificate=${NEWNAME}"
log "api-ssl nutzt jetzt ${NEWNAME} (vorher ${OLD:-unbekannt})"
# Hochgeladene Dateien entfernen - der private Schluessel hat im Flash nichts zu suchen.
rt "/file remove [find name~\"${BASE}\"]" >/dev/null 2>&1 || true
# Altes Objekt erst nach dem Umschalten loeschen.
if [ -n "${OLD:-}" ] && [ "$OLD" != "$NEWNAME" ] && [ "$OLD" != "none" ]; then
rt "/certificate remove [find name=\"${OLD}\"]" >/dev/null 2>&1 \
&& log "altes Zertifikat ${OLD} entfernt" || log "altes Zertifikat ${OLD} blieb stehen"
fi
sleep 2
got="$(echo | timeout 20 openssl s_client -connect "${RT_HOST}:8729" 2>/dev/null \
| openssl x509 -noout -subject -enddate 2>/dev/null || true)"
log "api-ssl zeigt jetzt vor: $(echo "$got" | tr '\n' ' ')"
# Abschliessend pruefen, dass wirklich ein gueltiges Zertifikat ausgeliefert
# wird - sonst war die Erneuerung wirkungslos und muss auffallen.
end_new="$(echo "$got" | sed -n 's/^notAfter=//p')"
[ -n "$end_new" ] || die "api-ssl liefert kein Zertifikat mehr aus"
left=$(( ( $(date -d "$end_new" +%s) - $(date +%s) ) / 86400 ))
[ "$left" -gt 0 ] || die "ausgeliefertes Zertifikat ist nicht gueltig (${left}d)"
log "OK - noch ${left} Tage gueltig"
# Kette pruefen: gegen die ROOT allein verifizieren. Das gelingt nur, wenn der
# Router die Zwischen-CA mitschickt. Kein die(), denn das Zertifikat selbst ist
# in Ordnung - es ist ein Mangel, kein Ausfall, und ein Abbruch hier wuerde einen
# erfolgreichen Austausch als Fehlschlag melden.
if [ -r "$ROOT_CA" ]; then
if echo | timeout 20 openssl s_client -connect "${RT_HOST}:8729" \
-CAfile "$ROOT_CA" 2>&1 | grep -q "Verify return code: 0"; then
log "Kette vollstaendig (gegen die Root-CA verifiziert)"
else
log "WARNUNG: api-ssl liefert keine vollstaendige Kette - liegt die Zwischen-CA"
log " noch im Speicher? /certificate print where common-name~\"Intermediate\""
fi
else
log "Kettenpruefung uebersprungen: $ROOT_CA nicht lesbar"
fi

View File

@@ -0,0 +1,12 @@
# Erneuert das API-SSL-Zertifikat von mt-az.famfi.home aus der OpenBAO-PKI und
# importiert es per SSH in den Router. Ohne diesen Timer lief das Zertifikat am
# 2026-09-26 unbemerkt ab und hat den CrowdSec-MikroTik-Bouncer 10 Tage lang
# lahmgelegt (967 Neustarts mit "x509: certificate has expired").
[Unit]
Description=Erneuert mt-az.famfi.home aus der OpenBAO-PKI und importiert es in den MikroTik
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/home/lutz/Projects/OpenBAO/scripts/renew-mikrotik-cert.sh

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Taeglicher Check/Renew des API-SSL-Zertifikats fuer mt-az.famfi.home
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target