Hardened baseline for mixed client fleets: one tested role library, per-tenant inventories, applied in rolling batches.
  • Shell 55.7%
  • PowerShell 15.7%
  • Python 14.5%
  • Jinja 10.1%
  • Makefile 2.3%
  • Other 1.7%
Find a file
Claudio Maradonna 0b05707142
docs(agents): upgrade agent-foundry to fef5993
Bring in agent-foundry 7e46402..fef5993 by three-way merge: the
foundry-upgrade skill and its .claude/skills link, the optional
Memory triggers section of AGENTS.local.md in the Project memory rules,
and the curator's no-write for candidates outside declared triggers in
docs/LOCALRECALL.md.

The skill's Record now names the source and adopted commit, so the
README credit keeps the attribution without the commit and the template
lifecycle rule points at the skill for re-adoption.
2026-09-29 22:41:56 +02:00
.agents/skills docs(agents): upgrade agent-foundry to fef5993 2026-09-29 22:41:56 +02:00
.claude docs(agents): upgrade agent-foundry to fef5993 2026-09-29 22:41:56 +02:00
.codex/agents
docs docs(agents): upgrade agent-foundry to fef5993 2026-09-29 22:41:56 +02:00
group_vars
plugins/cache refactor(make): cut the Makefile to four targets 2026-09-29 19:36:08 +02:00
roles fix(scan-share): render dict fail_msg values as JSON 2026-09-29 22:03:51 +02:00
scripts
tests test(gates): log tag_propagation beside the scenarios 2026-09-29 22:13:43 +02:00
.ansible-lint
.gitignore test(molecule): run scenarios in parallel, fail fast 2026-09-29 20:00:05 +02:00
.yamllint
AGENTS.md docs(agents): upgrade agent-foundry to fef5993 2026-09-29 22:41:56 +02:00
ansible.cfg refactor(make): cut the Makefile to four targets 2026-09-29 19:36:08 +02:00
CLAUDE.md
CONTRIBUTING.md perf(molecule): start the slowest roles first 2026-09-29 22:13:43 +02:00
COPYING
handbook.yml
Makefile perf(molecule): start the slowest roles first 2026-09-29 22:13:43 +02:00
os-update.yml
reachable.yml
README.md docs(agents): upgrade agent-foundry to fef5993 2026-09-29 22:41:56 +02:00
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. handbook.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 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:

    managed:
      hosts:
        web1:
          ansible_host: 192.0.2.10
    # webservers:        # a host joins as many groups as apply
    #   hosts:
    #     web1:
    
  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 handbook.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

  • handbook.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 handbook.yml -i inventory/<tenant>/                   # the whole tenant
ansible-playbook handbook.yml -i inventory/<tenant>/ -e target=web1    # one host
ansible-playbook handbook.yml -i inventory/<tenant>/ --tags firewall   # one concern
ansible-playbook handbook.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.