Fixing & rollback
Hostveil never mutates blindly. Every fix is classified by how safely it can be automated, always shows the exact change first, backs up the original to a checkpoint, and can be undone with one command.
How a finding is classified
Each finding is tagged with a remediation kind that decides how much automation is appropriate:
| Kind | Meaning | Fixable? |
|---|---|---|
| Auto-fix | One clearly-correct change. You still see it before it applies. | Yes |
| Review | Several valid alternatives; you choose which one to apply. | Yes |
| Manual | No safe automation — Hostveil explains what to do instead. | No |
| Unavailable | Known issue with no fix yet (e.g. a CVE with no patch). | No |
Only Auto-fix and Review findings can be applied by hostveil. fix --all applies only the Auto-fix ones, leaving anything that needs a decision to you; fix --all --review is how you say, in the command, that you accept the Review ones too.
What an Auto-fix has to be
Auto-fix is the only kind fix --all applies unless you add --review, so the bar is what makes a change defensible when nobody has looked at it first. Three things have to hold at once.
Reversible. The action has to leave something a rollback can restore: an edit stores the original bytes, a permission change stores the original mode. A fix that runs a command has nothing to store — hostveil history records that it ran and marks the entry not reversible — so a fix that runs a command is never Auto-fix, and Hostveil applies that floor to the fix itself rather than trusting each check to declare it. updates.disabled is the shape the floor exists for: its whole remediation is a single action — install unattended-upgrades, enable the service — which is all “Auto-shaped” means, and it is still a command, so you get Review. fileperms.shadow and fileperms.owner are the same criterion landing on opposite sides: tightening /etc/shadow’s mode is Auto-fix because a checkpoint records the old mode, and chown root:root is Manual because a checkpoint has nowhere to record the old owner.
Recoverable in practice, not just on disk. If the change is wrong, you have to still be able to reach the machine to undo it. Anything that can cut off your own access fails this even when the edit reverts perfectly. That is why several SSH findings are Review despite being a one-line change to sshd_config. ssh.passwordauth writes PasswordAuthentication no, which is byte-for-byte reversible and no help at all if key-based login was not already working, because rolling it back needs the session it just ended. ssh.gatewayports, ssh.hostbasedauth and ssh.kbdinteractive each close a route that may be the one you are on. firewall.inactive is the same objection taken far enough to leave the finding Manual: a default-deny policy takes effect immediately, on every connection no rule already allows, and a command leaves no checkpoint to undo it with.
Unambiguous. There has to be exactly one correct value, and applying it must not break a legitimate configuration. The kernel-hardening findings all fail this and are all Review: writing a sysctl.d drop-in is one mechanical, reversible edit, but rp_filter is 1 on a single-homed server and 2 on a VPN or multi-homed one, and you are the one who knows which host this is. Hostveil still writes the value — after you have looked at it. What passes looks like fileperms.shadow, whose fix only ever removes permission bits — 0604 becomes 0600, never 0640, so it cannot grant access the file did not have — or agent.config-perms, where the mode it sets is the one the runtime’s own hardening guide ships.
Why a failing criterion sometimes means Manual instead of Review
Failing one of the three makes a finding Review when there are two or more independent alternatives to choose between. Review is a choice, not a warning: Hostveil shows you the options and you pick one. Where the only mechanical remediation is a single action, there is nothing to choose between, and the finding is Manual instead.
The kernel findings have a genuine pair — write the drop-in, which survives a reboot, or sysctl -w, which takes effect now and does not — so they are Review. compose.ds017 does not: adding :ro is the one mechanical remediation and it breaks any service that legitimately writes to the mount, and the alternative the finding names, mounting a narrower subdirectory, needs to know which paths the service touches. One option and no way to make it safe is Manual.
Every Manual finding carries one sentence saying what stopped the fix, next to the finding itself — in hostveil explain, in the terminal’s detail pane, and in the dashboard. The reasons on What it checks are not a policy Hostveil applies to your host; they are a decision someone made about each finding, and you should be able to read it where you meet it.
How the kind is decided
Two sources settle the kind on your scan output, and the more cautious of the two wins.
The fix registry decides whether a fix exists at all. A finding whose check asked for a fix but has none registered is shown as Manual, so a fix button never leads nowhere. (A finding the check marked Unavailable — cve.unpatched-image, vulnerabilities nobody has published a fix for — stays Unavailable.) compose.ds016, the Docker socket mounted into a container, is the clearest case: the compose check asks for a fix and the registry declines to offer one, because the only honest remediation deletes the mount and breaks Portainer, Traefik and Watchtower.
The check decides how much judgement applying it needs. A fix registered as Auto-fix is a statement about its shape — one mechanical action — not a claim that resolving the finding is safe to do unattended, and it cannot talk a check down. Where the check asked for Review, you get Review. ssh.passwordauth is where you can watch this happen: its fix is a single edit setting a single directive, which is the shape Hostveil registers as Auto-fix, and the SSH check asks for Review because of what disabling password login can do to your own access. You are shown Review, and fix --all leaves it alone.
The rule runs in both directions: a check that says Manual is not overruled by a fix that happens to match the finding ID. compose.ds018 shows it — a datastore published on all interfaces is Auto-fix when it comes from a Compose service, because there is a file to edit, and Manual for the same rule on a container started with docker run, because there is not. Only the check knows which it is looking at. accounts.uid0 and every dockerd. finding are Manual the same way — declared so by the check that found them, not by an absent fix.
Preview, fix, the TUI and the dashboard all resolve the kind through the same code, so the kind you read in a report is the one that decides what fix --all will touch.
The procedure, in four steps
You can work out any finding's kind yourself. Hostveil applies these in order:
- The check declares how much judgement it thinks the finding needs.
- Is a fix registered for this ID, and does it build for this finding? If not, the answer is Manual, and nothing below can change that.
- The registered fix has a shape: Auto-fix is exactly one action, Review is two or more independent alternatives. A fix that would be Auto-fix by shape but runs a command instead of editing a file becomes Review here, because a command leaves no checkpoint.
- You are shown whichever of step 1 and step 3 asks for more human involvement, on the order Auto-fix < Review < Manual < Unavailable.
Step 3 is why a fix cannot be Review with only one option: there would be nothing to choose between, and a warning attached to a single action is not a choice. That is the rule the section above turns into Manual.
A Review finding is one Hostveil can fix completely — it will not do so while you are not looking. Applying one takes the same command plus the alternative you want, which is the next section.
Applying a fix
Preview and apply a single finding by its ID:
hostveil fix ssh.rootlogin
Applying always: 1) shows the exact diff or command, 2) backs up the original file to a checkpoint, 3) applies the change, then 4) re-runs that finding's own domain to see whether it is actually gone. You confirm at a [y/N] prompt unless you pass --yes.
That last step exists because "Hostveil wrote the file" and "the finding is gone" are different claims, and only the first used to be established. The result is reported, never guessed at:
| Result | What it means |
|---|---|
Gone gone | The domain was re-checked and no longer reports it. |
Not in force yet pending | The file is correct and the running host has not changed. A Compose file is what Docker was told once, not what it is running from, so the container keeps its old configuration until docker compose up -d recreates it. The re-check reads the file, so all it can establish is that the file is right. The message names the command that puts the change into force. |
Still reported still-present | The change landed and the finding persists, usually because it is not yet in force. A sysctl drop-in applies at the next boot; sshd serves from the config it already loaded until it restarts. Not a failure, and the fix stays rollbackable. |
Unconfirmed unavailable | The re-check could not run or could not cover its ground, for example no Docker or no root. “Couldn't look” is not “still broken”, and it is not “fixed” either. |
Not checked not-run | No re-check was attempted. This is what fix --all reports, for the reason below. |
The middle two rows are worth telling apart. Still reported means the checker can see the old state; not in force yet means it cannot, because the checker reads the file the fix just wrote. The second is the more dangerous one to get wrong, so Hostveil reports it separately rather than calling the finding gone over a host nothing has changed about.
Verification does not change whether the finding is marked fixed. What it can change is the score: a fix that is not in force has changed nothing an attacker can see, so the finding stays charged until it is. That does not depend on the re-check — a fix that needs a restart declares so itself, which is how fix --all reaches the same answer without re-checking anything, and reports how many of the fixes it applied are still waiting.
fix --all skips the per-fix re-check for a different reason: running a checker after each of twenty fixes would run it twenty times, and a batch ends with a rescan anyway.
The names in --json are the ones in the second column.
Choosing a Review alternative
Review findings offer independent alternatives. Pass the 0-based index with --action:
hostveil fix ssh.passwordauth --action 0Index 0 is the recommendation. The alternatives are listed in recommended order in every interface, the TUI and the dashboard preselect the first, and fix --all --review applies it without asking. So it is also what you get if you never read the list.
Recommended here means recommended for applying unattended, which is not always what a person would pick first. The kernel-hardening findings are the case: persisting the setting to a drop-in and applying it to the running kernel with sysctl -w are a genuine choice, and neither is better in general. For a batch there is no choice, because only the drop-in leaves a checkpoint and survives a reboot, so that is index 0 and the other sits behind it.
Disambiguating by service
When the same Compose rule fires for more than one service, narrow it with --service:
hostveil fix compose.ds018 --service dbApplying every safe fix
hostveil fix --allApplies each Auto-fix finding, each with its own checkpoint so any of them can be rolled back individually.
History & rollback
Every applied fix is recorded as a checkpoint. List them newest-first:
hostveil historyEach entry shows a timestamp, the finding ID, a label, and the exact rollback command. Undo one by its checkpoint ID:
hostveil rollback <checkpoint-id>Rollback restores the backed-up file byte-for-byte. Because the TUI, web dashboard, and CLI all go through one shared engine, a fix applied in any of them is listed in history and reversible from any of them. (A few actions are inherently not reversible; history marks those not reversible.)
How long a checkpoint is kept
Hostveil keeps the most recent 200 checkpoints, and never discards one written in the last hour however many arrive at once.
The second rule is the one that matters. Pruning runs on every save, so a count cap on its own would let a single large fix --all delete its own earliest checkpoints: on a host with a hundred Compose services, the fixes applied first would stop being rollbackable the moment they were applied. The hour covers any one run plus the time it takes to notice the host misbehaving and reach for rollback.
Scan snapshots are pruned far more aggressively, because losing one loses a trend line rather than the ability to undo something.
When rollback declines
A checkpoint records the checksum of what the fix wrote. If the file no longer matches it — because you edited it yourself afterwards — rollback stops instead of restoring:
hostveil: /etc/ssh/sshd_config changed after the fix was applied
Rolling back would overwrite it with the pre-fix backup and discard those edits.
Rollback keeps no backup of its own, so this cannot be undone.
Save a copy first, then: hostveil rollback cp-3f9a --forceThis is a refusal, not a failure: nothing was touched. Rollback writes no checkpoint of its own, so restoring over an hour of your own edits would be unrecoverable — the one outcome a recovery feature must never produce. Save a copy of the current file, then pass --force if you still want the backup back.
The dashboard asks the same question in a confirmation dialog rather than a flag. Fixing the same file twice is not treated as an external edit, so fix --all over one Compose file leaves every one of its checkpoints rollback-able.
Fixes that leave nothing to restore
A fix that runs a command — pulling a new image, restarting a service — has no file to back up, so it gets a history record but no restore point. Hostveil says so in the preview, before you apply it, and history marks the entry not reversible. Undoing one is a manual job.