Versions the CIS Tier-A hardening pass that has been running on twelve containers since 2026-07-24.
Why
The script existed only as /root/cis-tierA.sh — a root-owned copy on each machine it hardens. Nothing referenced it, nothing reviewed it, and a change would have had to be made twelve times. It was found only by searching for the string that caused a support case (below).
What it does
SSH drop-in, login.defs aging + YESCRYPT, pwquality/faillock, PAM (pwquality, pwhistory, faillock, nullok removed) and auditd rules. Idempotent, backs everything up to /root/cis-hardening-backup-<ts>/, gates the SSH restart on sshd -t, rolls PAM back when a referenced module is missing, and probes whether auditd works at all in an unprivileged LXC (it does not — the host owns the audit subsystem — so the package is removed again).
Two substantive changes over the deployed copy
1. Forwarding is denied per option, not via DisableForwarding.
Same effect, different diagnosability. DisableForwarding overrides every other forwarding option and does not appear in sshd -T. The symptom this produced:
$ ssh -L 9749:127.0.0.1:9749 lutz@luki-dev
channel 3: open failed: administratively prohibited: open failed
$ sudo sshd -T | grep -i forwarding
allowtcpforwarding yes <- and yet every forward is refused
A configuration that reports itself as permissive while refusing the operation is expensive to diagnose. The four explicit options report honestly.
2. MODDIR command substitution quoted (SC2046, the only shellcheck finding in the file).
Host-specific exceptions
Section 1 rewrites 99-cis-hardening.conf wholesale on every run, so exceptions must not live in it. They belong in a drop-in that sorts after it — 99-zz-local-forward.conf on luki-dev, granting one user a local forward to one loopback port. Verified by simulating a rerun: the patched heredoc was written into a copy of the configuration and checked with sshd -t -f / sshd -T -C user=... -f; the exception survives. Header and README both record that a Match block extends until the next Match, across Include file boundaries, so such a drop-in has to stay alphabetically last.
Checks
bash -n clean, tests/check_ct_source.sh and tests/test_validation.sh pass (50/50), shellcheck -S warning -e SC1090,SC1091 install/cis-tierA.sh clean. Note that the existing scripts do report shellcheck findings — the CI step is currently skipped because shellcheck is not on the runner (K-114). Not addressed here.
Not in scope
The eleven other containers still carry the deployed copy with DisableForwarding. That is correct as long as only luki-dev needs a tunnel; rolling the new version out is a separate, deliberate step. The PVE hosts and the Wazuh manager VM stay unhardened by design — they are the break-glass foundation.
Versions the CIS Tier-A hardening pass that has been running on twelve containers since 2026-07-24.
## Why
The script existed only as `/root/cis-tierA.sh` — a root-owned copy on each machine it hardens. Nothing referenced it, nothing reviewed it, and a change would have had to be made twelve times. It was found only by searching for the string that caused a support case (below).
## What it does
SSH drop-in, `login.defs` aging + YESCRYPT, pwquality/faillock, PAM (pwquality, pwhistory, faillock, `nullok` removed) and auditd rules. Idempotent, backs everything up to `/root/cis-hardening-backup-<ts>/`, gates the SSH restart on `sshd -t`, rolls PAM back when a referenced module is missing, and probes whether auditd works at all in an unprivileged LXC (it does not — the host owns the audit subsystem — so the package is removed again).
## Two substantive changes over the deployed copy
**1. Forwarding is denied per option, not via `DisableForwarding`.**
Same effect, different diagnosability. `DisableForwarding` overrides every other forwarding option *and* does not appear in `sshd -T`. The symptom this produced:
```
$ ssh -L 9749:127.0.0.1:9749 lutz@luki-dev
channel 3: open failed: administratively prohibited: open failed
$ sudo sshd -T | grep -i forwarding
allowtcpforwarding yes <- and yet every forward is refused
```
A configuration that reports itself as permissive while refusing the operation is expensive to diagnose. The four explicit options report honestly.
**2. `MODDIR` command substitution quoted** (SC2046, the only shellcheck finding in the file).
## Host-specific exceptions
Section 1 rewrites `99-cis-hardening.conf` wholesale on every run, so exceptions must not live in it. They belong in a drop-in that sorts *after* it — `99-zz-local-forward.conf` on luki-dev, granting one user a local forward to one loopback port. Verified by simulating a rerun: the patched heredoc was written into a copy of the configuration and checked with `sshd -t -f` / `sshd -T -C user=... -f`; the exception survives. Header and README both record that a `Match` block extends until the next `Match`, across `Include` file boundaries, so such a drop-in has to stay alphabetically last.
## Checks
`bash -n` clean, `tests/check_ct_source.sh` and `tests/test_validation.sh` pass (50/50), `shellcheck -S warning -e SC1090,SC1091 install/cis-tierA.sh` clean. Note that the existing scripts do report shellcheck findings — the CI step is currently skipped because shellcheck is not on the runner (K-114). Not addressed here.
## Not in scope
The eleven other containers still carry the deployed copy with `DisableForwarding`. That is correct as long as only luki-dev needs a tunnel; rolling the new version out is a separate, deliberate step. The PVE hosts and the Wazuh manager VM stay unhardened by design — they are the break-glass foundation.
The script that sets the SSH, PAM, pwquality and auditd baseline on twelve
containers existed only as a root-owned copy on the machines it hardens.
Bring it into the repo so a change reaches one place instead of twelve.
Two substantive changes over the copy that shipped on 2026-07-24:
- Deny forwarding per option instead of via DisableForwarding. Same effect,
but DisableForwarding overrides every other forwarding option and is
invisible in sshd -T, which makes a rejected port-forward read as a
configuration that should work.
- Quote the command substitution in MODDIR (SC2046).
The header and README now record the rollout command, the containers left
unhardened as break-glass foundation, and why host-specific exceptions must
live in a drop-in that sorts after 99-cis-hardening.conf.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Versions the CIS Tier-A hardening pass that has been running on twelve containers since 2026-07-24.
Why
The script existed only as
/root/cis-tierA.sh— a root-owned copy on each machine it hardens. Nothing referenced it, nothing reviewed it, and a change would have had to be made twelve times. It was found only by searching for the string that caused a support case (below).What it does
SSH drop-in,
login.defsaging + YESCRYPT, pwquality/faillock, PAM (pwquality, pwhistory, faillock,nullokremoved) and auditd rules. Idempotent, backs everything up to/root/cis-hardening-backup-<ts>/, gates the SSH restart onsshd -t, rolls PAM back when a referenced module is missing, and probes whether auditd works at all in an unprivileged LXC (it does not — the host owns the audit subsystem — so the package is removed again).Two substantive changes over the deployed copy
1. Forwarding is denied per option, not via
DisableForwarding.Same effect, different diagnosability.
DisableForwardingoverrides every other forwarding option and does not appear insshd -T. The symptom this produced:A configuration that reports itself as permissive while refusing the operation is expensive to diagnose. The four explicit options report honestly.
2.
MODDIRcommand substitution quoted (SC2046, the only shellcheck finding in the file).Host-specific exceptions
Section 1 rewrites
99-cis-hardening.confwholesale on every run, so exceptions must not live in it. They belong in a drop-in that sorts after it —99-zz-local-forward.confon luki-dev, granting one user a local forward to one loopback port. Verified by simulating a rerun: the patched heredoc was written into a copy of the configuration and checked withsshd -t -f/sshd -T -C user=... -f; the exception survives. Header and README both record that aMatchblock extends until the nextMatch, acrossIncludefile boundaries, so such a drop-in has to stay alphabetically last.Checks
bash -nclean,tests/check_ct_source.shandtests/test_validation.shpass (50/50),shellcheck -S warning -e SC1090,SC1091 install/cis-tierA.shclean. Note that the existing scripts do report shellcheck findings — the CI step is currently skipped because shellcheck is not on the runner (K-114). Not addressed here.Not in scope
The eleven other containers still carry the deployed copy with
DisableForwarding. That is correct as long as only luki-dev needs a tunnel; rolling the new version out is a separate, deliberate step. The PVE hosts and the Wazuh manager VM stay unhardened by design — they are the break-glass foundation.