KeiSeiKit-1.0/skills/vm-provision/phase-4-harden.md
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

3.4 KiB

Phase 4 — Harden via harden-base.sh

Goal: run _primitives/harden-base.sh on the VM, over SSH, idempotently. Verify criterion: script exited 0; systemctl is-active returns active for ssh, ufw, fail2ban, auditd.


4.a — Ship the script

The script lives on the workstation; copy to the VM and run with sudo:

scp _primitives/harden-base.sh "${ADMIN_USER}@${VM_IP}:/tmp/harden-base.sh"
ssh "${ADMIN_USER}@${VM_IP}" "sudo bash /tmp/harden-base.sh \
  --admin-user ${ADMIN_USER} \
  --ssh-port  ${SSH_PORT} \
  $(for p in ${APP_PORTS[@]}; do echo --allow-port $p; done)"

Why not curl … | bash? Because that depends on a hosted URL AND a trusted TLS cert. scp the file you already audited locally. Lower surface area, reproducible.

The script is idempotent — safe to re-run. Re-runs converge the VM to the declared state; missing directives get rewritten, extra ones are left alone.


4.b — Stream logs

harden-base.sh logs to stderr with timestamps. Capture to <run-dir>/harden.log:

ssh "${ADMIN_USER}@${VM_IP}" "sudo bash /tmp/harden-base.sh …" 2> >(tee <run-dir>/harden.log >&2)

If the script exits non-zero: STOP. Do NOT proceed to Phase 5. Surface the last 30 lines of <run-dir>/harden.log + ask the user to choose:

  • (A) Fix locally + re-ship — edit the primitive (if bug is there) or adjust flags. Commit the fix under checkpoint: before retry.
  • (B) Patch the VM manually — user logs in, fixes, we re-run the script to ensure idempotency.
  • (C) Destroy + reprovision — when remediation risk > cost of a fresh VM (2 min on Hetzner).

4.c — Post-hardening live-check

After exit 0, SSH back in and confirm:

ssh "${ADMIN_USER}@${VM_IP}" "
  set -e
  systemctl is-active ssh ufw fail2ban auditd unattended-upgrades.service 2>/dev/null || true
  ufw status | head -20
  sudo auditctl -l | head -10
"

All four services must be active. auditctl -l must show the baseline rules (sshd_config, sudoers, identity, module, time). Record the output in <run-dir>/post-harden.txt.


4.d — AskUserQuestion (ready to verify?)

One AskUserQuestion:

Hardening applied. Four services active; auditd rules loaded.

  • Run verification gate (Phase 5).
  • Apply one more pass (typo in APP_PORTS, extra user, etc. — loops 4.a with a delta).
  • Pause (leave the VM in current state).

4.e — Verify criterion

  • harden-base.sh exited 0.
  • ssh / ufw / fail2ban / auditd all active.
  • <run-dir>/harden.log + <run-dir>/post-harden.txt captured.

Emit: Phase 4 done: 4/4 services active. Log: <run-dir>/harden.log.

Proceed to Phase 5 (hard gate).


4.f — Non-obvious failure modes

  • systemctl reload ssh fails because sshd -t rejects the drop-in. Usually a custom SSH_PORT collides with ufw still configured for 22. Fix: ensure ufw rule + sshd Port match BEFORE reload. harden-base.sh writes both in one pass, but if an out-of-band edit happened between runs, you get this.
  • fail2ban service flaps. Usually a systemd-journal backend mismatch on very old Debian. Verify backend = systemd in /etc/fail2ban/jail.local (script sets this).
  • auditd refuses -e 2. Means an earlier rules load is still mastered; augenrules --load forces reload. Already in the script.

None of these require a Level-2 escalation — all three have known fixes.