Skip to content

lss 2.0, stage 1: core, base and security modules - #2

Open
VectoDE wants to merge 38 commits into
mainfrom
feat/lss-2-core
Open

VectoDE wants to merge 38 commits into
mainfrom
feat/lss-2-core

Conversation

@VectoDE

@VectoDE VectoDE commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

What changes and why

This is stage 1 of lss 2.0, which replaces the single 1.x install script with a modular tool. A server can be set up from a profile in one run or one module at a time, and modules can be added, reconfigured, verified or removed later. The 1.x script stays in the repository until 2.0.0 is released.

  • Core: the lss command and lib/. It handles settings (parsed, never sourced), dependency order, a separate process per step, managed files with backup and restore, pinned repository keys and checksums, secrets, firewall rules per module, state and the interactive menu.
  • Modules: six in base (system, time, swap, admin-user, tools, logging) and sixteen in security (ssh-hardening, ssh-2fa, firewall, fail2ban, crowdsec, kernel-hardening, mac, auditd, auto-updates, password-policy, aide, rkhunter, clamav, lynis, usbguard, guard). The minimal-secure profile installs 18 of them. ssh-2fa, crowdsec (instead of fail2ban), clamav and usbguard are opt-in.
  • Tests: 112 bats unit tests with 96 % line coverage of lib/, enforced at 90 %. Integration tests install, verify, reinstall and remove every module on Debian 12 and 13, Ubuntu 22.04 and 24.04, Rocky Linux 9 and AlmaLinux 10. A virtual machine runs the kernel modules and the full profile, followed by a Lynis audit that must reach an index of 70 (it measures 75).
  • Repository:
    • CI, integration, meta (actionlint, zizmor, REUSE), security scans (dependency review, gitleaks, ShellCheck code scanning), DCO and Scorecard workflows.
    • Dependabot with a cooldown, and SonarQube Cloud configuration.
    • Governance, security policy, contributing guide and issue forms.
    • Documentation: architecture, hardening, troubleshooting, module guide and the assurance case.
  • OpenSSF Best Practices: project 15442 is at passing; silver is at 93 % and gold at 57 %. The open silver items (signed releases, SBOM, upgrade path from 1.x) come with the release workflow in stage 7 (Stage 7: Ansible collection, bootstrap, lss update and release 2.0.0 #9).

Closes #11

Checklist

  • Unit tests and scripts/dev/lint.sh pass
  • New behaviour has tests; a new module is in the integration matrix
  • Every step stays idempotent and restores its files when it fails
  • New settings are in MOD_OPTIONS and the README; CHANGELOG.md has an entry under "Unreleased"
  • Secrets stay out of logs, command lines and notifications

VectoDE and others added 15 commits October 13, 2026 09:00
The design (docs/superpowers/specs) and the plan for the first stage are
included. The core parses configuration without sourcing it, writes
managed files with backup and restore, keeps secrets in 0600 files and
masks them in logs, and runs every module step in its own subshell so a
failing command always stops the step.

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

A rule that could not be removed now stays in the registry, and firewalld
rules are changed with firewall-offline-cmd while the service is stopped,
so a port can no longer reopen unnoticed when firewalld starts again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
bash ignores set -e inside functions called from a condition, and
subshells, also asynchronous ones, inherit that. A module step now runs
in its own process (lib/step.sh) with the settings serialised into a
private context file, so a failing command always stops the step. Also:
secret names are validated before any command substitution, and the
package index refresh is remembered per run across those processes.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Cover the remaining branches of log, os, config, files, template, secrets,
state, pkg, service, firewall, fetch and module, and add tests for repo.sh
(gpg stub, file_write recorded instead of writing below /etc) and ui.sh
(whiptail stub; the menu itself runs under script for a terminal).

New stubs: gpg, journalctl, whiptail. The systemctl stub now exits with
$STUB_EXIT_systemctl for commands other than is-active/is-enabled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Modules system, time, swap, admin-user, tools and logging, plus ssh-hardening (keys only, algorithms
filtered to what the installed OpenSSH knows, lockout check) and firewall (ufw/firewalld, SSH first,
rollback timer over SSH). Library helpers opt/opt_bool/opt_int, file_ensure_lines, svc_unit and
os_in_container. tests/integration/run.sh installs, verifies, reinstalls (must be unchanged) and removes
each module in a systemd container per distribution or on the runner VM for kernel modules.

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

Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
docker build needs -f for Containerfile. fw_ssh_port and the fail2ban jail no longer fail under pipefail
when sshd is missing or awk stops reading early. Unit tests for opt/opt_bool/opt_int, file_ensure_lines,
os_in_container, svc_unit and the fw_ssh_port fallback. Adds the fail2ban module.

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

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

Modules fail2ban, crowdsec (packagecloud repository with pinned key), kernel-hardening, mac, auditd
(rules filtered per architecture), auto-updates (reboot window), aide, rkhunter, clamav (EICAR check),
lynis (lss audit, index history), usbguard, password-policy (pwquality, faillock, login.defs),
ssh-2fa (TOTP with nullok transition) and guard (daily verify and repair, alerts by webhook or mail).
repo.sh: per-release suite override and RPM_GPGCHECK; a repository must check at least one signature.

Tests: docker exec -i so settings reach the container, Debian/Ubuntu images without policy-rc.d,
getent lookups no longer fail under pipefail, matrix covers every module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Security: ssh-2fa creates and reads the TOTP secret as the user and refuses symlinks; file_ensure_lines
and admin-user refuse symlinked targets, chown no longer follows links (a user could otherwise make root
chown /etc/shadow).

Integration fixes: unattended-upgrades Origins-Pattern uses key=value entries; time sets only the time
zone inside containers (the host owns the clock) and is tested with chrony on the VM; ssh-hardening
installs the ssh client, creates /run/sshd before sshd -t and computes algorithm lists outside the
process substitution; admin-user checks the sudoers group rule instead of sudo -l (PAM in containers);
auditd matches auditctl -l output; rkhunter without unhide on the RHEL family.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
fail2ban on Rocky and AlmaLinux logs to the journal, so the recidive jail found no
log file and the configuration test failed. lss now pins fail2ban's own log to
/var/log/fail2ban.log and creates it before the test.

freshclam failed silently on AlmaLinux 10 and the module only noticed after seven
minutes. The first download now runs in the foreground with up to three tries, so
its reason shows in the log; the daemon is stopped meanwhile so it cannot hold the
database lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Workflows for Scorecard, secret scanning and ShellCheck code scanning, sign-off
checks, actionlint, zizmor, REUSE and Dependabot auto-merge, all with pinned
actions, harden-runner and concurrency groups. Dependabot keeps the actions and
the digest-pinned test images current.

Security policy, contributing guide, governance, support, code of conduct, issue
and pull request templates, and a changelog with the 1.x history.

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

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

VectoDE and others added 4 commits October 13, 2026 10:41
freshclam runs as clamupdate on RHEL and could not open /var/log/freshclam.log,
so the daemon exited at once. It now logs to the journal only. The foreground
download from the last commit is reverted: it broke Debian and Ubuntu, where the
daemon already worked.

CrowdSec has no packages for EL10 yet; RPM_BASEURL_<major> lets a repository use
the EL9 packages there, which are static binaries. The firewall bouncer could
start before the local API listened, and dnf may install it before cscli exists,
leaving it without a key. lss now waits for the API and registers the bouncer
itself when the key is missing, passing the key through the environment so it
never appears in the process list.

svc_enable shows the unit's journal when a start fails, so CI logs name the
cause. The integration workflow splits modules with read -a instead of an
unquoted variable (actionlint), and Dependabot waits seven days before
proposing a new release (zizmor).

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

The README describes the modular lss instead of the 1.x script. The hardening
guide maps the security modules to the CIS Benchmark areas and lists every
deliberate deviation with its reason and setting; the troubleshooting guide
covers lockouts and the common messages; the module guide is for contributors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
authselect has no with-pwquality feature: every profile includes pam_pwquality.
The profile for local accounts is "minimal" up to authselect 1.4 (EL9) and
"local" from 1.5 (EL10); lss now picks the one that exists and only adds the
faillock feature.

A reload restarts CrowdSec's local API for a few seconds, so a verify right after
the install could fail. The check now waits up to 30 seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
A misspelt key in lss.conf was silently ignored, so the module ran with its
default. lss now warns about every setting that no module reads. Two keys in the
integration settings had exactly that problem.

The integration harness gains a profile mode: install a whole profile, verify,
install again without a change, then run the Lynis audit with a minimum index.
CI runs it for minimal-secure on a virtual machine. The minimum starts at 0 to
record the index; it is raised once the first runs show what the profile reaches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Comment thread tests/unit/module.bats Fixed
VectoDE and others added 9 commits October 13, 2026 10:59
module_meta printed metadata without a final newline, so every while-read loop
over MOD_OPTIONS skipped the last option: lss info never listed it, and the new
unknown-settings check reported valid keys such as LSS_SYSTEM_UPGRADE. It now
ends with a newline, which command substitutions drop as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
visudo -c checks every sudoers file, so a problem in a file of the cloud image or
CI runner made admin-user fail although its own file was valid and sudo worked.
The module now fails only when /etc/sudoers.d/00-lss is invalid and reports
problems in other files as a warning with visudo's output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
aide --init hashes every file. On the CI runner VM, with tens of GB of
toolchains, the profile run sat at "Running aide --init" until it was cancelled;
servers with large data directories have the same problem. LSS_AIDE_EXCLUDE adds
directories to leave out, the database is built at low CPU and I/O priority, and
lss reports how long it took. The integration settings exclude the runner's
toolchain directories.

On RHEL the excludes were appended to /etc/aide.conf only once, so a changed
setting never took effect. The block after the lss marker is now rewritten on
every run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The verify step hid the output, so a failure on the CI runner VM gave no reason.
It now prints the last lines of a debug run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
As soon as auto-updates enables unattended-upgrades it starts and holds the dpkg
lock, so the next module's package install failed (lynis in the profile run), and
so could any install on a fresh server where apt-daily runs at boot. apt now
waits up to ten minutes for the lock, and the auto-updates check waits while
another unattended-upgrade holds its own lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
apt-daily-upgrade.timer has Persistent=true. On a server where it was off but
had run before (cloud and CI images), enabling it made systemd catch up at once:
unattended-upgrades upgraded every package and held the dpkg lock for many
minutes while the next modules waited. Before a stopped timer is started, lss now
records "ran now" in its stamp file, so the first automatic upgrade happens at
the next regular time. The system module already installs all updates during
the setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
A reload sends SIGHUP and CrowdSec restarts its local API in the background, so
the bouncer and the next lss verify could hit it while it was down. lss now
restarts the service, which systemd waits for, and the check uses the result of
its wait loop instead of asking a second time. On failure it shows cscli's
answer and the journal.

The profile run on a VM reached a Lynis hardening index of 75; CI now fails
below 70.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
unattended-upgrade --dry-run needs the dpkg lock. When a real update ran
on the VM, lss verify waited silently for up to ten minutes. apt-config
parses the same files without any lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Sources and tests are now separate, and three rules are accepted with
their reasons. The integration images run as root on purpose; Bash
predicates need their last command's status, so a trailing return 0
would break them.

Also merged nested ifs, moved the faillock arithmetic out of the
heredoc, sorted the image package lists and added log group helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
VectoDE and others added 10 commits October 13, 2026 15:01
docs/assurance.md documents the threat model, the trust boundaries,
the secure design principles and the weaknesses lss counters, with
the tests that back each claim. The OpenSSF Best Practices entry links
to it, and the README shows its badge.

Governance no longer says that version tags are already restricted to
the release workflow. That restriction comes with 2.0.0. Pull requests
now get a dependency review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
apt and its hooks (man-db, needrestart, package postinst scripts)
filled the console with lines that say nothing about lss. log_command
now writes this output to the run log. The terminal shows it only with
--verbose, or the last 40 lines when the command fails. Package
installs, removals and upgrades and the AIDE database build use it.
The two AIDE hints are now debug messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
Ubuntu 26.04 (resolute) has been the current LTS release since April
2026. lss now accepts it, and CI runs every module group in a 26.04
container. CrowdSec has no resolute suite yet, so the noble packages
are used, as with Debian 13. The LTS check now accepts any NN.04
release among the supported majors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The README will state what each profile and module needs. These
figures come from the test runs instead of guesses: each install
prints its duration, the memory it took from MemAvailable, the disk
space it used and the largest processes.

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

MemAvailable after an install says little: ClamAV loads its whole signature
database only while it scans, so the figure the README needs is the peak.
A sampler reads MemAvailable twice a second from the install through the
verify step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The figures come from the RESOURCES lines of the integration tests on all
seven systems: the largest value per module, rounded up. ClamAV peaks at
1.6 GB while it scans, not 1.2 GB as the profile and the hardening guide
said.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
chronyd resolves the server names in the background. On a fast start the
verify step ran before the first source was listed and failed although
chronyd was configured correctly (seen once on the kernel VM job).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
The ignores for shelldre:S7682 and S7679 in .sonarcloud.properties never
took effect (299 open issues on the pull request), and the 1.x script on
main carries 58 more. All are fixed in the code now:

- S7682: every function ends its body with a return. A bare return keeps
  the status of the last command, so predicates behave as before.
- S7679: positional parameters are copied into named locals; secret_load
  uses prefixed names because printf -v would assign a local that has the
  caller's variable name.
- 1.x script: [[ instead of [ (S7688), a default branch in every case
  (S131), lower-case locals (S7684).

The docker:S6471 ignore for the test images works and stays.

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

shfmt expands one-line functions with two statements, so they are written
on several lines, and stubs return 0 or 1 directly. Touching the 1.x script
made SonarQube Cloud report its downloads (shell:S6506): curl now accepts
only HTTPS with TLS 1.2 or later, also on redirects, and replaces wget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: VectoDE <system.bot.discord.utility@gmail.com>
SonarQube Cloud (shelldre:S1192) asks for a constant instead of six copies
of the same option.

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

Copy link
Copy Markdown

@VectoDE
VectoDE marked this pull request as ready for review October 13, 2026 16:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

OpenSSF Best Practices badge

2 participants