Skip to content

lss 2.0 stage 4: backup, monitoring, network, devops profile - #23

Draft
VectoDE wants to merge 42 commits into
feat/lss-3from
feat/lss-4
Draft

VectoDE wants to merge 42 commits into
feat/lss-3from
feat/lss-4

Conversation

@VectoDE

@VectoDE VectoDE commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Stage 4 of lss 2.0 (#6): backup, monitoring, network, the devops profile and guard's backup checks.

Modules

  • Backup: restic (local path, SFTP or S3; password, S3 keys and SSH key in the lss secrets; systemd timer, retention with forget/prune, check after each run) and db-dumps (MariaDB, MySQL, PostgreSQL, MongoDB, Redis and Valkey, dumped before each backup).
  • Monitoring: node-exporter, prometheus (alert rules for down targets, disks, memory, guard findings, failed or old backups and failed restore tests), alertmanager, grafana, loki, alloy, netdata and smartd.
    • Alloy replaces Promtail, which reached its end of life on 2026-03-02.
    • Every UI and API binds to 127.0.0.1, or to the WireGuard address with LSS_<MODULE>_ACCESS=vpn. With LSS_<MODULE>_WEB_ADMIN=true it is published below the web server's admin port.
  • Network: wireguard (server, peers from the settings, keys in the secrets, firewall), dns-resolver (Unbound with DNSSEC on localhost) and ntp-server (the chrony of time serves the WireGuard network or everyone, rate-limited).
  • Profile devops: minimal-secure, Docker and the monitoring stack. A comment marks where Forgejo will join.
  • guard: checks the backup age, runs a weekly restore test and exports its findings to Prometheus through the textfile collector.

Package sources

Source Packages
Distribution Prometheus, Alertmanager and the node exporter on Debian/Ubuntu; restic (EPEL on RHEL); Unbound, WireGuard, smartmontools
Upstream, pinned by sha256 in data/checksums.txt Prometheus, Alertmanager and the node exporter on the RHEL family
Vendor repository, pinned key fingerprint Grafana's (Grafana, Loki, Alloy) and Netdata's

Every service gets a sandbox drop-in.

Tests

  • Unit: tests/unit/{backup,monitoring,network}.bats, plus additions to the guard, web and system tests.
  • Integration: groups backup, backup-alt, monitoring and network; wireguard in the host and VM kernel runs; profile devops on Debian 13 and AlmaLinux 10.

Refs #6

🤖 Generated with Claude Code

VectoDE and others added 30 commits October 14, 2026 16:07
lib/monitoring.sh gives the monitoring modules one way to bind to 127.0.0.1
or the WireGuard address, open ports to the VPN only, publish below the
admin port, register Prometheus targets and Grafana data sources, install
checksum-verified upstream releases and check listening sockets.
fw_vpn_address names the server's WireGuard address, flat apt repositories
need no component, and the web servers can proxy with the prefix stripped.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
wireguard sets up wg0 with keys in the lss secrets and peers from the
settings; dns-resolver runs a validating Unbound on localhost and points
the server at it, restoring the previous resolver on removal; ntp-server
opens the chrony of the time module to the WireGuard network or everyone,
rate-limited. time now writes a confdir line so ntp-server can add its own
file without owning chrony.conf.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…d modules

Debian and Ubuntu get Prometheus, Alertmanager and the node exporter from
the distribution, the RHEL family the upstream releases pinned by sha256.
Grafana, Loki and Alloy come from Grafana's repository, Netdata from its
own, both with pinned key fingerprints. Alloy replaces Promtail, which
reached its end of life on 2026-03-02. Every interface listens on
127.0.0.1 or the WireGuard address and runs with a sandbox drop-in.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
restic backs up to a local path, SFTP or S3 on a systemd timer, applies the
retention, checks the repository and exports its state for Prometheus;
the password, the S3 keys and the SSH key live in the lss secrets.
db-dumps dumps every database engine lss installed before each backup.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
With restic installed, guard reports a last successful backup older than
LSS_GUARD_BACKUP_MAX_AGE hours and runs the restore test once a week. Its
findings go to the node exporter's textfile directory, where an alert
rule picks them up.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
devops is minimal-secure, Docker and the monitoring stack. A comment marks
where Forgejo joins once its module exists. The integration matrix tests
the backup, monitoring and network modules and the devops profile; the
haproxy test port moves off 9100, which the node exporter takes.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Module groups, addresses, settings and the guard's backup checks. The
resource figures follow from the integration runs.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
How a module with a web interface binds, opens its ports, publishes below
the admin port and registers with Prometheus and Grafana.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The finding names the restore-test command, and finding also logs it, so
the check matched its own message.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…ecret

The network tests use the public keys of the wg(8) examples as peers.
.gitleaksignore lists these two findings by fingerprint.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…nar findings

mon_url builds the URL of a module's interface and states once why plain
HTTP is safe there (127.0.0.1 or the encrypted WireGuard tunnel).
Positional parameters go to named locals, and Loki's retention hours are
computed before the heredoc.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…ansion

GF_*__FILE variables are resolved by the entry script of Grafana's
container image only; the packaged grafana-server ignored the variable and
created the admin with the default password, so the login check failed.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
NoNewPrivileges forbids the switch to ifconfig_t when wg-quick runs ip, so
wg0 never came up on AlmaLinux 10 with SELinux enforcing.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…ainers

Debian starts chronyd with '!' as root although the unit says User=_chrony;
the replacement command line lost the prefix and chronyd stopped with
'Not superuser'. The drop-in now takes the vendor line and adds -x, and
also lifts the unit's condition on running outside a container.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Priming the root servers and keys can take longer than one 5 s probe.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…t output

Packages that add a system user (grafana) print 'Not creating home
directory', a line that starts with Not.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
wg reads its settings from a pipe through /dev/fd; in the private /dev of
PrivateDevices SELinux denied that on AlmaLinux 10 (fopen: Permission
denied).

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…switch

Netdata 2 removed the setting that turned its Cloud off; /api/v1/info
reports cloud-enabled as true on every agent. An agent connects only
once claimed, so the verification now requires agent-claimed and
aclk-available to be false, and lss no longer writes the ignored
cloud.conf.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
file_remove kept the checksum, so a drop-in a module stopped writing, or
an admin path the removed web server took along, was reported as missing
on every verify. mon_web_admin forgets the admin path when no web server
is left; the integration test removes prometheus before nginx.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The package's installation script restarts Loki before mod_configure
creates /var/lib/loki; with ReadWritePaths=- the directory stayed
read-only in the sandbox and Loki hit its start limit (seen on the
Debian 13 VM in the devops profile). StateDirectory creates it for the
user loki, and a failed start is reset before lss enables the unit.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…ners

chrony 4.8 on Ubuntu 26.04 stops with 'Could not send notification to
$NOTIFY_SOCKET' inside a container. Where the unit runs chronyd in the
foreground, the container drop-in makes it Type=simple without a
notification socket; the forking unit of the RHEL family stays as is.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
prepare exported AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and
RESTIC_PASSWORD_FILE before the database dumps ran, so the dump helper and
its children (pg_dumpall as postgres) inherited them. The credentials now
go into the environment of each restic process only (a subshell in
run_restic), the dumps run before prepare, and the dump programs start
with an empty environment; pg_dumpall as postgres gets nothing of root's.
A unit test fails if the dump helper sees any AWS_ or RESTIC_ variable.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The pass-through to restic still called it directly and lost the
repository and password once they stopped being exported, so the restic
module's verification failed. It now goes through restic_env like every
other restic call.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…h start

unbound.service of the RHEL family runs unbound-anchor as ExecStartPre and
fails when the root servers are out of reach (Rocky 9 in CI). Unbound
keeps the anchor current itself; DISABLE_UNBOUND_ANCHOR=yes is the unit's
own switch.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The parsed port, allow and confdir lines and chronyd's sockets go to the
log before the verification fails, to find why AlmaLinux 10 does not
serve on port 123.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Taken from the RESOURCES lines of the integration run on all systems:
the largest value, rounded up, and the resident size of each service.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
VectoDE and others added 12 commits October 15, 2026 00:35
… answer

chronyd -p listed port 0 from the main file but none of the lines of
the confdir file on Rocky 9 and AlmaLinux 10; the full output, the confdir
line, the directory and the running command line tell why.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…no DNS

On the CI machines outgoing DNS to the root servers is blocked. In the
profile all job dns-resolver still pointed systemd-resolved at Unbound,
and every later package download failed on name resolution. The server
now switches to Unbound only after the signed root zone resolved;
otherwise it keeps its previous resolvers and lss warns, naming
LSS_DNS_RESOLVER_FORWARD. Verify skips the system check in that case.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The EXIT trap read the local variable of mon_install_stopped. When
pkg_install died, bash on Debian 13 ran the trap with that variable gone
("policy: unbound variable"), so policy-rc.d stayed and forbade every
later service start. The trap now uses the global MON_POLICY_RC. The test
runs the failure path with set -u; Git Bash does not reproduce the unbound
case, CI's bash did.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…not answer

Rocky 9 and AlmaLinux 10 list the confdir line and the file in it, yet
chronyd -p shows none of its lines. The version, the end of the main file
and parse runs of the file and of the confdir alone narrow it down.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
…rnel

A RHEL-family container on a Debian or Ubuntu host runs chronyd under
the host's AppArmor profile for /usr/sbin/chronyd, which allows
/etc/chrony/** and /etc/chrony.* only. chronyd could not open
/etc/chrony.d/lss-ntp-server.conf (Permission denied) and never opened
port 123. The time module and ntp-server now share one rule for the
directory.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The encryption controller can report later than the API on a busy host;
the profile run on AlmaLinux 10 failed now and then right after start.
The check now waits up to 60 seconds and logs the status (key names and
hash state, no key material) when it still fails.

Refs #6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>

# Conflicts:
#	.github/workflows/integration.yml
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant