Commit Graph
143 Commits
Author SHA1 Message Date
Clément Désiles 5efc0b0d6e fix(nginx): make Let's Encrypt opt-in per service
certbot cannot issue for internal hostnames (*.nas.local), which made
nginx-enabled services with internal names fail on deploy.

Add a <service>_nginx_letsencrypt flag (default false) and computed
<service>_nginx_ssl_certificate/_key paths: when off the vhost uses the
shared self-signed cert (nginx_default_ssl_cert), when on it uses the
Let's Encrypt lineage. Gate the certbot include on the flag and apply the
pattern to all service roles (deluge, fdroid, gitea, grafana, immich,
metabase, ntfy, prowlarr, radarr, seerr, static_web, uptime_kuma).
2026-09-21 10:04:47 +02:00
Clément Désiles 61621715e0 fix(ansible): reference facts via ansible_facts to silence deprecation
INJECT_FACTS_AS_VARS auto-injection of top-level facts is deprecated and
removed in ansible-core 2.24. Use ansible_facts['architecture'] and
ansible_facts['date_time']['epoch'] instead.
2026-09-21 09:59:03 +02:00
Clément Désiles a73fb2aa29 feat(arr): add prowlarr, radarr and seerr roles 2026-09-18 18:53:43 +02:00
Clément Désiles 6ba208a455 docs(zfs): link pool capacity metric to Grafana alerts 2026-09-18 18:35:04 +02:00
Clément Désiles be73f3adfa feat(grafana): provision disk alerts to ntfy
Provision Grafana Unified Alerting as code: ntfy webhook contact point
(?template=grafana), notification policy tree, and cumulative disk tiers
for HDD temperature (48-60C, <20C) plus ZFS/filesystem capacity. Lower the
smartctl exporter interval to 30m for alert responsiveness.
2026-09-18 18:32:02 +02:00
Clément Désiles c127336321 feat(zfs): add capacity metrics and Grafana dashboard
Deploy an hourly systemd timer that writes pool and dataset capacity to
node_exporter's textfile collector, since --collector.zfs exposes only
ARC/I-O/state. Add a provisioned 'ZFS - Pools & Datasets' dashboard with
per-pool totals, per-dataset used/quota and daily growth panels.
2026-09-18 15:25:23 +02:00
Clément Désiles 501671770f add deluge role 2026-09-18 15:24:18 +02:00
Clément Désiles 30480f61d5 generalize zsh role to support darwin 2026-09-18 11:32:06 +02:00
Clément Désiles fd8c64aed1 add darwin support to install homebrew deps 2026-09-15 17:50:09 +02:00
Clément Désiles f89a919022 add darwin support 2026-09-15 17:30:20 +02:00
Clément Désiles 9fc28377d7 add wg prom exporter 2026-09-11 21:32:27 +02:00
Clément Désiles 20cbf01040 enhance dashboarding 2026-09-11 21:32:00 +02:00
Clément Désiles 9440d78cd9 add exa alias 2026-09-11 14:40:51 +02:00
Clément Désiles 281245ee32 Merge branch 'main' of gitlab.com-perso:cdesiles/ansible-playbooks 2026-09-10 21:26:00 +02:00
Clément Désiles ea93eb8c59 fix(sshd): make authorized_keys fallback readable by the session user
The fallback was created via cp as root:root 0600 and relied on a separate
chown task to fix ownership. When that chown did not take, sshd could not
read /etc/ssh/authorized_keys/<user> in the unprivileged session, so pubkey
auth failed whenever the encrypted home was locked.

Replace the cp + chown pair with a single idempotent copy that keeps the
file root-owned but world-readable (0644). It contains only public keys, so
0644 is safe and passes StrictModes.
2026-09-10 21:21:06 +02:00
Clément Désiles 0ec6224941 feat(backup): per-job schedules, stale-lock recovery, service manifests
- Add per-job `schedule` (systemd OnCalendar) overrides and document the
  global `backup_schedule` / `backup_randomized_delay` knobs.
- Run `restic unlock` before backup/forget/check so a run killed mid-flight
  no longer wedges every later run on a stale exclusive lock.
- Publish backup manifests for fdroid, immich and unifi, so the inventory
  decides only where (repositories, retention, schedule).
- Document that `read_data_subset` re-reads on every job targeting the
  repository, so a large repository wants a small fraction.
2026-09-10 16:47:33 +02:00
Clément Désiles f6c4d36bbe feat(monitoring): add backup and systemd metrics, alerts, and dashboards
- backup role emits last-success/run metrics into node_exporter's textfile collector
- node_exporter enables systemd and textfile collectors
- prometheus deploys BackupStale/BackupTimerSilent/SystemdUnitFailed alert rules
- grafana gains Backups - Restic and Systemd dashboards
- fix prometheus.yml params rendering (quote values so {job=...} stays a string)
2026-09-09 12:07:50 +02:00
Clément Désiles 8756eb1183 feat(monitoring): add smartctl, fail2ban, wireguard exporters and dashboards
- smartctl_exporter (disks), fail2ban exporter, wireguard exporter
- grafana dashboards for each exporter
- prometheus: firewall source rules, honor_labels/params scrape config
- node_exporter: zfs collector
- wireguard: nftables dep, resolvconf wrapper, reload fix
- remove superseded monitoring role
2026-09-08 23:03:34 +02:00
Clément Désiles c29042fe1d feat(tooling): add nmap 2026-09-08 22:52:00 +02:00
Clément Désiles 56fe5304c5 feat(unbound): enable prefetch, prefetch-key, and deny-any 2026-09-08 22:49:50 +02:00
Clément Désiles e4d0812a69 fix(grafana): NUT dashboard instance variable ignores stale series
The variable used label_values() which lists all instances across full
retention, alphabetically sorting and auto-selecting the dead
127.0.0.1:9199 (relabeled to 'eaton' ~31h ago). Its stale series has no
samples in the visible window so every panel rendered empty. Replace with
query_result(count by (instance) (...)) which only surfaces live targets.
2026-09-06 23:51:22 +02:00
Clément Désiles 65fdb4face fix(monitoring): exporter auth and reload fixes
- postgres: use lib/pq conninfo DATA_SOURCE_NAME; the postgresql:// URL
  form broke on special chars in the password (net/url: invalid userinfo)
- nginx: flush handlers after deploying stub_status so nginx reloads
  before the exporter starts scraping
- valkey: drop invalid +arcount command from the exporter ACL set
  (Valkey aborted startup parsing the ACL file)
2026-09-06 23:47:13 +02:00
Clément Désiles ed5d0c68f1 feat(unbound): add prometheus exporter, dashboard, fix socket buffer sysctl 2026-09-06 23:44:05 +02:00
Clément Désiles 304a3fae12 feat(monitoring): add nginx, postgres, valkey dashboards 2026-09-06 23:43:50 +02:00
Clément Désiles aa78e238aa feat(monitoring): add nginx, postgres, valkey prometheus exporters
- nginx: nginx-prometheus-exporter gated by nginx_exporter_enabled,
  deploys loopback-only stub_status (8090), exporter on 9113
- postgres: postgres_exporter gated by postgres_exporter_enabled,
  creates postgres_exporter role with pg_monitor, exporter on 9187
- valkey: redis_exporter gated by valkey_exporter_enabled, adds
  read-only ACL user to users.acl, exporter on 9121

Each exporter follows the nut pattern (tasks/exporter.yml included from
main.yml, OS vars, 0600 secret env files, password asserts).
2026-09-06 09:37:36 +02:00
Clément Désiles 6f1f7f88f0 fix(metabase): bind host port 3400 instead of default 3000 2026-09-05 17:24:17 +02:00
Clément Désiles 5218fa2714 refactor(nut): merge prometheus exporter into nut role
- exporter gated by nut_prometheus_exporter_enabled
- env-file config (correct NUT_EXPORTER_* vars + --web.listen-address)
- remove standalone nut_exporter role
2026-09-05 17:24:12 +02:00
Clément Désiles 12b81919c3 feat(monitoring): add grafana, prometheus, node_exporter roles
- grafana: postgres backend, provisioning (prometheus datasource + node/NUT dashboards)
- prometheus: native service, scrape config via host_vars, localhost bind
- node_exporter: host metrics exporter (localhost:9100)
2026-09-05 17:24:08 +02:00
Clément Désiles b94743567f feat(sys_autoupdate): notify on pending kernel reboot
Detect drift between the running kernel and the newest installed one
(Arch: /usr/lib/modules vs uname -r; Debian: /var/run/reboot-required)
and push a weekly ntfy reminder until the host is rebooted.

Also bundles the in-flight ignore-packages feature and hostname
refactor in the autoupdate script.
2026-09-05 16:59:49 +02:00
Clément Désiles 24502f543f fix(fail2ban): pass blocktype via banaction to override the ufw action default
jail-level blocktype is ignored (action [Init] wins); it must be
passed as banaction = ufw[blocktype=deny].
2026-09-05 16:57:45 +02:00
Clément Désiles 6355a9aae5 fix(fail2ban): ban via ufw deny (DROP) instead of reject
REJECT needs the nft_reject kernel extension, which is unavailable on
this Arch kernel (iptables-nft reports 'Extension REJECT revision 0
not supported'), so every ban silently failed. DROP works everywhere
and is quieter toward attackers.
2026-09-05 16:45:12 +02:00
Clément Désiles 40cc0efcf8 fix(fail2ban): use ProtectSystem=full so ufw can create its lock file
strict makes /run read-only, which blocks ufw's /run/ufw.lock and
cannot be whitelisted via ReadWritePaths (the file does not exist).
full still protects /usr and /etc while leaving /run and /var writable,
so only /etc/ufw needs an explicit write path.
2026-09-05 16:28:55 +02:00
Clément Désiles 120615c38f fix(fail2ban): start under ProtectSystem=strict hardening
- log to journald via fail2ban.local (logtarget is a fail2ban.conf option,
  not jail.local) so the server can start without /var/log write access
- create the socket dir via RuntimeDirectory=fail2ban instead of a
  non-creating ReadWritePaths entry
- add ufw write paths so bans persist under the read-only namespace
- manage override.conf unconditionally and restart on change
2026-09-05 16:21:49 +02:00
Clément Désiles ceca06ff2e feat(backup): add restic backup role with encrypted incremental snapshots
Restic-backed backups using a many-to-many repository/job model.

- repository = destination: restic repository URL verbatim (s3, local,
  removable device), backend credentials via env passthrough, retention
  policy, and an optional per-repository ntfy notify channel
- job = source set: paths, sqlite and postgres dumps (consistent .backup /
  pg_dump instead of torn live-file copies), pushed to one or more repos
- systemd timer per job, per-repo + per-job flock serialisation, restic
  check with read_data_subset for rotating integrity verification

Service roles declare what to back up (ntfy_backup_*, uptime_kuma_backup_*);
the inventory declares where.
2026-09-01 22:20:27 +02:00
Clément Désiles 8a3c323059 fix(wireguard): reload tunnel via syncconf on config change
Config template changes were never applied to the running interface,
leaving a stale peer list after redeploy. Notify a syncconf reload so
peer edits apply without dropping active connections.
2026-08-27 10:14:23 +02:00
Clément Désiles 842374616b fix: add unzip to tooling role 2026-08-26 00:16:33 +02:00
Clément Désiles fb01dfbf67 doc: minor alias to help me switch to podman 2026-08-26 00:16:09 +02:00
Clément Désiles 4c4cb16f37 fix(wireguard): allow ufw forwarding for server_mode tunnels
ufw FORWARD policy drop overrode the nft/iptables forward rules,
blocking all VPN traffic through the tunnel. Add routed rules in
both directions between the tunnel and primary interface.
2026-08-25 23:47:34 +02:00
Clément Désiles 642a0f8ea4 fix(sshd): move login banner to zsh .zprofile
sshd Banner fires on every connection (scp, sftp, non-interactive), the wrong layer for a welcome banner. Move to .zprofile, interactive login shells only.
2026-08-25 22:58:03 +02:00
Clément Désiles 43b510e77f fix(podman): restore newuidmap/newgidmap file caps for rootless namespaces
pacman strips file capabilities, leaving rootless podman unable to set up
user namespaces (rc 125). Re-apply cap_setuid/cap_setgid on Arch.
2026-08-23 22:03:31 +02:00
Clément Désiles 174a96eefc security: always restore sudoers after makepkg, drop dead paru block
The archlinux and zfs roles temporarily write `%wheel ALL=(ALL) NOPASSWD: ALL`
to /etc/sudoers so makepkg/paru can run unattended, then restore it in a
following task. Since the restore was a plain sibling task, any failure in
between aborted the play and left the host with passwordless root for the
whole wheel group. Move the restore into `always:` so it runs on failure too,
and narrow the window in yay.yml to just the makepkg call (dependency install,
directory setup and git clone no longer run inside it).

This is not hypothetical: a provisioning run of omega failed on paru's git
clone and left the sudoers hack in place.

The clone failed because the deprecated compile-from-source paru block, which
was commented out in tree, had been re-enabled. It fed paru_git_repo into the
git module, but that variable now holds a GitHub owner/repo slug for the
releases API rather than a clone URL. Remove the block outright, along with
the orphaned paru_src_path default; paru is installed from the release binary.

Also fix the ansible-lint violations this surfaced so roles/archlinux passes
the production profile: explicit `become` next to `become_user`, a pinned
branch on the yay clone, and `changed_when` on the command/shell tasks.
2026-08-18 21:52:49 +02:00
Clément Désiles 1970564c8f refactor: drop shared-service meta dependencies from service roles
podman, postgres, valkey, and nginx are shared services deployed once
per host. Declaring them in meta/main.yml re-ran them on every service
deploy, adding significant time to targeted runs. Playbooks now list
shared services first (tagged 'infra') and every role entry carries a
group tag plus its own name, so '--tags <role>' deploys just that role.

Roles still fail fast when a shared service is missing (password
asserts, postgresql tasks). Prerequisites are documented in role
READMEs. CLAUDE.md updated to reflect the new convention.
2026-07-04 00:15:50 +02:00
Clément Désiles 7972fe09ad security: deploy pod manifests containing credentials with mode 0600
gitea.yaml, immich.yaml, and metabase.yaml embed database passwords in
env vars; per repo policy, rendered files containing secrets must not
be world-readable.
2026-07-04 00:06:29 +02:00
Clément Désiles 95a743ddeb fix(roles): detect new image on podman pull and restart services
Same bug as immich (556fae8): 'Writing manifest' goes to stderr, so
changed_when never matched and the restart handlers never fired after
image upgrades. Compare local image IDs before/after pull instead, and
notify the restart handler only on a genuinely new image.

Applies to fdroid, gitea, metabase, ntfy, unifi, uptime-kuma.
2026-07-04 00:06:13 +02:00
Clément Désiles 556fae86d9 fix(immich): detect new image on pull and restart service
'Writing manifest' is printed by podman pull on stderr (not stdout), so
changed_when never matched, the Restart Immich handler never fired, and
pods kept running the old image after upgrades. Matching on stderr would
be wrong too: podman prints that line even when the image is unchanged.

Compare the local image ID before/after pull instead, and notify the
restart handler only when the tag resolves to a new image.
2026-07-04 00:03:34 +02:00
Clément Désiles f48d68da9d doc: add readme to zsh 2026-07-03 00:11:52 +02:00
Clément Désiles 75d6aae668 Add UniFi Network Controller role 2026-07-03 00:09:02 +02:00
Clément DésilesandClaude Opus 4.8 501f4e1f87 feat(nut): report outage duration and battery % in ntfy
ONBATT records the epoch start time and appends current battery
charge; ONLINE appends battery charge and the total outage duration
(HH:mm:ss) computed from the recorded start, then clears the state.
State lives in /run so it self-clears on reboot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 23:50:48 +02:00
Clément Désiles 21f47196cf feat(net_config): support IPForward and IPMasquerade in network config 2026-06-19 23:55:02 +02:00
Clément Désiles 4c57d28b4d Add new fdroid role to host custom apks 2026-06-19 23:50:56 +02:00