English · 한국어
Hostveil finds the security mistakes on your self-hosted Linux server, explains them in plain language, and fixes them safely. One binary, no config file, no cloud account. Every fix is previewed, backed up, and reversible with one command.
Website · Docs · Changelog · Latest release
Winner of the Grand Prize (Development) at the 2026-1 Ajou University SoftCon.
Most self-hosted boxes — Jellyfin, Nextcloud, a game server, a local LLM, a self-hosted AI agent like OpenClaw or Hermes Agent — run on whatever defaults the install left behind, and one bad default is enough to lose the box. Point Hostveil at one and it checks the places that matter most, gives you a single 0–100 score, describes each problem without jargon, and offers to fix it: preview first, backup, then apply, with rollback for file changes.
The September 11, 2026 rerun measured a deliberately weakened Ubuntu VM with Lynis, Docker's CIS benchmark, a TCP scan from a container on the Docker bridge, and the kernel's listening sockets. The seed runs Nextcloud with PostgreSQL, Jellyfin with Redis, and Portainer with Watchtower, with root SSH login allowed and the firewall and automatic updates disabled.
| Measured by | Before | After Auto + Review fixes and restarts |
|---|---|---|
| Ports answering from the Docker bridge | 6 | 1 |
| CIS Docker Benchmark (pass / warn) | 17 / 15 | 20 / 12 |
| Lynis hardening index | 57 | 79 |
| Hostveil's SSH domain | 10/100 | 100/100 |
| Hostveil score | 28 | 58 |
These are results from source commit 290b4d1, built as v3-dev, on the
configuration in scripts/measure/seed.sh. The
final run record links the raw
output and explains the method. This includes Review actions and service
restarts; it is not the result of fix --all alone.
Rollback restored 25 of 28, across 79 checkpoints, for 89% fidelity.
16 of the 40 reviewed fixes leave nothing to roll back: they are marked
[not reversible]. Three paths retained changes: /etc/shadow after an
account-locking command, and /etc/issue and /etc/issue.net after a package
upgrade rewrote the banners. All checkpoint rollbacks completed without a
reported failure, but the host did not return to its original state.
The container score remained near zero and the CVE score did not improve. Two of Lynis's 3 warnings concern the extra UID-0 account, which Hostveil also reports and leaves for manual handling. The reviewed scan reached only SSH, but the kernel still listed both SSH and Redis as wildcard/routable listeners. A listening socket and a reachable port are different observations.
A separate fresh VM hardened by control.sh, with Hostveil only scanning,
scored 28 → 38. Its external scan reported 7 → 2 reachable ports while
its socket list stayed at 7; the cause was not established in that run.
Full results and limitations are on the
Measured results page. Use
scripts/measure/run.sh -c only on a disposable VM or container. A successful
check allows residue from irreversible Review actions and does not mean every
file was restored.
Most server-hardening tools are auditors — a report, and the fixing left to you. Hostveil closes that loop and applies the fix itself.
- Lynis is a thorough, expert-oriented host auditor. It prints a long list of suggestions but doesn't apply them, and reading the output assumes you already know which ones matter.
- docker-bench-security checks a Docker host against the CIS benchmark. It's container-only, and it reports rather than fixes.
- Trivy scans images and filesystems for known CVEs and misconfigurations. It's excellent at that one job, and Hostveil actually runs Trivy for its CVE domain, but it doesn't look at your SSH config, firewall or accounts.
Hostveil's angle is to merge the host, your containers and image CVEs into a single 0–100 score, explain each finding in plain language, and then apply the fix, with a preview, a backup and one-command rollback. One binary, and no report to interpret.
Findings are named after the domain that found them. The prefix in the second
column is what you pass to hostveil fix and hostveil explain.
| Domain | Findings | What it looks at | Needs |
|---|---|---|---|
| Docker / Compose | compose.* |
Privileged mode, Docker socket mounts, exposed datastores and admin panels, host networking, unsafe bind mounts, shared PID and IPC namespaces, writable root filesystems, missing no-new-privileges, hardcoded secrets, and more. Your Compose files are audited natively, and so are containers started with a plain docker run |
Docker |
| SSH | ssh.* |
Root login, password authentication, empty passwords, weak brute-force limits, login grace time, gateway ports, host-based and keyboard-interactive auth, X11 forwarding. Parsed from sshd_config directly, following Include into sshd_config.d/. Reading stops at the first Match block and the domain reports partial coverage, because a directive that applies to some connections is not a statement about the host |
— |
| Firewall | firewall.* |
Whether ufw, firewalld, nftables or iptables is actually active, and whether published container ports are quietly bypassing it | — |
| Auto-updates | updates.* |
Whether unattended-upgrades (apt) or dnf-automatic (dnf) is enabled | — |
| Exposed services | ports.* |
Host processes listening on a non-loopback address, read from ss. This is the natively installed database, admin panel or app that a Compose audit cannot see |
ss |
| Accounts | accounts.* |
Who can become root, and what stands in their way: non-root accounts carrying root's UID (0), login accounts with an empty password, and accounts a sudo rule lets run anything as root without being asked for one — the rule cloud and VM images ship so the first login works. sudo is asked what each account may actually run, rather than /etc/sudoers being re-parsed |
root (for /etc/shadow and sudo) |
| File permissions | fileperms.* |
Over-permissive modes on /etc/shadow, /etc/passwd, /etc/group, sshd_config, and SSH host private keys |
— |
| AI agent runtimes | agent.* |
Self-hosted agent runtimes, OpenClaw, Hermes Agent and Goose: a gateway reachable off-host, authentication turned off on one that is, unrestricted shell and elevated tools, a disabled sandbox, and loose permissions on the config and the API keys beside it | — |
| Kernel hardening | sysctl.* |
Eight kernel parameters read straight from /proc/sys: the quiet knobs that stop a local foothold from becoming root and a spoofed packet from becoming a route. No sysctl binary needed |
— |
| Docker daemon | dockerd.* |
The daemon underneath your containers. An API served over TCP without TLS client verification is unauthenticated root for anyone who can reach the port; Hostveil also checks for a world-writable socket, who holds the socket's group, and the defaults for no-new-privileges, userns-remap and live-restore | Docker |
| Service hardening | systemd.* |
The units you installed yourself, read as systemd's own effective configuration: whether a service can gain privileges through setuid, write to /usr and /etc, read every user's home directory, or share /tmp with the rest of the host. Distribution units are left to the distribution |
systemd |
| Reverse proxy | proxy.* |
The thing on 443 that everything else is behind. nginx read from /etc/nginx, following include into conf.d and sites-enabled, for deprecated TLS versions and directory listing; Caddy read from /etc/caddy or a container's mounted Caddyfile, for an admin API reachable off loopback — no authentication, and it can replace the whole configuration — and for file_server browse; Traefik read from your Compose files, for the dashboard served in insecure mode — an unauthenticated page listing every backend address behind the proxy, and the setting nearly every Traefik tutorial tells you to add |
— |
| Proxmox VE host | proxmox.* |
The hypervisor every guest trusts, read only on a host with /etc/pve: a web interface on 8006 that accepts a login from any address, root@pam logging in with a password alone, and an enterprise repository enabled without a subscription — so Proxmox's own packages silently stop updating while Debian's keep arriving. The datacenter firewall is recognised by the firewall domain rather than counted twice |
a Proxmox VE host |
| Single-node Kubernetes | kube.* |
One box running one k3s or k0s node, with k3s's settings resolved across all four of its layers (config.yaml, its drop-ins, the service environment file, the command line): a cluster-admin kubeconfig or join token readable by every account, anonymous-auth=true on the API server or kubelet, and Secrets stored unencrypted |
k3s or k0s |
| Image CVEs (optional) | cve.* |
Known vulnerabilities in the images your Compose services run | Trivy |
If Docker or Trivy is missing, those domains are skipped and the score is renormalized over the ones that ran, so a partial scan never comes back as a misleadingly perfect result.
Hostveil can report 183 findings across those domains, and 128 of them
carry a fix — 75 Hostveil will apply unattended, 53 only after you have read
the diff. The rest are Manual on purpose, and each one is named in the register
in internal/fix/register.go with the reason
Hostveil will not touch it. Manual by decision, never Manual because nobody
looked — a test fails the build on the difference.
Hostveil runs as root and edits configuration files on servers people depend on. That is a lot to ask, so this is what stands behind it.
- There is more test code in this repository than product code. Not a ratio anyone targets — it is what auditing an operating system honestly costs, and a test enforces the inequality rather than a number.
internal/docs/fails the build when a published page drifts from the code. Every domain table, every finding ID in a screenshot, every figure on the measured-results page — checked against what the program actually does.- 5 fuzz targets run nightly, five minutes each, over the config editors and parsers that rewrite your files.
- Every pull request drives the real binary end to end — scan, fix, history, rollback, rescan — inside a seeded Debian container.
- Every release target is cross-compiled on every pull request, so a break appears while it is still a pull request.
demo/is a Vagrant VM seeded to be vulnerable, so you can watch a hardening tool edit a server before you let it near one of yours.
None of that answers whether the fixes change anything real. That is what the
harness in scripts/measure/ is for — see the section
above.
curl -fsSL https://hostveil.seolcu.com/install.sh | bashOr install a native package from the latest release:
sudo apt install ./hostveil_<version>_linux_amd64.deb # or dnf install ./hostveil-<version>.x86_64.rpmEither route puts the same binary at the same path (/usr/bin/hostveil).
Docker and iproute2 are recommended but not required — without them, those
domains report N/A instead of failing the scan. To build it yourself you need
Go 1.26 or later:
go install github.com/seolcu/hostveil/cmd/hostveil@latestTrivy is optional, for image CVE scanning. hostveil update and
hostveil uninstall handle upgrades and removal themselves, however you
installed it. Releases are checksummed and carry a signed build provenance
attestation and an SBOM — see Installation
for verifying a download before you run it.
hostveil # interactive TUI (default on a terminal)
hostveil scan # print a scored report (add -v for details, --json for JSON)
hostveil fix <id> # preview, then apply the fix for one finding
hostveil fix --all # apply every safe (Auto) fix at once
hostveil fix --all --review # and the Review ones too, after reading what they are
hostveil rollback <id> # undo a previously applied fix
hostveil history # list applied fixes and their rollback IDs
hostveil history --scans # the score of every saved scan, oldest first
hostveil diagnostics # collect version/OS/crash/scan info to attach to a bug report
hostveil fleet web1 db1 # scan several hosts over SSH, scores side by side (--sudo, --tui)
hostveil explain <id> # explain a finding (add --ai for an AI second opinion)
hostveil export --format markdown --output report.md # or json, sarif, docx, pdf
hostveil ai-context "a personal media server, want fast patches over stability"
hostveil advise --ai # which fixable findings suit this host, and which don't
hostveil serve # web dashboard on 127.0.0.1:8787 (open the printed URL)
hostveil update # update this binary the way it was installed
hostveil uninstall # remove it, keeping your checkpointsSome checks read root-owned files, and applying a fix needs root, so Hostveil
re-runs itself under sudo when it has to — the same password prompt as
sudo hostveil. version and help never prompt. For scripts and CI, set
HOSTVEIL_NO_SUDO=1; the root-only domains are skipped with a message saying
so.
hostveil scan reports what it found in its exit status, so a scheduled check
does not have to parse any output.
| Code | Meaning |
|---|---|
0 |
The scan ran and found nothing High. |
1 |
At least one unfixed High finding. |
3 |
A detection domain failed outright, so the scan covered less of the host than it should have. |
HOSTVEIL_NO_SUDO=1 hostveil scan --json > report.json || echo "action needed"Exit code 3 is worth wiring up: a failed domain contributes no findings,
so without it a blind scan and a healthy host look identical to a consumer
that only reads the exit status. --sarif writes SARIF 2.1.0 for GitHub code
scanning and most CI systems, and --only/--skip narrow a run to some
domains. See CLI reference for the
full flag set and the SARIF field mapping.
Other commands exit 0 on success, 1 on failure and 2 on a usage error.
HOSTVEIL_DEBUG=1 traces every command Hostveil runs against the host to
stderr — what ran, how long it took, and whether it failed. Command output
is never logged, deliberately: docker inspect prints the resolved
environment of every container, and a trace that included it would leak
credentials as a matter of routine.
HOSTVEIL_DEBUG=1 hostveil scanSee Troubleshooting for what Degraded, a declined rollback, an empty history and exit code 3 mean.
Every finding is classified, so the tool never changes anything blindly:
| Kind | Meaning | Hostveil can apply it |
|---|---|---|
| Auto | One clearly correct change. You still see it first. | Yes |
| Review | Two or more independent alternatives; you pick one. | Yes |
| Manual | Nothing safe to automate, so Hostveil explains what to do instead. | No |
| Unavailable | A real problem with no fix in existence yet, such as a CVE with no published patch. | No |
fix --all applies only the Auto ones. fix --all --review applies the
Review ones too, each through its first alternative.
Applying a fix is not always a change the host has seen, and the score
knows the difference. A Compose file is read when the container is
recreated, a systemd drop-in when the unit is reloaded, a sysctl drop-in at
the next boot — until then the finding stays charged and its row is marked
PEND, and each fix names the command that puts it in force.
A finding is Auto only when all three hold: it's reversible (a checkpoint restores exactly what changed — a fix that runs a command has nothing to store, so it's never Auto), recoverable in practice (nothing that can cut off your own access, like SSH auth or firewall policy, even if the edit itself reverses cleanly), and unambiguous (exactly one correct remediation). Failing that makes a finding Review when there are alternatives to choose from, and Manual when there is only one thing to do and no way to make it safe. The full procedure is on Fixing & rollback.
Applying a fix always shows you the exact diff or command, backs the original
file up to a checkpoint, and only then applies it. hostveil rollback restores
that backup. All three interfaces run on one engine, so a fix applied in any of
them can be undone from any of them.
The score is one number between 0 and 100, built so it cannot flatter a host. Each detection domain is an axis with a fixed share of the 100; a domain that did not run is reported N/A and the remaining caps are renormalized over it, so a host without Docker is not quietly handed free points. If nothing ran at all, the score is N/A rather than 100.
Inside an axis, each finding erodes what is left instead of subtracting a fixed number of points — the tenth finding still costs something, unlike a model that sums penalties and clamps at zero, where two findings exhaust the axis and everything after is free.
Severity answers how urgent a finding is, not how bad it could turn out to be:
| Level | What it means | Takes |
|---|---|---|
| High | Reachable or usable right now, from off the host, by someone holding nothing. | Half of what the axis has left |
| Medium | A boundary that gives way to a foothold, a guessed credential, or a local account. | An eighth |
| Low | Defence in depth. No known path today. | A sixteenth |
Two adjustments sit on top: the same finding ID repeated on one axis is damped harmonically, so four services missing the same setting count as one mistake made four times rather than four mistakes; and a finding nothing can fix yet (an unpatched CVE) counts a quarter, since charging it in full would pin a well-maintained host's axis at 0. The overall score also gets a name in every interface: 80 and above is in good shape, 50 to 79 middling, 25 to 49 exposed, below 25 wide open.
The full model, including why each axis cap is the size it is, is on the Scoring page.
- TUI — keyboard-driven, and what you get when you run
hostveilon a terminal. - Web —
hostveil serve, a localhost-bound dashboard. It prints a URL carrying a one-off access token; open that exact URL. Loopback keeps the dashboard off the network but not away from other accounts on the same machine, and it runs as root. For remote access, forward the port over SSH. - CLI — scriptable
scan/fix/rollbackwith--jsonoutput.
All three are thin layers over one shared engine, so they behave identically.
The TUI and the dashboard share five color themes: onedark (the default),
gruvbox, nord, catppuccin and tokyonight, and six screen arrangements
(console is the default). Press t or l in the TUI to pick one and have
it remembered, use the pickers in the dashboard's status bar, or set
--theme/--layout or HOSTVEIL_THEME/HOSTVEIL_LAYOUT explicitly.
The TUI and scan can also draw their status markers from a patched
Nerd Font instead of plain Unicode: --glyphs nerd,
or HOSTVEIL_GLYPHS=nerd. This is opt-in rather than detected, since a
terminal cannot be asked what font it is using. See
Interfaces for theme
screenshots and Nerd Font details.
hostveil explain <id> --ai adds a plain-language explanation from an AI
model. The default provider is Ollama, running on your own machine, so
nothing leaves your host. Set HOSTVEIL_AI_PROVIDER=anthropic or openai to
use an external API instead — useful on a machine too small to run a local
model — with ANTHROPIC_API_KEY or HOSTVEIL_OPENAI_API_KEY for
credentials; see hostveil explain --help for the full list of variables.
Only a finding's title, description, suggested fix, and service name are ever
sent — never raw evidence. AI is strictly advisory and never applies changes.
Every explanation, score and fix works with no AI at all.
The one request Hostveil makes on its own is a once-a-day check of the
GitHub releases page, to notice that a newer version exists. It sends nothing
about your host and the answer is cached; HOSTVEIL_NO_UPDATE_CHECK=1 turns
it off. Nothing else in Hostveil contacts the network unless you ask it to —
update, and Trivy pulling vulnerability data during a CVE scan.
A new detection domain is a bigger change than it looks — a new axis, funded by taking weight from every existing one — so it ships as a named release, not a line in a patch note. Solid is shipped, dashed is planned but not scheduled, and the boxed list at the bottom is not on this line at all — those items were never headed anywhere to fall off of.
graph TD
v1["3.1.0: AI agent runtimes"] --> v2["3.6.0: Kernel hardening"]
v2 --> v3["3.8.0: Docker daemon"]
v3 --> v4["3.9.0: Service hardening"]
v4 --> v5["3.22.0: Reverse proxy"]
v5 --> v6["3.30.0: Proxmox VE host"]
v6 --> v7["3.31.0: Single-node Kubernetes"]
v7 --> v8["3.32.0: SSH fleet view"]
v8 -.-> n1["More agent runtimes"]
n1 -.-> n2["More reverse proxies"]
n2 -.-> n3["Wider distro coverage"]
n3 -.-> e4(["More languages"])
subgraph np["Not planned, on purpose"]
x1["Plugin / rule system"]
x2["AI applies fixes alone"]
x3["Hosted dashboard"]
x4["Automatically fixing Manual findings"]
end
classDef shipped fill:#16231f,stroke:#d2bc74,stroke-width:2px,color:#f5ead2
classDef next fill:#101816,stroke:#d2bc74,stroke-width:2px,stroke-dasharray:6 3,color:#f5ead2
classDef exploring fill:#0b1512,stroke:#9aa89f,stroke-width:1px,stroke-dasharray:2 3,color:#c8bea7
classDef notplanned fill:#101816,stroke:#516057,stroke-width:1px,stroke-dasharray:4 3,color:#9aa89f
classDef npTitle fill:#07100f,stroke:#31413b,color:#9aa89f
class v1,v2,v3,v4,v5,v6,v7,v8 shipped
class n1,n2,n3 next
class e4 exploring
class x1,x2,x3,x4 notplanned
class np npTitle
The reasoning behind every item, and why each "not planned" one is ruled out rather than just missing, is on the Roadmap page.
go build ./cmd/hostveil
go test ./...See docs/DEVELOPMENT.md for full setup (per-platform demo VM, repo layout, contributing checklist).

