Hardened baseline for mixed client fleets: one tested role library, per-tenant inventories, applied in rolling batches.
  • Shell 63.3%
  • Python 12.2%
  • PowerShell 12.1%
  • Jinja 6.8%
  • JavaScript 2.6%
  • Other 3%
Find a file
Motoko Kusanagi 3553a82ba4
All checks were successful
check / check (pull_request) Successful in 19m22s
check / check (push) Successful in 19m34s
ci: run on pushes to main only
A pull request's branch ran the job twice, once for the push and once
for the pull request. Pull requests are covered by their own event, so
pushes need running only on main, where merges land.
2026-11-12 10:46:17 +00:00
.agents
.claude
.codex
.config/molecule
.forgejo ci: run on pushes to main only 2026-11-12 10:46:17 +00:00
.opencode
docs
group_vars
plugins/cache
roles
scripts
tests fix(tests): pass ShellCheck 0.10 in the molecule runner 2026-11-12 10:22:27 +00:00
.ansible-lint
.gitignore
.prettierrc
.yamllint
AGENTS.md docs(contributing): say what the forge runs 2026-11-12 09:29:17 +00:00
ansible.cfg
CONTRIBUTING.md ci: run on pushes to main only 2026-11-12 10:46:17 +00:00
COPYING
fortify.yml
Makefile
os-update.yml
reachable.yml
README.md
requirements-dev.txt
requirements.yml
ssh-copy-id.yml

castrum

Hardened baseline for mixed client fleets: one tested role library, per-tenant inventories, applied in rolling batches.

A Roman castrum was built to the same plan wherever the legion stopped — same layout, same gates, same defences, on any ground. This repository is that plan for a fleet of machines: the same hardening, firewall and service configuration applied identically across every site it manages, varied only where the ground demands it.

Each site's inventory lives outside this repository. What is here is what a group means, the roles that implement it, and the tests that prove they do.

Goal

castrum gives every host in a fleet the same hardening, firewall, services, sysadmin tooling and updates, varied only by the groups a host belongs to and its own host variables, never by editing a playbook. fortify.yml and os-update.yml apply it in widening batches, one host first, so a bad change breaks one machine while the rest are still untouched.

Scope is GNU/Linux, Debian family, plus Windows 11 workgroup PCs over OpenSSH: their hostname, a hardening baseline, the Windows firewall, Windows Update and, in scan_shares, a scanner drop share. A Windows Server gets the Windows firewall, key-only SSH and Windows Update. castrum is not a provisioning tool — it configures machines that already exist — and not a multi-OS abstraction.

Quick start

  1. Clone the repository and build the toolchain (needs Python 3.12+):

    git clone https://git.unitoo.it/claudiomaradonna/castrum.git && cd castrum
    make setup && . .venv/bin/activate
    
  2. Create inventory/<tenant>/hosts.yml with one host in managed, the group every playbook targets, and give the host its address in inventory/<tenant>/host_vars/web1.yml:

    managed:
      hosts:
        web1:
    # webservers:        # a host joins as many groups as apply
    #   hosts:
    #     web1:
    
    ansible_host: 192.0.2.10
    
  3. Install your key once, with password authentication this one time (needs sshpass on your machine and a host that accepts a root password login once):

    ansible-playbook ssh-copy-id.yml -i inventory/<tenant>/ -k \
      -e ssh_user=root -e "ssh_copy_id='$(cat ~/.ssh/id_ed25519.pub)'"
    

    castrum connects as root (ansible_user in group_vars/all.yml); a tenant that connects as someone else sets ansible_user in its hosts' host_vars (Per-host settings).

    A Windows PC also joins windows — managed alone sends it the Linux play — and runs scripts/bootstrap-windows-ssh.ps1 once, at the PC, instead of this step; set ansible_user in its host_vars to the account that script creates (Windows PCs).

  4. Dry run:

    ansible-playbook fortify.yml -i inventory/<tenant>/ --check --diff
    
  5. Read the diff, then run the same command without --check.

Pass the tenant directory, never inventory/ itself, which would merge every tenant into one run (Tenants and inventories).

Common usage

  • fortify.yml applies the baseline: hardening, firewall, the service roles a host's groups switch on, and sysadmin tooling.
  • os-update.yml updates packages, or runs Windows Update; -e os_update_auto=reboot or shutdown acts when an update wants a reboot.
  • ssh-copy-id.yml installs a public key for a user. Linux only.
ansible-playbook fortify.yml -i inventory/<tenant>/                   # the whole tenant
ansible-playbook fortify.yml -i inventory/<tenant>/ -e target=web1    # one host
ansible-playbook fortify.yml -i inventory/<tenant>/ --tags firewall   # one concern
ansible-playbook fortify.yml -i inventory/<tenant>/ --tags zram       # one role
ansible-playbook os-update.yml -i inventory/<tenant>/ -e os_update_auto=reboot

Two modes are passed with -e, for one run only (Modes):

  • update_only=true skips the install-and-configure work of the roles that implement it.
  • uninstall=true deletes the castrum firewall table until the next ordinary run.

Documentation

License

GNU General Public License, version 3 or later (GPL-3.0-or-later); see COPYING.

Agent policy seeded from Agent Foundry by Claudio Maradonna.