ops(init): ship logrotate config so /var/log/decnet can't fill the disk

Without rotation, the syslog listener and per-host collector grow
/var/log/decnet/ without bound — a noisy attacker (or an active
probe storm) fills the disk in hours on a small VPS. New
deploy/logrotate.d/decnet caps at 7 daily rotations or 100 MiB,
whichever comes first, and uses copytruncate because the ingester
and forwarder hold the files open via Python and won't reopen on
a rename rotation.

Wire install / remove into `decnet init` and `decnet init --deinit`
alongside the existing tmpfiles.d / polkit handling.
This commit is contained in:
2026-04-27 21:26:13 -04:00
parent 5415e98458
commit 673bc5b819
3 changed files with 90 additions and 0 deletions

View File

@@ -493,6 +493,27 @@ def _install_tmpfiles(
return result
def _install_logrotate(
deploy: Path, logrotate_dir: Path, *, force: bool, dry_run: bool
) -> str:
"""Drop the logrotate config into ``/etc/logrotate.d/decnet``.
The ingester / forwarder hold the log files open via Python, so the
config uses ``copytruncate`` rather than rename+create. Without this
rule, /var/log/decnet/ grows without bound and a single noisy day of
attacker traffic fills the disk on a small VPS. Best-effort: a host
without logrotate installed (rare on systemd distros) still boots
fine — the operator just needs to wire their own rotation.
"""
src = deploy / "logrotate.d" / "decnet"
if not src.is_file():
raise RuntimeError(f"missing logrotate config at {src}")
return _copy_if_changed(
src, logrotate_dir / src.name,
mode=0o644, force=force, dry_run=dry_run,
)
def register(app: typer.Typer) -> None:
@app.command(name="init")
def init_cmd(
@@ -595,6 +616,7 @@ def register(app: typer.Typer) -> None:
systemd_dir = pfx / "etc/systemd/system"
polkit_dir = pfx / "etc/polkit-1/rules.d"
tmpfiles_dir = pfx / "etc/tmpfiles.d"
logrotate_dir = pfx / "etc/logrotate.d"
etc_decnet = pfx / "etc/decnet"
if deinit:
@@ -627,6 +649,13 @@ def register(app: typer.Typer) -> None:
dry_run=dry_run,
),
)
_step(
"remove logrotate config",
lambda: _remove_file(
logrotate_dir / "decnet",
dry_run=dry_run,
),
)
_step(
"systemctl daemon-reload",
lambda: (_run(["systemctl", "daemon-reload"], dry_run=dry_run), "ok")[1],
@@ -775,6 +804,12 @@ def register(app: typer.Typer) -> None:
deploy, tmpfiles_dir, force=force, dry_run=dry_run,
),
)
_step(
"install logrotate config",
lambda: _install_logrotate(
deploy, logrotate_dir, force=force, dry_run=dry_run,
),
)
_step(
"systemctl daemon-reload",
lambda: (_run(["systemctl", "daemon-reload"], dry_run=dry_run), "ok")[1],