Hostveil / Docs / What it checks

What it checks

Hostveil checks the highest-impact paths that actually get self-hosters breached, then merges everything into one 0–100 score. Each finding has a stable ID of the form domain.rule — use it with explain, fix, and rollback.

Domains at a glance

DomainPrefixNeeds
Docker / Composecompose.Docker
SSHssh.
Firewallfirewall.
Auto-updatesupdates.
Exposed servicesports.ss
Accountsaccounts.root (for /etc/shadow)
File permissionsfileperms.
AI agent runtimesagent.OpenClaw or Hermes installed
Kernel hardeningsysctl.
Docker daemondockerd.Docker
Service hardeningsystemd.systemd
Reverse proxyproxy.nginx or Docker
Image CVEs (optional)cve.Trivy

Missing Docker or Trivy? Those domains are skipped cleanly and the score is renormalized, so you are never handed a misleadingly perfect result for a scan that did not run.

Across those domains Hostveil can report 175 findings, and 128 of them carry a fix — 75 it will apply unattended, 53 only after you have read the diff. The rest are Manual on purpose, and every one is named in the register in internal/fix/register.go with the reason Hostveil will not act: deleting an account cannot be undone from a checkpoint, removing a Docker socket mount breaks the tool that needs it. Manual by decision, never Manual because nobody looked — a test fails the build on the difference, and the counts in this paragraph are computed from the registry rather than typed here.

How the 175 findings resolve

Auto
75
Review
53
Manual
47

Severity & the score

Hostveil grades a finding by how urgent it is, not by how bad it might turn out to be. A scanner reading your config cannot tell you how bad a container running as root is — that depends entirely on what the container does. What it can tell you is how much stands between an attacker and the problem right now, and that is what these three levels measure.

LevelWhat it meansTakes
HIGHReachable or usable right now, from off the host, by someone holding nothing. An open service with no authentication, a login with no password, a container that has already been handed host root. Nothing has to be broken first.1/2 of what remains
MEDIUMA boundary that gives way to a foothold, a guessed credential, or a local account. The attacker needs something they do not have yet, and this is what makes getting it worthwhile.1/8
LOWDefence in depth. No known path today; it narrows what a future compromise reaches.1/16

Three levels, not four, and the definitions above are the whole taxonomy. The names are ordinary because a name has one job here — carrying the order — and it has to do that on a chip, in one line of --json, and in a SARIF level, where there is no room for anything else. What each level means is the column beside it, and it is a claim about your host rather than an opinion about the finding.

Changed in 3.11

Hostveil used to have four levels — Critical / High / Medium / Low — inherited from Trivy so a CVE's published rating could pass straight through. Vulnerabilities are rolled up per image now, so what reaches a finding is one level for the whole image rather than a rating per CVE, and the fourth level was left asking a question a config file cannot answer.

HIGH is exactly what Critical and High were together, so scan's exit code and the SARIF export mean what they always did. If you are comparing against Trivy's own output, note that a vulnerability Trivy calls CRITICAL lands here as HIGH: Hostveil has three levels and Trivy has four, and the top of one is not the top of the other by name.

Because each finding takes a share of what is left rather than a fixed amount, an axis never bottoms out after two findings — the tenth issue still costs you something, and a host with thirty problems scores below one with three. Scoring is the whole model: the arithmetic, a worked example from a real scan, and what the number cannot tell you.

A finding Hostveil marks Unavailable — a vulnerability with no upstream patch — costs exactly what its severity says, the same as one you could act on. Not being able to fix something yet does not make it less real, so there is no discount for it; a host that pulls unpatched images will see that reflected in its Vulnerabilities axis.

What each domain is worth

The axes are weighted by how much they matter to a self-hosted server, and the weights sum to 100. When a domain does not run, its weight leaves the total and the rest are renormalized.

AxisWeight
Container exposure13
SSH hardening13
Vulnerabilities9
Host firewall8
Exposed services7
AI agent runtimes7
Reverse proxy6
Account hygiene7
Auto-updates7
Docker daemon7
Service hardening6
File permissions5
Kernel hardening5

When a domain can't be fully checked

"I couldn't look" and "there was nothing there" produce the same empty list, and they mean opposite things. Hostveil never lets one pass for the other, so every domain reports how much ground it actually covered:

StateWhat it meansEffect on the score
DoneThe domain was checked end to end.Scored normally.
SkippedA dependency is missing — no Docker, no Trivy, no ufw.Shown as N/A and excluded; the other axes are renormalized.
PartialSome of the ground was covered and some was not — Trivy could scan three images out of five.Scored on what was seen, and flagged. The TUI writes 42~, the dashboard writes 42~ in amber.
FailedThe checker could not examine its ground at all.Excluded like Skipped, and the reason is shown.

Both UIs print the reason above the findings list — "Container partial: cannot inspect containers started outside Compose", "CVEs skipped: Trivy not installed". An excluded axis is never scored 100: a scan that could not see your images is not a scan that found your images clean. This is why running without root produces a lower, not a higher, score than running with it.

hostveil scan exits non-zero when any unfixed finding is HIGH — useful as a CI or cron gate.

SSH ssh.

Parsed natively from sshd_config, following Include directives into sshd_config.d/. That matters on Debian and Ubuntu, which put the Include at the top of the file: sshd keeps the first value it obtains for each keyword, so a drop-in wins over the lines below it. Reading these needs root, which hostveil obtains by auto-elevating with sudo. A fix edits the file the winning directive actually lives in.

Reading stops at the first Match. sshd applies what follows only to connections that match, so a directive there does not describe the host the way a global one does — but not reading it is a gap, not a clean bill, so the domain is reported Degraded with the file named. Match Address 0.0.0.0/0 followed by PasswordAuthentication yes puts passwords back for every connection there is, and a scan that called that host fully audited would be claiming something it never looked at.

IDWhat it flagsSeverityFix
ssh.emptypasswordsEmpty passwords are permittedHIGHAuto-fix
ssh.rootloginRoot login with a password is allowedHIGHReview
ssh.passwordauthPassword authentication is allowedMEDIUMReview
ssh.gatewayportsRemote-forwarded ports are exposed to the networkMEDIUMReview
ssh.hostbasedauthHost-based authentication is enabledMEDIUMReview
ssh.kbdinteractiveInteractive password prompts survive PasswordAuthentication noMEDIUMReview
ssh.permituserenvironmentUsers can supply SSH session environment variablesMEDIUMReview
ssh.permittunnelUsers can create SSH network tunnelsMEDIUMReview
ssh.maxauthtriesToo many auth attempts per connectionLOWAuto-fix
ssh.logingracetimeUnauthenticated connections are held open too longLOWAuto-fix
ssh.x11forwardingX11 forwarding is enabledLOWAuto-fix
ssh.allowtcpforwardingTCP forwarding is enabledLOWReview
ssh.clientalivecountmaxDead SSH sessions survive too many keepalive failuresLOWAuto-fix
ssh.clientaliveintervalIdle SSH sessions have no server-side timeoutLOWAuto-fix
ssh.fingerprinthashHost-key fingerprints use a legacy hashLOWAuto-fix
ssh.ignorerhostsLegacy rhosts trust is not explicitly ignoredLOWAuto-fix
ssh.loglevelSSH authentication logging is not verboseLOWAuto-fix
ssh.maxsessionsOne SSH connection may open many sessionsLOWReview
ssh.printlastlogUsers are not shown their previous loginLOWAuto-fix
ssh.strictmodesSSH does not enforce ownership and mode checksLOWAuto-fix
ssh.tcpkeepaliveSSH trusts spoofable TCP keepalivesLOWAuto-fix
ssh.usednsSSH performs unnecessary reverse DNS lookupsLOWAuto-fix
ssh.allowagentforwardingSSH agent forwarding is enabledLOWReview

Docker / Compose compose.

A native audit of your Compose files — no external scanner required. Containers started with plain docker run are audited too, read from the daemon rather than a file. Those findings are always Manual: there is no file for a fix to edit, so Hostveil tells you what to change when you recreate the container instead of offering a button that could not work.

What counts as exposed. A published port is treated as reachable from every interface when its host address is 0.0.0.0, ::, or not written at all. That last case is the one that catches people: "6379:6379" names no address, and Docker binds it to every interface, so it is exposed by this rule and by the daemon. Only an explicit loopback address such as "127.0.0.1:6379:6379" is not, and a port with no host side at all is not published and never counts.

The rule is applied to the merged project, which is what docker compose config prints, not to any single file. Compose appends port mappings across files rather than replacing them, so a base file binding a service to loopback and an override republishing it leaves the service exposed. Reading the base alone would call that host clean.

IDWhat it flagsSeverityFix
compose.ds016Docker socket mounted into a containerHIGHManual
compose.ds018Datastore exposed on all interfacesHIGHAuto-fix
compose.ds001Container runs in privileged modeHIGHManual
compose.ds019Admin panel exposed on all interfacesHIGHAuto-fix
compose.dr001Container uses host network modeHIGHManual
compose.ds017Sensitive host path mounted read-writeHIGHManual
compose.ds005Adds a dangerous Linux capabilityHIGHManual
compose.ds020Shares the host PID namespaceHIGHManual
compose.dr005Hardcoded secret in the environmentHIGHManual
compose.ds006Missing no-new-privileges hardeningMEDIUMAuto-fix
compose.ds021Shares the host IPC namespaceMEDIUMManual
compose.ds009Container runs as rootMEDIUMManual
compose.dr002Port published on all interfacesMEDIUMAuto-fix
compose.ds008No restart policy setLOWAuto-fix
compose.ds022Container filesystem is writableLOWManual
compose.ds023Seccomp filtering is disabledHIGHManual
compose.ds024AppArmor confinement is disabledMEDIUMManual
compose.ds026Host user namespace is usedMEDIUMManual
compose.ds010No memory limit setLOWReview
compose.ds012No healthcheck definedLOWManual
compose.dr004Loads secrets from an env_fileLOWManual

Firewall firewall.

IDWhat it flagsSeverityFix
firewall.inactiveNo active host firewall (ufw, firewalld, nftables, or iptables)HIGHReview
firewall.default-allowA firewall is running but its default inbound policy accepts everythingHIGHReview
firewall.docker-bypassPublished container ports are reachable despite an active ufwHIGHManual

Auto-updates updates.

IDWhat it flagsSeverityFix
updates.disabledAutomatic security updates are not enabled (apt unattended-upgrades or dnf-automatic)MEDIUMReview
updates.pending-securitySecurity updates are available but not installedHIGH / MEDIUMReview
updates.reboot-requiredInstalled updates need a reboot to take effectHIGHManual
updates.process-accountingProcess accounting is not installedLOWReview
updates.sysstatSystem activity accounting is not installedLOWReview
updates.auditdThe Linux audit daemon is not installedMEDIUMReview
updates.debsumsInstalled package files cannot be verifiedLOWReview
updates.rkhunterNo rootkit scanner is installedLOWReview
updates.pam-pwqualityPAM has no password-quality moduleMEDIUMReview
updates.apt-show-versionsPatch inventory tooling is not installedLOWReview
updates.sysstat-disabledSystem activity collection is disabledLOWAuto-fix
updates.apt-listchangesAPT changelog review is not installedLOWReview
updates.pam-tmpdirPAM sessions share global temporary directoriesLOWReview
updates.fail2banRepeated authentication failures are not automatically blockedMEDIUMReview

Exposed services ports.

Reads the host's listening TCP sockets with ss and flags services bound to a non-loopback address — the natively-installed database, admin panel, or app that a Compose-file audit can't see because it isn't a container. Loopback-only binds are ignored; SSH is expected and never flagged here.

IDWhat it flagsSeverityFix
ports.exposed-datastoreA datastore (Postgres, MySQL, Redis, MongoDB, …) is reachable from the networkHIGHManual
ports.exposed-adminAn admin/management UI (e.g. Portainer) is reachable from the networkHIGHManual
ports.exposedOther services listen on a non-loopback address and no host firewall is activeLOWManual
ports.redis-bindRedis is configured to listen beyond loopbackMEDIUMReview
ports.redis-protected-modeRedis protected mode is disabledMEDIUMAuto-fix
ports.redis-disable-configRedis exposes the CONFIG commandMEDIUMReview

Accounts accounts.

One question asked three ways: who can become root, and what stands in their way. The first two answers come out of /etc/passwd and /etc/shadow, parsed natively. Reading /etc/shadow needs root, which hostveil obtains by auto-elevating with sudo; without it, the empty-password check is skipped and the UID-0 check still runs.

The third answer does not come from a file. sudo rules are a layered configuration with aliases, group specs that may resolve through LDAP rather than /etc/group, and include ordering — so Hostveil asks sudo itself what each account may actually run, rather than re-deriving it from /etc/sudoers. That needs root too: without it the domain reports the gap instead of reporting that nobody was found.

IDWhat it flagsSeverityFix
accounts.uid0A non-root account has root's UID (0)HIGHManual
accounts.emptypasswordA login account has an empty passwordHIGHReview
accounts.sudo-nopasswdA sudo rule lets an account run anything as root without being asked for a passwordMEDIUMManual
accounts.duplicate-uidMultiple accounts share one UIDMEDIUMManual
accounts.weak-password-hashA login account uses a weak password hashMEDIUMManual
accounts.password-roundsPassword hashing rounds are not hardenedLOWAuto-fix
accounts.default-umaskNew files default to broad permissionsLOWAuto-fix
accounts.local-bannerThe local login warning does not discourage unauthorized accessLOWReview
accounts.remote-bannerThe remote login warning does not discourage unauthorized accessLOWReview
accounts.password-agingPassword aging defaults are too permissiveLOWAuto-fix
accounts.core-dumpsCore dumps are not explicitly disabledLOWAuto-fix

Reverse proxy proxy.

The component this audience is most likely to have and least likely to have configured: everything else here sits behind whatever answers on 443. Two surfaces, because there are two ways people run one — nginx from a package, read out of /etc/nginx and following include into conf.d and sites-enabled; and Traefik as a container, read out of the Compose files the container domain already discovers.

Three things are deliberately not checked. A proxy serving plain HTTP, because a vhost whose only job is to redirect to HTTPS and a proxy behind something that already terminates TLS are both correct and neither is distinguishable from the config. Missing security headers, because they are worth having and are not what gets somebody breached. And anything about certificates, because expiry and issuer are facts about the network rather than about a file.

IDWhat it flagsSeverityFix
proxy.traefik-api-insecureTraefik's API and dashboard are served with no authentication, listing every router and backend address behind the proxyHIGHManual
proxy.tls-deprecated-protocolsssl_protocols still offers TLS 1.0 or 1.1, which RFC 8996 deprecated and every current browser refusesMEDIUMManual
proxy.directory-listingautoindex on makes every directory with no index file browsableMEDIUMManual
proxy.no-scan-jailfail2ban is installed but its nginx-botsearch jail, built to catch repeated requests for nonexistent or known-vulnerable paths, is not enabledMEDIUMReview

File permissions fileperms.

Stats a curated set of security-critical files and flags any whose permission bits are looser than they should ever be.

IDWhat it flagsSeverityFix
fileperms.shadow/etc/shadow is more permissive than 0640HIGHAuto-fix
fileperms.passwd/etc/passwd is writable by non-root usersHIGHAuto-fix
fileperms.group/etc/group is writable by non-root usersHIGHAuto-fix
fileperms.hostkeyAn SSH host private key is readable beyond rootHIGHAuto-fix
fileperms.gshadow/etc/gshadow is too permissiveHIGHAuto-fix
fileperms.sudoers/etc/sudoers is too permissiveHIGHAuto-fix
fileperms.sudoers-dropinsA sudoers drop-in is too permissiveHIGHAuto-fix
fileperms.cronA system cron definition is writable by non-rootHIGHAuto-fix
fileperms.systemd-unitsA system service unit is writable by non-rootHIGHAuto-fix
fileperms.docker-configRoot's Docker credentials are too permissiveHIGHAuto-fix
fileperms.grub-configGRUB configuration permissions are too broadHIGHAuto-fix
fileperms.grub2-configGRUB2 configuration permissions are too broadHIGHAuto-fix
fileperms.at-allowThe at allowlist is too permissiveMEDIUMAuto-fix
fileperms.at-denyThe at denylist is too permissiveMEDIUMAuto-fix
fileperms.cron-allowThe cron allowlist is too permissiveMEDIUMAuto-fix
fileperms.cron-denyThe cron denylist is too permissiveMEDIUMAuto-fix
fileperms.crontabThe system crontab is too permissiveHIGHAuto-fix
fileperms.passwd-backupThe passwd backup is writable by non-rootHIGHAuto-fix
fileperms.ownerOne of the files above is not owned by root — permissions are only half of who can read itHIGHManual
fileperms.sshd-configsshd_config is writable by non-root usersMEDIUMAuto-fix
fileperms.compilerThe system compiler is available to every local userLOWReview

Kernel hardening sysctl.

Read directly from /proc/sys — no sysctl binary needed. These are the quiet knobs whose safe value is the same on essentially every server: none of them is the hole an attacker comes in through, each is what stops a foothold from becoming root or a spoofed packet from becoming a route. A parameter this kernel does not have (Yama not built in, say) is simply skipped.

Unambiguous settings are Auto-fix: Hostveil writes one reversible drop-in under /etc/sysctl.d and deliberately leaves the running kernel untouched. The change takes effect at the next boot, or when you explicitly run sysctl --system. Rollback removes or restores that file and refuses if it has since been edited.

Router-, relay-, and multi-homing-dependent settings remain Review. For example, strict reverse-path filtering can break asymmetric routing and disabling Proxy ARP breaks a host deliberately acting as a relay. Those retain the persistent and immediate alternatives so the operator makes the topology-dependent choice.

net.ipv4.ip_forward is deliberately not audited: Docker, WireGuard, Tailscale exit nodes, and virtualization hosts all legitimately enable it, and Hostveil cannot tell those apart from an accident.

IDWhat it flagsSeverityFix
sysctl.ptrace-scopeAny process can debug its siblings and read their memoryMEDIUMAuto-fix
sysctl.syncookiesTCP SYN-flood protection is offMEDIUMAuto-fix
sysctl.accept-redirectsICMP redirects can rewrite this host's routingMEDIUMAuto-fix
sysctl.protected-linksSymlink/hardlink race protections are disabledMEDIUMAuto-fix
sysctl.kptr-restrictKernel pointer addresses are visible to all usersLOWAuto-fix
sysctl.dmesg-restrictAny user can read the kernel logLOWAuto-fix
sysctl.sysrqMagic SysRq is fully enabledLOWReview
sysctl.rp-filterSource-address spoofing filter is off (strict and loose both pass)LOWReview
sysctl.accept-source-routeSource-routed packets are acceptedMEDIUMAuto-fix
sysctl.send-redirectsThe host sends ICMP redirectsMEDIUMReview
sysctl.suid-dumpablePrivileged processes may write core dumpsMEDIUMAuto-fix
sysctl.protected-fifosFIFO protections are weakMEDIUMAuto-fix
sysctl.protected-regularRegular-file protections are weakMEDIUMAuto-fix
sysctl.unprivileged-bpfUnprivileged users can load BPF programsMEDIUMAuto-fix
sysctl.perf-eventsPerformance events are exposed to usersMEDIUMAuto-fix
sysctl.icmp-broadcastsBroadcast pings are acceptedLOWAuto-fix
sysctl.bogus-icmp-errorsBogus ICMP errors are not ignoredLOWAuto-fix
sysctl.log-martiansSuspicious source addresses are not loggedLOWAuto-fix
sysctl.aslrFull address-space randomization is disabledMEDIUMAuto-fix
sysctl.core-uses-pidCore dump names omit the process IDLOWAuto-fix
sysctl.ctrl-alt-delCtrl-Alt-Delete immediately reboots the hostLOWAuto-fix
sysctl.bpf-jit-hardenBPF JIT hardening is incompleteMEDIUMAuto-fix
sysctl.tty-ldisc-autoloadTTY use can autoload line disciplinesMEDIUMAuto-fix
sysctl.ipv6-accept-redirects-allIPv6 redirects are accepted globallyMEDIUMAuto-fix
sysctl.ipv6-accept-redirects-defaultNew IPv6 interfaces accept redirectsMEDIUMAuto-fix
sysctl.ipv6-accept-source-route-allIPv6 source routing is accepted globallyMEDIUMAuto-fix
sysctl.ipv6-accept-source-route-defaultNew IPv6 interfaces accept source routingMEDIUMAuto-fix
sysctl.ipv6-send-redirectsThe host sends IPv6 redirectsLOWAuto-fix
sysctl.ipv4-default-accept-redirectsNew IPv4 interfaces accept redirectsMEDIUMAuto-fix
sysctl.ipv4-default-rp-filterNew IPv4 interfaces lack source validationLOWReview
sysctl.proxy-arp-allProxy ARP is enabled globallyMEDIUMReview
sysctl.proxy-arp-defaultNew interfaces enable Proxy ARPMEDIUMReview
sysctl.multicast-forwardingIPv4 multicast forwarding is enabledLOWReview
sysctl.bootp-relayBOOTP relay is enabledLOWReview
sysctl.tcp-rfc1337TCP TIME-WAIT protection is disabledLOWAuto-fix
sysctl.module-dccpThe optional DCCP protocol is availableLOWReview
sysctl.module-sctpThe optional SCTP protocol is availableLOWReview
sysctl.module-rdsThe optional RDS protocol is availableLOWReview
sysctl.module-tipcThe optional TIPC protocol is availableLOWReview
sysctl.module-usbstorageThe USB storage driver is availableLOWReview

Docker daemon dockerd.

The Compose audit judges what your services declare and the CVE scan judges the images they run. This domain judges the daemon underneath both — the part of the stack that actually holds root.

It is worth being blunt about the worst finding here, because it is the worst finding Hostveil has. A daemon listening on a TCP port without TLS client verification is root on the host for anyone who can reach that port: no password, no vulnerability, one HTTP request that starts a container with your filesystem mounted inside it. Membership in the socket's group is the same authority from a local account, and unlike sudo it asks for nothing and logs nothing.

Three sources, and they disagree. Docker's settings live in /etc/docker/daemon.json, in flags on the service unit, and in the running daemon — and after an edit without a restart those are different answers. So the daemon defaults are read from docker info, which is what the daemon actually loaded rather than what someone wrote down; the socket and TLS settings are read from the file and the unit together, because docker info does not report them at all; and ss supplies the endpoint you can go and verify, without ever deciding whether a finding exists.

Every finding here is Manual, on purpose. The checker reads the running daemon, but a fix would edit a file the daemon does not read again until it restarts — and restarting Docker stops every container on the host. A fix that marked the finding resolved and raised your score while changing nothing an attacker can see would be worse than no fix, so the how-to-fix text carries the exact change and you choose the moment.

IDWhat it meansSeverityFix
dockerd.api-unauthenticatedThe Docker API is served over TCP with no TLS client verification — unauthenticated root for anyone who can reach the portHIGHManual
dockerd.socket-world-writableEvery local account can connect to the Docker socket, and so become rootHIGHManual
dockerd.api-tls-unverifiedThe API is encrypted but accepts any client — TLS without tlsverify authenticates the server, not the callerHIGHManual
dockerd.group-membersAccounts other than root hold the socket's group, which is root-equivalent with no password prompt and no audit recordMEDIUM / HIGHManual
dockerd.no-new-privilegesContainers can still gain privileges through setuid binariesMEDIUMManual
dockerd.userns-remapUser namespaces are not remapped, so container root is host rootLOWManual
dockerd.live-restoreRestarting the daemon stops every container, which is why daemon updates get deferredLOWManual

dockerd.group-members is MEDIUM when the accounts holding the group are people, and HIGH when any of them is a service account — one with a system UID or no interactive login. Nobody deliberately grants a CI runner or a monitoring agent the ability to become root, and a credential that never logs in is one nobody is watching.

A loopback-only TCP socket is not flagged: it reaches nothing the unix socket did not, and it is the documented way to put a proxy in front of the daemon. A daemon running rootless has userns-remap and group-members suppressed. Its API finding stays HIGH: rootless bounds the damage to that user's containers rather than the host, which is a difference the description reports, but it does not make the port any harder to reach. live-restore is suppressed on a swarm node, where Docker does not support it.

Service hardening systemd.

Roughly half of a self-hosted server is not in a container. The Compose domain reads what your containers declare — privileged, running as root, no-new-privileges off — and a service started by a unit file has exactly the same decisions made about it. Until this domain existed, nothing looked at them.

systemd already knows the answers. systemctl show reports the effective value of each property after the unit file, every drop-in, and every default have been merged — so what Hostveil reads is what the service will actually run with, not what some file says.

Only your own units are audited. A unit counts if its file lives under /etc/systemd/system or /usr/local/lib/systemd/system — the ones you wrote or installed by hand. Your distribution hardens its own units on its own schedule, and second-guessing them means taking on maintenance of somebody else’s service. Reporting on them would also bury your services under dozens of findings about software you did not choose, which is how a domain stops being read.

IDWhat it meansSeverityFix
systemd.no-new-privilegesThe service can gain privileges through a setuid binary — the usual way a foothold inside a service becomes a foothold outside itLOW / MEDIUMReview
systemd.protect-systemThe service can write to /usr, /boot and /etc; one that can edit /etc owns every login on the hostMEDIUM / LOWManual
systemd.protect-homeThe service can read /home, /root and /run/user — everyone’s SSH keys, cloud credentials and password databasesMEDIUM / LOWManual
systemd.private-tmpThe service shares /tmp with every other process, which is the ground symlink races live onLOWManual
systemd.private-devicesThe service can access host devicesMEDIUMManual
systemd.protect-kernel-tunablesThe service can change kernel tunablesMEDIUMManual
systemd.protect-kernel-modulesThe service can alter kernel modulesMEDIUMReview
systemd.protect-control-groupsThe service can alter control groupsMEDIUMManual
systemd.protect-kernel-logsThe service can access kernel logsLOWReview
systemd.protect-clockThe service can change system clocksLOWReview
systemd.restrict-suid-sgidThe service can create setuid filesMEDIUMReview
systemd.restrict-namespacesThe service can create namespacesMEDIUMManual
systemd.lock-personalityThe service can change execution personalityLOWReview
systemd.memory-deny-write-executeThe service can create writable executable memoryMEDIUMManual

Two severities, and which one applies depends on the account. no-new-privileges is MEDIUM on a service running as somebody other than root and LOW on one running as root — a root service can already do anything, so the setuid path it closes buys little there. The two filesystem protections go the other way for the same reason: they are worth most exactly where no-new-privileges is worth least.

Every finding here is Manual, on purpose. The drop-in is two lines and Hostveil could write it — but writing it is not the same as knowing it is safe. ProtectSystem=full breaks a service that writes under /usr; PrivateTmp=yes breaks two services that hand each other files through /tmp. Neither failure shows up until the next restart, which on a self-hosted box is the next reboot — and the service that does not come back is the one holding your data. So the how-to-fix carries the exact path and the exact two lines, and you pick the moment to restart and watch.

A property this systemd does not implement is reported as nothing at all, and nothing is not evidence that a protection is off — those are left alone rather than turned into a finding.

AI agent runtimes agent.

Self-hosted AI agents — OpenClaw and Hermes Agent — run a network gateway and keep API keys on disk in your home directory. Misconfigured, the worst case is someone reaching that gateway and driving an agent that can read your files and run commands as you.

This domain is skipped entirely unless one of those runtimes is actually installed, so a host that has never run an agent is not scored on it. Hostveil looks up home directories in /etc/passwd and stats only the paths listed below — it never walks your home.

These projects ship their own config auditors (openclaw security audit), which cover their settings far more thoroughly than Hostveil tries to. What Hostveil adds is the part those tools cannot see: whether the gateway is actually listening on a reachable address, whether a firewall stands behind it, and whether your credential files are readable by other accounts on the box.

IDWhat it flagsSeverityFix
agent.auth-disabledAn exposed gateway that accepts requests with no authenticationHIGHManual
agent.gateway-exposedThe gateway is bound to a network-reachable addressHIGHManual
agent.secret-exposedA credentials file or directory is readable beyond its ownerHIGHAuto-fix
agent.exec-unrestrictedThe agent may run shell commands with no approval stepHIGHReview
agent.elevated-enabledElevated (host-level) command execution is enabledHIGHAuto-fix
agent.sandbox-offAgent tools run unsandboxed, directly on the hostHIGHManual
agent.control-ui-insecureThe control UI has its auth or device-identity checks disabledHIGHAuto-fix
agent.config-permsA config or state path is more permissive than upstream ships itMEDIUMAuto-fix
agent.ssrf-private-networkThe agent browser may reach private-network addressesMEDIUMAuto-fix

Most of the config findings can now be fixed. OpenClaw's config is JSON5 and carries your own comments, so Hostveil edits it by replacing exactly the bytes of the one value and leaving the rest of the file alone — then re-reads what it wrote and refuses to save it unless nothing else changed. A one-key change stays a one-line diff, and your comments stay where you put them.

Three are still Manual, each for its own reason. For agent.sandbox-off Hostveil knows off is wrong and does not know which mode turns the sandbox on, and a guessed value is not a fix. For agent.auth-disabled the safe posture is no key at all — OpenClaw fails closed when it is absent — and Hostveil replaces values rather than deleting them. And rebinding an exposed gateway can cut you off from the agent you administer remotely. Hermes settings are left alone throughout: they may come from the config, from .env, from a systemd unit, or from a docker -e flag, and a finding cannot tell which is in force.

Only the two permission findings are auto-fixable, and both are a chmod that only ever removes access, checkpointed so hostveil rollback restores the previous mode exactly.

Image CVEs cve. (optional)

When Trivy is installed, Hostveil scans the images your Compose services run for known Critical, High, and Medium vulnerabilities. Findings are reported per image, not per vulnerability — a single image routinely ships hundreds of CVEs, and they all share one remediation.

Each scanned image produces up to two findings, split by what you can actually do about them:

IDWhat it flagsSeverityFix
cve.outdated-imageVulnerabilities that are already fixed upstreamWorst in that groupReview
cve.unpatched-imageVulnerabilities nobody has published a fix forWorst in that groupUnavailable

Each finding names the severity counts and its worst few CVEs. The complete list of vulnerability IDs is in the finding's evidence — use hostveil scan --json to read it.

Why not one finding per CVE

Trivy tells you a package must reach version 3.0.11-1~deb12u2. That is an OS package version inside the image, not an image tag, and there is no mapping from one to the other — so a per-CVE fix would have to invent one. There is nothing you can do about a single CVE that you would not do about all of them at once, so the image is the honest unit. Re-pulling the tag you already chose needs no version mapping, and it promises only that it re-resolves the tag, not that any particular CVE is gone. Images pinned by digest get no fix, because pulling a digest cannot change anything.

Honest about "no patch yet"

Vulnerabilities with no available patch get their own finding rather than being folded into the fixable ones or dropped. Aggregating them away would let an image whose vulnerabilities are all unfixed disappear from the report entirely, and Hostveil would be claiming a host is clean when it is only stuck. The state is modelled as a first-class Unavailable instead of pretending it can be fixed.

What each fix actually does

Every finding above with an Auto-fix or Review in its Fix column has a fix registered for it in internal/fix, and this is exactly what that fix does — generated from the current build, so it can never say something the code doesn't do. A Review finding with more than one entry lists every alternative it offers; the first is the one preselected everywhere, including fix --all --review.

Container

compose.dr002 Auto-fix

Bind published port to 127.0.0.1

✓ Once recreated, the service stops being reachable from the network at all — only processes on this host can reach it, closing off whatever guessing or scanning the open port currently invites.

⚠ Once app is recreated, it is reachable only from this host. If you access it from another machine, use an SSH tunnel, VPN, or reverse proxy. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds006 Auto-fix

Add security_opt no-new-privileges:true

✓ A process that breaks into this container can no longer gain more privilege than it started with via a setuid binary — one of the more common container-escape stepping stones, closed.

⚠ This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds008 Auto-fix

Set restart: unless-stopped

✓ The service comes back on its own after a crash, an OOM kill, or a host reboot instead of silently staying down until someone notices.

⚠ This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds010 Review
  • Limit app to 1g — typical application container (recommended)

    ✓ Gives the container a memory ceiling, so a leak or a runaway process inside it gets OOM-killed and restarted instead of exhausting the host's memory and taking every other service down with it.

    ⚠ Too low a limit gets the container OOM-killed under load. Start generous, watch docker stats, and tighten later. This is a file edit, so it is fully reversible. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

  • Limit app to 512m — small service (proxy, exporter, static site)

    ✓ Gives the container a memory ceiling, so a leak or a runaway process inside it gets OOM-killed and restarted instead of exhausting the host's memory and taking every other service down with it.

    ⚠ Too low a limit gets the container OOM-killed under load. Start generous, watch docker stats, and tighten later. This is a file edit, so it is fully reversible. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

  • Limit app to 2g — database or JVM service

    ✓ Gives the container a memory ceiling, so a leak or a runaway process inside it gets OOM-killed and restarted instead of exhausting the host's memory and taking every other service down with it.

    ⚠ Too low a limit gets the container OOM-killed under load. Start generous, watch docker stats, and tighten later. This is a file edit, so it is fully reversible. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds018 Auto-fix

Bind published port to 127.0.0.1

✓ Once recreated, the service stops being reachable from the network at all — only processes on this host can reach it, closing off whatever guessing or scanning the open port currently invites.

⚠ Once app is recreated, it is reachable only from this host. If you access it from another machine, use an SSH tunnel, VPN, or reverse proxy. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds019 Auto-fix

Bind published port to 127.0.0.1

✓ Once recreated, the service stops being reachable from the network at all — only processes on this host can reach it, closing off whatever guessing or scanning the open port currently invites.

⚠ Once app is recreated, it is reachable only from this host. If you access it from another machine, use an SSH tunnel, VPN, or reverse proxy. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

SSH

ssh.allowagentforwarding Auto-fix

Disable SSH agent forwarding

✓ Stops a compromised host from riding a forwarded SSH agent to authenticate elsewhere as the user, using credentials that were never actually stored here.

⚠ Existing workflows that hop through this host using a forwarded agent will stop working.

ssh.allowtcpforwarding Auto-fix

Disable SSH TCP forwarding

✓ Closes SSH's local, remote, and dynamic port forwarding — a common pivot and tunnel-out vector for anyone who gets a session on this host.

⚠ Existing local, remote, and dynamic SSH tunnels will stop working.

ssh.clientalivecountmax Auto-fix

Limit unanswered SSH keepalives

✓ Bounds how long a dead or hung session is held open, freeing the session slot instead of leaving it reserved indefinitely.

ssh.clientaliveinterval Auto-fix

Probe idle SSH clients every five minutes

✓ Detects and disconnects idle or dead clients on a schedule, instead of leaving a session open for as long as the network happens to keep the TCP connection alive.

⚠ Long-running idle sessions may be disconnected when their clients stop responding.

ssh.emptypasswords Auto-fix

Disable empty passwords

✓ Closes SSH login with a blank password — the one credential check an attacker never even has to guess.

ssh.fingerprinthash Auto-fix

Use SHA-256 host-key fingerprints

✓ Uses a stronger hash for the host-key fingerprint shown to users and tooling verifying this server's identity.

ssh.gatewayports Auto-fix

Bind remote-forwarded ports to loopback only

✓ Stops an ssh -R remote-forwarded port becoming reachable from other machines, closing a route around whatever this host's own firewall is doing.

⚠ If you rely on ssh -R tunnels being reachable from other machines, this closes them to loopback.

ssh.hostbasedauth Auto-fix

Disable host-based authentication

✓ Removes a trust-based login path that bypasses per-user keys entirely.

⚠ If any user logs in via host-based trust rather than their own key, this removes that path.

ssh.ignorerhosts Auto-fix

Ignore legacy rhosts trust files

✓ Ignores legacy .rhosts trust files, closing a decades-old authentication bypass that has no place on a hardened host.

ssh.kbdinteractive Auto-fix

Disable keyboard-interactive authentication

✓ Closes the same password-equivalent prompt path PasswordAuthentication closes, for the keyboard-interactive method some clients use instead.

⚠ PAM-based one-time codes (2FA prompts) also use this mechanism — keep it enabled if your logins go through one.

ssh.logingracetime Auto-fix

Lower LoginGraceTime to 60 seconds

✓ Shortens the window an unauthenticated connection can hold a login slot open, reducing exposure to connection-slot exhaustion.

ssh.loglevel Auto-fix

Log SSH key fingerprints

✓ Logs the key fingerprint used on every login, giving the operator forensic evidence of exactly which key authenticated — not just that a login succeeded.

ssh.maxauthtries Auto-fix

Lower MaxAuthTries to 3

✓ Throttles how many credential guesses a single SSH connection gets before it is dropped, slowing a brute-force attempt against this host specifically.

ssh.maxsessions Auto-fix

Limit multiplexed SSH sessions

✓ Caps multiplexed sessions per connection, limiting how much one compromised or stolen connection can do at once.

⚠ Clients opening more than two sessions over one SSH connection will be refused.

ssh.passwordauth Auto-fix

Disable password authentication

✓ Removes password-guessing as an SSH attack surface entirely — only a held key gets in from here on.

⚠ Make sure key-based login works BEFORE applying this, or you may lock yourself out of SSH.

ssh.permittunnel Auto-fix

Disable SSH tun/tap tunnels

✓ Disables SSH's own VPN-like tun/tap tunneling, removing a route around whatever network controls this host is behind.

⚠ Existing SSH VPN or tun/tap workflows will stop working.

ssh.permituserenvironment Auto-fix

Disable user-supplied SSH environments

✓ Stops a user's ~/.ssh/environment overriding the server process's own environment — a known way to hijack LD_PRELOAD or similar into a login session.

⚠ Login automation that relies on ~/.ssh/environment will stop receiving those variables.

ssh.printlastlog Auto-fix

Show the previous login

✓ Shows the previous login on every connect, so a user has a chance to notice a login they did not make.

ssh.rootlogin Review
  • Allow root only with an SSH key (prohibit-password) (recommended)

    ✓ Keeps key-based root access working while removing the one thing root logins are actually dangerous for: a guessable or stolen password.

    ⚠ Keep a working key for root, or use a sudo user instead.

  • Disable root login entirely (no)

    ✓ Removes direct root login over SSH entirely, so compromising this host's authentication means compromising a named, sudo-capable user instead of the one account every attacker already knows the name of.

    ⚠ Make sure another user can log in and use sudo before applying this.

ssh.strictmodes Auto-fix

Enforce SSH login-file ownership

✓ Refuses to honor login files (keys, config) with loose ownership or permissions, closing a local tampering vector.

ssh.tcpkeepalive Auto-fix

Disable TCP keepalives for SSH

✓ Stops relying on spoofable TCP keepalives for liveness, leaving ClientAliveInterval's encrypted check as the one source of truth for whether a session is still alive.

⚠ Use ClientAliveInterval for encrypted liveness checks; dead connections may otherwise take longer to disappear from intermediate network devices.

ssh.usedns Auto-fix

Disable SSH reverse-DNS lookups

✓ Removes a DNS lookup from the login path — both a minor timing/DoS surface and a spoofing vector, for a lookup the login never actually needed.

ssh.x11forwarding Auto-fix

Disable X11 forwarding

✓ Removes an X11 forwarding channel that malware or an attacker with a foothold could otherwise ride out of the session.

Firewall

firewall.default-allow Review

Allow SSH on 22/tcp, then set ufw's default inbound policy to deny

✓ Flips a firewall that is already running from allow-by-default to deny-by-default, closing every port nothing has explicitly opened on a host where a firewall was already assumed to be doing that job.

⚠ Every inbound port except 22/tcp stops being reachable the moment this runs, including anything a container publishes. There is no rollback checkpoint — exec fixes are not file-backed — so undoing it means ufw default allow incoming by hand.

firewall.inactive Review

Allow SSH on 22/tcp, then enable ufw with a default-deny inbound policy

✓ Turns on default-deny, so only the ports hostveil confirmed sshd is actually listening on stay reachable — everything else stops accepting connections from off this host.

⚠ Every inbound port except 22/tcp stops being reachable the moment this runs, including anything a container publishes. There is no rollback checkpoint — exec fixes are not file-backed — so undoing it means ufw disable by hand.

Updates

updates.apt-listchanges Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.apt-show-versions Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.auditd Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.debsums Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.disabled Review

Install and enable dnf-automatic

✓ Once enabled, security patches for every installed package arrive on their own instead of depending on someone remembering to run apt/dnf.

⚠ Installs dnf-automatic and enables its timer. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.fail2ban Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.pam-pwquality Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.pam-tmpdir Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.pending-security Review

Refresh apt metadata and fully upgrade installed packages

✓ Applies whatever security patches are already sitting unapplied right now — closes today's already-known gaps rather than waiting for the next automatic run.

⚠ Upgrading packages can restart the services they belong to, including sshd and the Docker daemon — and a container whose compose file has no restart policy in force stays down until docker compose up -d brings it back. A kernel or libc update may also need a reboot to actually take effect. There is no rollback checkpoint; undoing this means downgrading the affected packages by hand.

updates.process-accounting Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.rkhunter Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.sysstat Review

Install and enable auditd

✓ Installs a kernel-level audit trail of security-relevant actions — file access, privilege changes — that survives a process exiting or an application log being cleared.

⚠ Installs auditd and enables its service. There is no rollback checkpoint; undoing this means removing the package by hand.

updates.sysstat-disabled Auto-fix

Set ENABLED to true

✓ Turns on the data collection the sysstat package already has installed, so historical resource data actually starts accumulating instead of the package sitting present but silent.

CVEs

cve.outdated-image Review
  • Pull the new image and recreate app now (recommended)

    ✓ Re-resolves the tag to whatever it currently points at, which is how the vendor ships fixes for a floating-tag image — it may include a patch for the CVE that triggered this finding, though hostveil cannot guarantee it does, since the same tag can also move for unrelated reasons. On a host where staying current matters more than staying unchanged, this is the fastest way to find out.

    ⚠ This recreates the container: the service goes down briefly and comes back on a different image. There is no rollback checkpoint: exec fixes are not file-backed, so Hostveil cannot undo this. Note the current image ID (docker compose -f /opt/example/docker-compose.yml images) before applying, so you can pin it back if the new one misbehaves. The new image may also carry unrelated upstream changes since you last pulled — on a host you are not trying to disturb, that is a real cost to weigh against a fix that is not guaranteed to land anyway.

  • Download the new image only; recreate app on your own schedule

    ✓ Downloads the new image without touching the running container, so there is a chance to inspect it — its digest, its changed packages, its release notes — before deciding whether pulling it in is worth the risk to a service that is stable today.

    ⚠ This changes nothing that is running: the image is downloaded but the container keeps using the old one until you recreate it, and the finding will still be reported until then. There is no rollback checkpoint: exec fixes are not file-backed, so Hostveil cannot undo this.

Ports

ports.redis-bind Auto-fix

Bind Redis to loopback

✓ Redis ships with no authentication by default; binding it to loopback means the only way to reach it is already being on this host — closes it to the network entirely.

⚠ Restarting Redis can interrupt clients; validate this setting first.

ports.redis-disable-config Auto-fix

Disable the Redis CONFIG command

✓ Removes remote CONFIG SET/GET, closing the well-known Redis-to-remote-code-execution chain that writes a webshell or SSH key to disk via CONFIG SET dir/dbfilename.

⚠ Restarting Redis can interrupt clients; validate this setting first.

ports.redis-protected-mode Auto-fix

Enable Redis protected mode

✓ Turns on Redis's own built-in refusal to serve remote clients when no password is set — a second, independent barrier behind the bind address, so a firewall or compose slip elsewhere doesn't leave Redis exposed anyway.

⚠ Restarting Redis can interrupt clients; validate this setting first.

Accounts

accounts.core-dumps Auto-fix

Set a hard core-size limit of zero

✓ Stops a crashed privileged process writing a core file to disk — the easiest place a live secret in memory (a password, a key) ends up sitting in plaintext after a crash.

accounts.default-umask Auto-fix

Set the default umask to 027

✓ New files a user creates default to unreadable by other accounts on the box, closing the easiest way a stray world-readable file leaks something it shouldn't.

accounts.emptypassword Review

Lock alice with passwd -l

✓ Closes the one account whose password prompt currently succeeds for anyone who tries it, including a stranger who has never touched this host before.

⚠ If alice is your only way to reach this machine locally (console, su) and it has no other credential, locking it removes that access. Confirm you have another route in — an SSH key, a different sudo-capable account — before applying. The remote SSH path is already closed by ssh.emptypasswords; this only closes the local one.

accounts.local-banner Auto-fix

Append the standard access warning

✓ Puts a legal notice in front of anyone who logs into this host, local or remote, which several jurisdictions require before monitoring or logging a session can be used as evidence against an intruder.

⚠ Have counsel or the system owner approve login-banner wording for this organization.

accounts.password-aging Auto-fix

Harden password aging defaults

✓ Forces credentials to actually expire and blocks changing a password back to itself immediately, so a leaked password has a shelf life instead of being valid forever.

accounts.password-rounds Auto-fix

Harden password hashing rounds

✓ Raises the SHA-512 rounds used to hash new or changed passwords, slowing an offline dictionary or brute-force attempt against a stolen /etc/shadow by roughly the same factor.

accounts.remote-banner Auto-fix

Append the standard access warning

✓ Puts a legal notice in front of anyone who logs into this host, local or remote, which several jurisdictions require before monitoring or logging a session can be used as evidence against an intruder.

⚠ Have counsel or the system owner approve login-banner wording for this organization.

File perms

fileperms.at-allow Auto-fix

Tighten /etc/shadow to 0640

✓ Keeps control of who is allowed to schedule future one-off commands to root alone.

fileperms.at-deny Auto-fix

Tighten /etc/shadow to 0640

✓ Keeps control of who is blocked from scheduling future one-off commands to root alone.

fileperms.compiler Auto-fix

Tighten /etc/shadow to 0640

✓ Removes the system compiler from every local account's reach by default, taking away a convenient way to build exploit code from a local foothold.

fileperms.cron Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account scheduling a command to run as root through a system cron definition.

fileperms.cron-allow Auto-fix

Tighten /etc/shadow to 0640

✓ Keeps control of who is allowed to schedule recurring cron jobs to root alone.

fileperms.cron-deny Auto-fix

Tighten /etc/shadow to 0640

✓ Keeps control of who is blocked from scheduling recurring cron jobs to root alone.

fileperms.crontab Auto-fix

Tighten /etc/shadow to 0640

✓ Closes an avoidable persistence path: the system crontab can run commands as root, and this keeps it restricted to what the scheduler itself needs.

fileperms.docker-config Auto-fix

Tighten /etc/shadow to 0640

✓ Stops other accounts on the host reading root's Docker registry credentials, which could otherwise pull private images or publish under this host's identity.

fileperms.group Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account adding itself to a privileged group — sudo, docker — by editing the group database directly.

fileperms.grub-config Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account reading or rewriting the bootloader configuration, which controls what runs before any of the host's normal access controls are active.

fileperms.grub2-config Auto-fix

Tighten /etc/shadow to 0640

✓ Same protection as the GRUB config, for the GRUB2 variant.

fileperms.gshadow Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account reading group passwords and membership data that /etc/gshadow is supposed to keep to root and the shadow group alone.

fileperms.hostkey Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account reading this server's SSH private host key — the one thing that would let them impersonate this host to anyone connecting.

fileperms.passwd Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account editing the account database directly, closing a path to adding an account or handing itself UID 0.

fileperms.passwd-backup Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account planting a change in the passwd backup that becomes a live credential the next time it is restored.

fileperms.shadow Auto-fix

Tighten /etc/shadow to 0640

✓ Stops any account other than root (and the shadow group) reading password hashes off disk, closing the offline-cracking path entirely.

fileperms.sshd-config Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account weakening the SSH server's own config (re-enabling root login or password auth) to open a way back in.

fileperms.sudoers Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account granting itself root by editing sudo's own policy file.

fileperms.sudoers-dropins Auto-fix

Tighten /etc/shadow to 0640

✓ Same protection as the main sudoers file, for a drop-in carrying the exact same authority.

fileperms.systemd-units Auto-fix

Tighten /etc/shadow to 0640

✓ Stops a non-root account replacing the command a systemd unit runs the next time it starts as root.

AI agents

agent.config-perms Auto-fix

Tighten /etc/shadow to 0640

✓ Stops other local accounts on this host reading the agent runtime's own configuration, which can carry API keys and tool permissions.

agent.control-ui-insecure Auto-fix

Set kernel.kptr_restrict to 1

✓ Closes the agent's control UI to unauthenticated access, so reaching it requires being someone the operator actually let in.

agent.elevated-enabled Auto-fix

Set kernel.kptr_restrict to 1

✓ Stops the agent running with elevated or administrative privileges by default, so a prompt-injected or misused tool call can't act with more authority than the task ever needed.

agent.exec-unrestricted Auto-fix

Set kernel.kptr_restrict to 1

✓ Stops the agent executing shell commands at all without an explicit human decision each time — closes the most direct path from "the agent was tricked by its input" to "a shell command ran on this host."

agent.secret-exposed Auto-fix

Tighten /etc/shadow to 0640

✓ Stops other local accounts reading a secret file the agent runtime keeps in its state directory.

agent.ssrf-private-network Auto-fix

Set kernel.kptr_restrict to 1

✓ Stops the agent's own tool calls reaching internal or private network addresses on the operator's behalf — closes the classic SSRF pivot from "the agent fetched a URL" to "the agent probed the internal network."

Kernel

sysctl.accept-redirects Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-accept-redirects.conf

✓ Stops the kernel trusting ICMP redirects, closing an easy way for something on the local network to quietly re-route this host's outbound traffic through itself.

sysctl.accept-source-route Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-accept-source-route.conf

✓ Stops the kernel honoring source-routed packets, closing a way to make traffic take a path chosen by whoever sent it rather than by this network's own routing.

sysctl.aslr Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-aslr.conf

✓ Turns on full address space layout randomization, making a memory-corruption bug substantially harder to turn into a reliable exploit.

sysctl.bogus-icmp-errors Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-bogus-icmp-errors.conf

✓ Stops the kernel logging bogus ICMP error responses as if real, reducing log noise and a minor spoofing-detection blind spot.

sysctl.bootp-relay Review
  • Persist it: write /etc/sysctl.d/60-hostveil-bootp-relay.conf (recommended)

    ✓ Stops this host forwarding BOOTP/DHCP broadcasts between networks it has no business relaying for.

  • Apply it now: kernel.kptr_restrict

    ✓ Stops this host forwarding BOOTP/DHCP broadcasts between networks it has no business relaying for.

    ⚠ Changes the running kernel immediately and has no rollback checkpoint.

sysctl.bpf-jit-harden Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-bpf-jit-harden.conf

✓ Hardens the BPF JIT compiler's output against the spraying techniques used to turn a BPF bug into kernel code execution.

sysctl.core-uses-pid Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-core-uses-pid.conf

✓ Includes the process ID in a core dump's filename, so a crash does not silently overwrite an earlier one a security investigation might still need.

sysctl.ctrl-alt-del Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-ctrl-alt-del.conf

✓ Stops Ctrl-Alt-Del triggering an instant reboot, closing a trivial denial-of-service available to anyone with physical or console access.

sysctl.dmesg-restrict Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-dmesg-restrict.conf

✓ Stops unprivileged users reading the kernel log, which can leak addresses and details useful for a kernel-level exploit.

sysctl.icmp-broadcasts Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-icmp-broadcasts.conf

✓ Stops this host answering ICMP echo requests sent to a broadcast address, closing its use as an amplifier in a Smurf-style denial-of-service attack.

sysctl.ipv4-default-accept-redirects Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-ipv4-default-accept-redirects.conf

✓ Same protection as accept-redirects, as the IPv4 default template new interfaces inherit.

sysctl.ipv4-default-rp-filter Review
  • Persist it: write /etc/sysctl.d/60-hostveil-ipv4-default-rp-filter.conf (recommended)

    ✓ Same protection as rp-filter, as the default template.

  • Apply it now: kernel.kptr_restrict

    ✓ Same protection as rp-filter, as the default template.

    ⚠ Changes the running kernel immediately and has no rollback checkpoint.

sysctl.ipv6-accept-redirects-all Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-ipv6-accept-redirects-all.conf

✓ Same protection as accept-redirects, for IPv6, applied to every interface.

sysctl.ipv6-accept-redirects-default Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-ipv6-accept-redirects-default.conf

✓ Same protection as accept-redirects, for IPv6, as the default template new interfaces inherit.

sysctl.ipv6-accept-source-route-all Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-ipv6-accept-source-route-all.conf

✓ Same protection as accept-source-route, for IPv6, every interface.

sysctl.ipv6-accept-source-route-default Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-ipv6-accept-source-route-default.conf

✓ Same protection as accept-source-route, for IPv6, as the default template.

sysctl.ipv6-send-redirects Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-ipv6-send-redirects.conf

✓ Same protection as send-redirects, for IPv6.

sysctl.kptr-restrict Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-kptr-restrict.conf

✓ Hides kernel pointer addresses from unprivileged reads of /proc, taking away information an attacker would otherwise use to build a kernel exploit.

sysctl.log-martians Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-log-martians.conf

✓ Logs packets carrying an impossible source or destination address, giving visibility into spoofing or misconfiguration attempts that would otherwise pass silently.

sysctl.module-dccp Auto-fix

Persist a modprobe install block

✓ Removes the kernel's ability to load the DCCP module at all — a rarely-used protocol on an ordinary self-hosted server, and one that has shown up repeatedly as a local attack vector when left loadable.

⚠ Confirm this host does not use dccp before applying.

sysctl.module-rds Auto-fix

Persist a modprobe install block

✓ Removes the kernel's ability to load the RDS module at all — the same reasoning as DCCP: rarely used here, and a repeat source of local kernel bugs when loadable.

⚠ Confirm this host does not use dccp before applying.

sysctl.module-sctp Auto-fix

Persist a modprobe install block

✓ Removes the kernel's ability to load the SCTP module at all — the same reasoning as DCCP: rarely used here, and a repeat source of local kernel bugs when loadable.

⚠ Confirm this host does not use dccp before applying.

sysctl.module-tipc Auto-fix

Persist a modprobe install block

✓ Removes the kernel's ability to load the TIPC module at all — the same reasoning as DCCP: rarely used here, and a repeat source of local kernel bugs when loadable.

⚠ Confirm this host does not use dccp before applying.

sysctl.module-usbstorage Auto-fix

Persist a modprobe install block

✓ Removes the kernel's ability to load the USB mass-storage driver, closing a common way to pull data off or introduce malware onto a physically-accessible server via a USB drive.

⚠ Confirm this host does not use dccp before applying.

sysctl.multicast-forwarding Review
  • Persist it: write /etc/sysctl.d/60-hostveil-multicast-forwarding.conf (recommended)

    ✓ Stops this host relaying multicast traffic between interfaces it has no business routing for.

  • Apply it now: kernel.kptr_restrict

    ✓ Stops this host relaying multicast traffic between interfaces it has no business routing for.

    ⚠ Changes the running kernel immediately and has no rollback checkpoint.

sysctl.perf-events Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-perf-events.conf

✓ Restricts performance-counter access so an unprivileged user can't use it to leak kernel addresses or run a side-channel attack against other processes.

sysctl.protected-fifos Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-protected-fifos.conf

✓ Stops a process writing into a FIFO it does not own in a world-writable sticky directory — the same class protected-links closes for symlinks.

Persist it: write /etc/sysctl.d/60-hostveil-protected-links.conf

✓ Stops a process following a symlink it does not own in a world-writable sticky directory, closing a classic local privilege-escalation and file-clobbering trick.

sysctl.protected-regular Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-protected-regular.conf

✓ Stops a process writing into a regular file it does not own in a world-writable sticky directory, for the same reason.

sysctl.proxy-arp-all Review
  • Persist it: write /etc/sysctl.d/60-hostveil-proxy-arp-all.conf (recommended)

    ✓ Stops the kernel answering ARP on behalf of other hosts across interfaces, closing an easy way to bridge or spoof between network segments this host sits on.

  • Apply it now: kernel.kptr_restrict

    ✓ Stops the kernel answering ARP on behalf of other hosts across interfaces, closing an easy way to bridge or spoof between network segments this host sits on.

    ⚠ Changes the running kernel immediately and has no rollback checkpoint.

sysctl.proxy-arp-default Review
  • Persist it: write /etc/sysctl.d/60-hostveil-proxy-arp-default.conf (recommended)

    ✓ Same protection as proxy-arp-all, as the default template.

  • Apply it now: kernel.kptr_restrict

    ✓ Same protection as proxy-arp-all, as the default template.

    ⚠ Changes the running kernel immediately and has no rollback checkpoint.

sysctl.ptrace-scope Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-ptrace-scope.conf

✓ Restricts ptrace to a process's own children, closing a well-known technique for one compromised process to read another's memory or inject code into it.

sysctl.rp-filter Review
  • Persist it: write /etc/sysctl.d/60-hostveil-rp-filter.conf (recommended)

    ✓ Drops packets whose source address could not have arrived on the interface they came in on, closing a common IP-spoofing technique.

  • Apply it now: kernel.kptr_restrict

    ✓ Drops packets whose source address could not have arrived on the interface they came in on, closing a common IP-spoofing technique.

    ⚠ Changes the running kernel immediately and has no rollback checkpoint.

sysctl.send-redirects Review
  • Persist it: write /etc/sysctl.d/60-hostveil-send-redirects.conf (recommended)

    ✓ Stops this host sending ICMP redirects itself, which it should not be doing unless it is actually a router — removes a route-injection tool a compromised host could use against its neighbors.

  • Apply it now: kernel.kptr_restrict

    ✓ Stops this host sending ICMP redirects itself, which it should not be doing unless it is actually a router — removes a route-injection tool a compromised host could use against its neighbors.

    ⚠ Changes the running kernel immediately and has no rollback checkpoint.

sysctl.suid-dumpable Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-suid-dumpable.conf

✓ Stops a crashed setuid process writing a core dump that can contain whatever secrets were in its memory at the moment it crashed.

sysctl.syncookies Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-syncookies.conf

✓ Enables SYN cookies, so a SYN-flood cannot exhaust the connection queue and deny the host's legitimate services.

sysctl.sysrq Review
  • Persist it: write /etc/sysctl.d/60-hostveil-sysrq.conf (recommended)

    ✓ Restricts the SysRq magic-key functions to the safe subset, closing the ones that can dump memory or force a reboot from an unprivileged local session.

  • Apply it now: kernel.kptr_restrict

    ✓ Restricts the SysRq magic-key functions to the safe subset, closing the ones that can dump memory or force a reboot from an unprivileged local session.

    ⚠ Changes the running kernel immediately and has no rollback checkpoint.

sysctl.tcp-rfc1337 Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-tcp-rfc1337.conf

✓ Protects against TIME-WAIT assassination, so an off-path attacker cannot reset a connection sitting in TIME_WAIT by spoofing packets at it.

sysctl.tty-ldisc-autoload Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-tty-ldisc-autoload.conf

✓ Stops the kernel auto-loading obscure TTY line disciplines on request, closing a local privilege-escalation path specific line-discipline modules have shipped in the past.

sysctl.unprivileged-bpf Auto-fix

Persist it: write /etc/sysctl.d/60-hostveil-unprivileged-bpf.conf

✓ Removes unprivileged users' ability to load BPF programs into the kernel, closing a well-documented local privilege-escalation and kernel-exploit vector.

Services

systemd.lock-personality Auto-fix

Set LockPersonality=yes for app

✓ Stops the unit switching to an alternate execution personality, closing an old technique for bypassing ASLR.

⚠ Needing an alternate execution personality is rare outside emulation and compatibility layers. An ordinary service is unaffected; one that needs one fails at the next restart.

systemd.no-new-privileges Auto-fix

Set NoNewPrivileges=yes for app

✓ Closes the setuid escalation path out of this unit — a compromised process cannot gain more privilege than the service already had, the same protection compose.ds006 gives a container.

⚠ Closes the setuid path out of this service. A service that deliberately escalates — anything calling a setuid helper — stops working, and it stops at the next restart rather than now.

systemd.protect-clock Auto-fix

Set ProtectClock=yes for app

✓ Stops the unit changing the system or hardware clock, closing off a way a compromised service could hide its tracks by tampering with timestamps or break time-based authentication.

⚠ Only time-sync daemons (chronyd, ntpd, systemd-timesyncd) legitimately need to change the system or hardware clock. A service that is not one of those stops being able to, at the next restart.

systemd.protect-kernel-logs Auto-fix

Set ProtectKernelLogs=yes for app

✓ Stops the unit reading /dev/kmsg directly, closing an information-leak path that can hand an attacker kernel addresses useful for a further exploit.

⚠ Only a service that reads kernel logs directly — a diagnostics tool, an agent reading /dev/kmsg — needs this off. Most services never touch it.

systemd.protect-kernel-modules Auto-fix

Set ProtectKernelModules=yes for app

✓ Stops the unit loading or removing kernel modules itself, closing a direct path to running arbitrary code in kernel space from what should be an ordinary service.

⚠ Only a service that loads or removes kernel modules itself at runtime — rather than modules already loaded at boot — needs this off.

systemd.restrict-suid-sgid Auto-fix

Set RestrictSUIDSGID=yes for app

✓ Stops the unit creating new setuid or setgid files, closing a persistence and privilege-escalation path a compromised service could otherwise leave behind for later.

⚠ Only a service that itself creates setuid or setgid files — a package manager, an installer — needs this off. An ordinary network daemon does not create such files and is unaffected.

Proxy

proxy.no-scan-jail Review
  • Enable it (fail2ban's own default: 10 minute ban) (recommended)

    ✓ Once fail2ban reloads, an IP that repeatedly requests nonexistent or known-vulnerable paths gets banned automatically — no more finding it and banning it yourself.

    ⚠ Takes effect on the next fail2ban-client reload, not immediately. A shared or NAT'd IP address could ban more than the one visitor responsible for it. This only ever affects HTTP(S) traffic — never SSH, and never the operator's own access to the host.

  • Enable it with a longer ban (1 week)

    ✓ Once fail2ban reloads, an IP that repeatedly requests nonexistent or known-vulnerable paths gets banned automatically — no more finding it and banning it yourself.

    ⚠ Takes effect on the next fail2ban-client reload, not immediately. A shared or NAT'd IP address could ban more than the one visitor responsible for it. This only ever affects HTTP(S) traffic — never SSH, and never the operator's own access to the host.