in-toto
A Conn3x circuit that installs the in-toto command-line tools on your hosts. With them, each machine in a build records what it did and signs it, and whoever receives the result can check that every step happened, was done by the right person, and that nothing was swapped in between.
Each host gets in-toto 3.1.0 in a virtual environment of its own under /opt/in-toto, installed by pip from a requirements file that pins every package and the SHA-256 of every file it may download. The commands are linked into /usr/local/bin.
module "in_toto" { source = "github.com/conn3xorg/circuit-in-toto?ref=v0.1.1"
hosts = { build = "203.0.113.10", release = "203.0.113.11" } ssh_private_key_file = "~/.ssh/id_ed25519"}Then run conn3x init and conn3x apply. A second conn3x plan says No changes.
On a build host, each step runs through in-toto-run, which records its materials and products in a signed link file:
in-toto-run -n package --signing-key bob -m app.sh -p app.tar.gz -- tar -czf app.tar.gz app.shWhere the result lands, in-toto-verify checks the links against a layout the project owner signed:
in-toto-verify --layout root.layout --verification-keys owner.pubThe keys are PEM files: PKCS8 for the private ones, SubjectPublicKeyInfo for the public. in-toto takes ed25519, RSA and ECDSA P-256 keys; the example uses ed25519. in-toto 3 has no command to make keys or layouts any more, so examples/local/check.sh does both with a few lines of Python from the same venv. It’s a whole working example: three keys, a two-step layout with an inspection, and the two kinds of tampering it catches.
To try it on a throwaway container first, see examples/local.
Which version, and why
Section titled “Which version, and why”The default is 3.1.0, the latest release on PyPI when this was written. A Python package isn’t one file, so pinning in-toto alone would leave its dependencies to pip’s choice on the day. requirements.txt pins all ten packages, each with the hashes of every file PyPI has for that version, and the install uses:
--require-hashes, so a file whose SHA-256 isn’t listed is refused before it’s installed--only-binary :all:, so only wheels are used and no package’s build code runs--no-deps, so nothing outside the file comes in
Your pip settings still apply, so a host that installs through a mirror keeps doing so. The hashes are what’s checked.
For another version, lock it yourself and pass both. Check its changelog first:
echo 'in-toto==3.1.0' | uv pip compile --generate-hashes --universal --python-version 3.12 --no-header - > in-toto.txtin_toto_version = "3.1.0"requirements = file("${path.module}/in-toto.txt")pip-compile --generate-hashes from pip-tools writes the same format. The plan stops if requirements doesn’t pin in-toto==<in_toto_version> with hashes, or if you change in_toto_version without it.
What it does
Section titled “What it does”Before the circuit runs, the conn3x_host data source reads each host’s distribution over SSH. Then, on each host:
- Installs
python3, andpython3-venvon Debian and Ubuntu. - Makes a virtual environment at
/opt/in-toto/<version>with the distribution’s Python. If the venv’s pip stops running, as after an upgrade to a new Python minor release, it’s made again from scratch. - Writes the requirements to
/opt/in-toto/<version>.requirements.txtand installs from them with pip. A copy inside the venv records what was installed, so the next plan knows whether anything changed. Changed requirements install again, and so does a venv wherepip checkfinds a problem. - Links
in-toto-run,in-toto-record,in-toto-verify,in-toto-sign,in-toto-match-productsandin-toto-mockinto/usr/local/bin.
What it doesn’t do
Section titled “What it doesn’t do”- It doesn’t make or hand out keys, write layouts or run anything through in-toto. That’s your build’s job.
- It doesn’t set up GPG. in-toto can sign with GPG keys, but that needs
gpgon the host, which this circuit doesn’t install. - It doesn’t install the attestation tooling for in-toto’s newer statement format, or any of the in-toto ports in other languages.
- It doesn’t keep old versions tidy. A new
in_toto_versiongets its own venv and leaves the previous one under/opt/in-toto. - It doesn’t support RHEL, AlmaLinux, Ubuntu 22.04 or Debian 12, and the plan stops on them. They just haven’t been tested here.
- Deleting the module leaves the venv and the links in place.
Inputs
Section titled “Inputs”| Name | Type | Default | Description |
|---|---|---|---|
hosts |
any | Required | Hosts to configure, by name. Each value is an address, such as "203.0.113.10", or a host object such as { address = "203.0.113.10", host_key = "ssh-ed25519 AAAA..." }. A host object can also set user, port, private_key_file and jump_hosts. The two forms can be mixed. |
ssh_user |
string | "root" |
User to log in as. |
ssh_port |
number | 22 |
SSH port. |
ssh_private_key_file |
string | null |
Path to a private key. When null, Conn3x uses the SSH agent. |
ssh_known_hosts_file |
string | null |
known_hosts file to check host keys against. When null, ~/.ssh/known_hosts. Not used for a host object with host_key. |
ssh_become |
bool | false |
Run commands as root through sudo. Set it when ssh_user isn’t root. |
in_toto_version |
string | "3.1.0" |
in-toto release to install. Any other needs requirements. See Which version, and why. |
requirements |
string | null |
Hash-locked pip requirements for in_toto_version. When null, the module’s requirements.txt for 3.1.0. |
Outputs
Section titled “Outputs”| Name | Description |
|---|---|
venv |
/opt/in-toto/<version>, the virtual environment in-toto is in. |
commands |
The in-toto commands linked into /usr/local/bin. |
version |
The in-toto release installed. |
Requirements
Section titled “Requirements”Requires Conn3x v0.7.0 or later. Hosts need to reach PyPI, or a mirror pip is set up to use. The Python each distribution ships works: 3.12 on Ubuntu 24.04 and Rocky Linux 10, 3.13 on Debian 13, and Fedora 45’s own.
Tested on
Section titled “Tested on”Ubuntu 24.04, Debian 13, Rocky Linux 10 and Fedora 45, with in-toto 3.1.0. examples/local was applied to each in a container on arm64, and CI does the same on amd64: it checks that the second plan has no changes, then runs check.sh, which makes three ed25519 keys and a two-step layout with an inspection, runs the build through in-toto-run, and checks that in-toto-verify accepts it and refuses both a source file changed between the steps and a tarball swapped afterwards. Then it restarts the container, plans again with no changes, and runs the check again. On Debian 13, requirements with one hash changed failed the apply with pip’s hash mismatch, and an in_toto_version without requirements stopped the plan.
Upgrades are tested too. CI starts a fresh host, applies in-toto 3.0.0 from the requirements in tests/upgrade, then applies the circuit’s own 3.1.0 over it. The plan after has no changes and check.sh passes on 3.1.0. That also ran in a container on arm64 on Rocky Linux 10 and Debian 13.
On an Ubuntu 24.04 droplet and a Rocky Linux 10 droplet with SELinux enforcing, both s-1vcpu-1gb, the circuit was applied and the same check passed. Changed requirements installed again, and the plan after had no changes. After a reboot the plan had no changes and the check passed again. Debian 13 and Fedora 45 ran in containers only.
License
Section titled “License”MPL-2.0. See LICENSE. in-toto is Apache-2.0.
Source
Section titled “Source”- Repository: conn3xorg/circuit-in-toto. This page covers v0.1.1; the repository’s tags list every release.
- Examples: examples/local.
- Related circuits: Baseline, Hardening, Firewall, fail2ban, Automatic updates, OpenSCAP, auditd, Trivy, CISO Assistant, ZAP, checkov, cosign, TruffleHog.