Hardline architecture
Inside Hardline: the snapshot, atomic write, validate and confirm transaction, the boot guard, state on disk, the fixed program list and the threat model.
Hardline is a single static Rust binary with no daemon and no network access. This page is how it works inside: the transaction every change goes through, what it keeps on disk, the programs it is allowed to run, and what it defends against. The full design document is ARCHITECTURE.md.
Four layers
- Controls: declarative TOML files in
controls/, compiled into the binary withinclude_str!. Nothing is loaded from disk at run time, so a writable directory can't add a control. - Probes: read-only checks (
sshd_effective,sysctl,module_disabled,file_mode, …).auditonly ever runs these. - Actions: typed fixes (
sshd_directive,sysctl,module_disable, …), each with a matching undo. - Guard: the revert timer, the pending marker and the boot guard that make SSH and firewall changes provable.
A control, exactly as shipped
A control names a check kind and a fix kind. It cannot contain a command, a path to execute or a script, because the schema has no field for one. Unknown keys are a build error.
schema = 1
id = "ssh-x11-forwarding"
phase = 1
title = "Disable SSH X11 forwarding"
severity = "high"
profiles = ["baseline", "stig"]
guide = "ssh-x11"
preflight = ["sshd-config-valid"]
[refs]
stig = ["UBTU-24-300022"]
cci = ["CCI-000366"]
[check]
kind = "sshd_effective"
key = "x11forwarding"
expect = { equals = "no" }
[fix]
kind = "sshd_directive"
directive = "X11Forwarding"
value = "no"
schema = 1
id = "kernel-dccp"
phase = 7
title = "The DCCP network protocol can't be loaded"
severity = "low"
profiles = ["baseline"]
guide = "kernel-modules"
[check]
kind = "module_disabled"
module = "dccp"
[fix]
kind = "module_disable"
module = "dccp"
The guide key points at the shared text behind hardline explain and /hardline/controls/, so the CLI and this site can't disagree.
The apply transaction
hardline apply --phase N --commit
│
├─ plan controls × profile × phase × distro, minus config skips
├─ probe read-only: sshd -T per user, nft list, sysctl, files…
├─ preflight key present? sshd config valid? not cutting own root login?
├─ snapshot copy every target → /var/lib/hardline/snapshots/phase<N>-<ts>/
├─ write temp file in same dir → fsync → rename(2) → fsync dir → restorecon (SELinux)
├─ validate sshd -t / nft -c / firewall-offline-cmd --check-config / dockerd --validate
│ └─ fail → restore snapshot, nothing reloaded
├─ arm guard pending.json + systemd-run revert timer (+ hardline-guard.service at boot)
├─ activate systemctl reload ssh / nft -f / firewall-cmd --reload / sysctl --system
└─ wait `hardline confirm` from a NEW SSH connection → disarm, write baseline
no confirm before the timer → revert snapshot, reload, clear pending
Phases with no lockout risk (kernel, audit, accounts, containers) still snapshot, write atomically and validate, but commit immediately; there is nothing to confirm. Rollback restores bytes, mode, owner and SELinux label from the manifest, refuses if a file changed since apply unless you pass --force, and puts back package-created config files instead of deleting them. Packages Hardline installed are left installed.
Why confirm needs a new connection
apply records SSH_CONNECTION (client IP and port, server IP and port). confirm compares its own and refuses a match: surviving an sshd reload proves an existing session is fine, not that a new login works. The new connection is the proof. On a console, confirm --from-console is the explicit escape hatch.
# session 1
sudo hardline apply --phase 1 --commit # countdown starts (120 s)
sudo hardline confirm # refused: same connection
# session 2, a brand-new ssh login
sudo hardline confirm # accepted, guard disarmed
Reboots and crashes
The revert timer is a transient systemd unit, so it fires even if Hardline, your shell or your network dies. A reboot clears transient units, so hardline-guard.service runs early at boot, ordered before sysinit.target, ssh/sshd, ufw, firewalld and Hardline's own nftables loader; if pending.json exists it reverts before anything can listen. Both paths are exercised on real VMs before every release.
State on disk
/var/lib/hardline/ 0700 root:root
├── baseline.json last committed audit (diff + watch compare against it)
├── pending.json only while a change waits for confirm
└── snapshots/phase<N>-<timestamp>/
├── manifest.json per file: path, existed, mode, uid, gid, sha256; plus units, packages, firewall + sysctl state
└── files/… byte-for-byte copies of every touched file
/run/hardline.lock flock: one Hardline at a time
/etc/hardline/config.toml optional; can only skip controls, with a reason
/etc/systemd/system/hardline-guard.service boot-time revert, ordered before ssh + firewall
hardline diff compares the live server with baseline.json and each managed file with its recorded SHA-256.
Running programs
Every process starts from one module. Arguments are a vector, never a string, so there is no shell and no quoting to get wrong. The environment is cleared, which also stops PATH and LD_PRELOAD tricks.
// src/exec.rs: the only place Hardline starts a process.
pub enum Program { Sshd, Systemctl, SystemdRun, Ufw, Nft, Sysctl, Modprobe, /* … */ }
impl Program {
fn path(self) -> &'static str {
match self {
Program::Sshd => "/usr/sbin/sshd",
Program::Nft => "/usr/sbin/nft",
// …one absolute path per program, nothing else can run
}
}
}
// Command::new(prog.path()).args(argv).env_clear().env("LC_ALL", "C")
The complete list of programs Hardline can run: sshd systemctl systemd-run ufw nft firewall-cmd firewall-offline-cmd sysctl modprobe auditctl augenrules aa-status getenforce restorecon apt-get dpkg-query dnf rpm passwd docker dockerd nginx apache2ctl openssl timeout. Each is used only by the distros and controls that need it.
Threat model
| Threat | Defence |
|---|---|
| The change locks the operator out | Preflight, validation, new-connection confirm, revert timer, boot guard. |
| A non-root user swaps the binary the root timer will run | Executable and every parent directory must be root-owned and not group/other-writable before a guard is armed. |
| Command injection through config, hostnames or file contents | No shell; fixed argv; controls compiled in; config can only skip. |
| Memory corruption | Rust with #![forbid(unsafe_code)]. |
| Half-written file after a crash or full disk | Write to a temp file, fsync, rename(2); write errors fail the run. |
| Two Hardlines racing | flock on /run/hardline.lock. |
| Clobbering someone else's config | Drop-ins and owned objects only: sshd_config.d, table inet hardline, merged daemon.json. |
| Leaking server details | No network calls or telemetry; reports 0600 with default-src 'none' CSP. |
Out of scope: an attacker who already has root, kernel exploits, and anything outside the controls listed. Hardline reduces attack surface; it is not intrusion detection.