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:
@@ -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],
|
||||
|
||||
28
deploy/logrotate.d/decnet
Normal file
28
deploy/logrotate.d/decnet
Normal file
@@ -0,0 +1,28 @@
|
||||
# /etc/logrotate.d/decnet — installed by `decnet init`.
|
||||
#
|
||||
# Without this, /var/log/decnet/ grows unbounded — the syslog listener writes
|
||||
# every forwarded worker line, the collector tails every container's stdout,
|
||||
# and a noisy attacker (or an active probe storm) can fill the disk in hours.
|
||||
# Bound to 7 daily rotations + size cap so a single bad day doesn't run away.
|
||||
#
|
||||
# Files we rotate:
|
||||
# - decnet.log: master ingest sink (DECNET_INGEST_LOG_FILE).
|
||||
# - agent.log: per-worker collector sink (DECNET_AGENT_LOG_FILE).
|
||||
# - *.log: any other component sink under /var/log/decnet/.
|
||||
#
|
||||
# `copytruncate` is required: the ingester / forwarder hold the file open via
|
||||
# Python and would otherwise keep writing to the deleted inode after rotation.
|
||||
# `notifempty` avoids spurious .1 files on quiet hosts.
|
||||
|
||||
/var/log/decnet/*.log {
|
||||
daily
|
||||
rotate 7
|
||||
maxsize 100M
|
||||
copytruncate
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
su decnet decnet
|
||||
create 0640 decnet decnet
|
||||
}
|
||||
@@ -81,6 +81,10 @@ def _seed_deploy(monkeypatch: Any, tmp_path: Path) -> Path:
|
||||
'// rule for {{ group }}\n'
|
||||
)
|
||||
(deploy / "tmpfiles.d" / "decnet.conf").write_text("d /run/decnet\n")
|
||||
(deploy / "logrotate.d").mkdir()
|
||||
(deploy / "logrotate.d" / "decnet").write_text(
|
||||
"/var/log/decnet/*.log {\n daily\n rotate 7\n copytruncate\n}\n"
|
||||
)
|
||||
monkeypatch.setattr(_init, "_deploy_root", lambda: deploy)
|
||||
return deploy
|
||||
|
||||
@@ -168,6 +172,25 @@ def test_unit_files_are_installed_then_idempotent(
|
||||
assert "unit files up to date" in r2.output
|
||||
|
||||
|
||||
def test_init_installs_logrotate_config(
|
||||
monkeypatch: Any, tmp_path: Path, subprocess_calls: List[List[str]],
|
||||
no_missing_tools: None, present_user_and_group: None,
|
||||
) -> None:
|
||||
"""`decnet init` must drop /etc/logrotate.d/decnet so /var/log/decnet/
|
||||
can't fill the disk on a noisy honeypot day."""
|
||||
_seed_deploy(monkeypatch, tmp_path)
|
||||
prefix = tmp_path / "root"
|
||||
r = runner.invoke(app, ["init", "--no-start", "--prefix", str(prefix)])
|
||||
assert r.exit_code == 0, r.output
|
||||
cfg = prefix / "etc/logrotate.d/decnet"
|
||||
assert cfg.is_file(), "logrotate config should be installed"
|
||||
body = cfg.read_text()
|
||||
assert "copytruncate" in body, (
|
||||
"must use copytruncate; ingester holds the file open and won't "
|
||||
"reopen on a rename rotation"
|
||||
)
|
||||
|
||||
|
||||
def test_init_writes_decnet_ini_not_config_ini(
|
||||
monkeypatch: Any, tmp_path: Path, subprocess_calls: List[List[str]],
|
||||
no_missing_tools: None, missing_user_and_group: None,
|
||||
@@ -354,6 +377,9 @@ def _seed_installed_state(prefix: Path) -> None:
|
||||
tmpfiles = prefix / "etc/tmpfiles.d"
|
||||
tmpfiles.mkdir(parents=True)
|
||||
(tmpfiles / "decnet.conf").write_text("d /run/decnet\n")
|
||||
logrotate = prefix / "etc/logrotate.d"
|
||||
logrotate.mkdir(parents=True)
|
||||
(logrotate / "decnet").write_text("/var/log/decnet/*.log {}\n")
|
||||
etc_decnet = prefix / "etc/decnet"
|
||||
etc_decnet.mkdir(parents=True)
|
||||
(etc_decnet / "decnet.ini").write_text("[decnet]\n")
|
||||
@@ -382,6 +408,7 @@ def test_deinit_removes_units_polkit_tmpfiles_and_preserves_data(
|
||||
assert not (prefix / "etc/systemd/system/decnet.target").exists()
|
||||
assert not (prefix / "etc/polkit-1/rules.d/50-decnet-workers.rules").exists()
|
||||
assert not (prefix / "etc/tmpfiles.d/decnet.conf").exists()
|
||||
assert not (prefix / "etc/logrotate.d/decnet").exists()
|
||||
assert not (prefix / "etc/decnet").exists()
|
||||
assert not (prefix / "opt/decnet").exists()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user