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.
A PostgreSQL cluster/database freezes its encoding at initdb / CREATE
DATABASE time; a C (non-UTF-8) locale yields a SQL_ASCII cluster, which
makes psycopg3 return bytes and crashes SQLAlchemy. Harden the installer
and add a reusable pattern for future DB installers:
- ensure_utf8_locale_active: generate AND activate en_US.UTF-8 for the
install process before the server package runs initdb; abort if the
locale is not actually available
- create the database explicitly with TEMPLATE template0 ENCODING 'UTF8'
LC_COLLATE/LC_CTYPE 'en_US.UTF-8' instead of inheriting the cluster
default
- assert_db_encoding_utf8: post-install guard, abort with an actionable
message if pg_encoding_to_char is not UTF8 (catches old SQL_ASCII DBs
on re-run too)
- credentials/README docs use su - postgres -c (minimal LXCs have no sudo)
- merge the two contributing sections into one (PR + cross-review rule,
both real incidents, CI enforcement in present tense - the suite is
live on the homelab runner since K-114/PR #5)
- script catalog: runner is in production (PR #6 merged)
- usage: document input validation behavior (re-prompt on junk bytes,
env values abort when malformed)
- pattern: SSH root login prompt (sshd drop-in) and the locale fix in
setup_base_apt
- repo layout: tests/ added
- validation helpers: sanitize_input trims CR/edge whitespace only;
embedded control/non-ASCII bytes FAIL validation and re-prompt with a
hint (2026-06-11 incident: invisible byte in a pasted VLAN tag broke
pct create mid-run) - never silently stripped
- prompt_lxc_config: every prompt validated (uint for CTID/disk/cores/
RAM, VLAN 1-4094, hostname/token formats, IP/CIDR/gateway, DNS list);
env-provided values are sanitized + validated too (abort, no re-prompt
loop in non-interactive use); helpers reusable for app prompts
- tests/test_validation.sh: 34 cases incl. the 2<0x80>0 repro, re-prompt
simulation, BASH_REMATCH clobbering regression (is_cidr), env dry-run
of prompt_lxc_config without PVE/TTY
- tests/check_ct_source.sh: every ct/*.sh must source build.func (bug
shipped twice); negative proof via prepared fixture in the test suite
- .gitea/workflows/ci.yml: bash -n over all scripts, source-check,
validation tests, shellcheck if present (documented skip otherwise)
- README: contributions via PR with cross-review (binding)