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, Hermes Agent or Goose installed
Kernel hardeningsysctl.—
Docker daemondockerd.Docker
Service hardeningsystemd.systemd
Reverse proxyproxy.nginx, Caddy or Docker
Proxmox VE hostproxmox.a Proxmox VE host
Single-node Kuberneteskube.k3s or k0s
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 184 findings, and 176 of them carry a fix — 75 it will apply unattended, 101 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: a leaked secret has to be rotated by whoever issued it, and a healthcheck written without knowing what the service exposes marks a working container unhealthy. A fix that is risky but mechanical — removing a Docker socket mount, which breaks the tool that needs it — is not Manual: it is a Review with a warning saying what it can break, applied only when you pick it, never by a batch. 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 184 findings resolve

Auto
75
Review
101
Manual
8

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
SSH hardening11
Container exposure10
Account hygiene7
Auto-updates7
Docker daemon7
Vulnerabilities7
Host firewall6
Exposed services6
AI agent runtimes6
Reverse proxy6
Proxmox VE host6
Service hardening6
Single-node Kubernetes5
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
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
ssh.logingracetimeUnauthenticated connections are held open too longLOWAuto
ssh.x11forwardingX11 forwarding is enabledLOWAuto
ssh.allowtcpforwardingTCP forwarding is enabledLOWReview
ssh.clientalivecountmaxDead SSH sessions survive too many keepalive failuresLOWAuto
ssh.clientaliveintervalIdle SSH sessions have no server-side timeoutLOWAuto
ssh.fingerprinthashHost-key fingerprints use a legacy hashLOWAuto
ssh.ignorerhostsLegacy rhosts trust is not explicitly ignoredLOWAuto
ssh.loglevelSSH authentication logging is not verboseLOWAuto
ssh.maxsessionsOne SSH connection may open many sessionsLOWReview
ssh.printlastlogUsers are not shown their previous loginLOWAuto
ssh.strictmodesSSH does not enforce ownership and mode checksLOWAuto
ssh.tcpkeepaliveSSH trusts spoofable TCP keepalivesLOWAuto
ssh.usednsSSH performs unnecessary reverse DNS lookupsLOWAuto
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 containerHIGHReview
compose.ds018Datastore exposed on all interfacesHIGHAuto
compose.ds001Container runs in privileged modeHIGHReview
compose.ds019Admin panel exposed on all interfacesHIGHAuto
compose.dr001Container uses host network modeHIGHReview
compose.ds017Sensitive host path mounted read-writeHIGHReview
compose.ds005Adds a dangerous Linux capabilityHIGHReview
compose.ds020Shares the host PID namespaceHIGHReview
compose.dr005Hardcoded secret in the environmentHIGHManual
compose.ds006Missing no-new-privileges hardeningMEDIUMAuto
compose.ds021Shares the host IPC namespaceMEDIUMReview
compose.ds009Container runs as rootMEDIUMReview
compose.dr002Port published on all interfacesMEDIUMAuto
compose.ds008No restart policy setLOWAuto
compose.ds022Container filesystem is writableLOWReview
compose.ds023Seccomp filtering is disabledHIGHReview
compose.ds024AppArmor confinement is disabledMEDIUMReview
compose.ds026Host user namespace is usedMEDIUMReview
compose.ds010No memory limit setLOWReview
compose.ds012No healthcheck definedLOWManual
compose.dr004Loads secrets from an env_fileLOWReview

Firewall firewall.

IDWhat it flagsSeverityFix
firewall.inactiveNo active host firewall (ufw, firewalld, the Proxmox VE firewall, 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 ufwHIGHReview

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 effectHIGHReview
updates.origin-excludedWaiting updates come from repositories unattended-upgrades never installs fromMEDIUMReview
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
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 networkHIGHReview
ports.exposed-adminAn admin/management UI (e.g. Portainer) is reachable from the networkHIGHReview
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
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)HIGHReview
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 hashMEDIUMReview
accounts.password-roundsPassword hashing rounds are not hardenedLOWAuto
accounts.default-umaskNew files default to broad permissionsLOWAuto
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
accounts.core-dumpsCore dumps are not explicitly disabledLOWAuto

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 and Caddy from a package, read out of /etc/nginx and /etc/caddy and following include and import into the files they pull in; and Traefik and Caddy as containers, read out of the Compose files the container domain already discovers, with a container's Caddyfile read through its bind mount. A Caddy configured in JSON rather than a Caddyfile is reported as not audited rather than as clean. Deprecated TLS versions are checked for nginx only, because Caddy accepts nothing older than TLS 1.2.

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 proxyHIGHReview
proxy.admin-api-exposedCaddy's admin API, which has no authentication and can replace the whole running configuration, listens on something other than loopback. Medium for a container whose port 2019 is not published to the host, since reaching it then takes a foothold in a neighbouring containerHIGHReview
proxy.tls-deprecated-protocolsssl_protocols still offers TLS 1.0 or 1.1, which RFC 8996 deprecated and every current browser refusesMEDIUMReview
proxy.directory-listingnginx's autoindex on or Caddy's file_server browse makes every directory with no index file browsableMEDIUMReview
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

Proxmox VE host proxmox.

The hypervisor every guest on it trusts, and the homelab one is usually reachable from the whole LAN on port 8006 with a root password and nothing else. This domain runs only on a host with /etc/pve, and reads what a single-host audit can see and nothing inside a guest could: who can reach the management interface (/etc/default/pveproxy), whether root@pam has a second factor (/etc/pve/priv/tfa.cfg, readable only by root, so a non-root scan reports it as not audited), and whether an enterprise repository is enabled without a subscription to use it (pvesubscription get).

The datacenter firewall has no finding here. A host with no filtering is already the firewall domain's finding, and that domain now asks pve-firewall directly, so an enabled PVE firewall is recognised as one rather than read as an unprotected host from its INPUT policy. SSH root login is left to the SSH domain, since PVE clusters use root SSH between nodes.

IDWhat it flagsSeverityFix
proxmox.webui-openThe web interface and API on 8006 accept a login from any address: no LISTEN_IP, and no allow-list that a deny rule or policy actually enforcesMEDIUMManual
proxmox.root-no-tfaroot@pam has no enabled second factor (TOTP, WebAuthn, U2F or Yubico); recovery keys alone do not countMEDIUMManual
proxmox.enterprise-repo-unsubscribedAn enterprise repository (PVE or Ceph) is enabled without an active subscription, so apt update is refused and Proxmox's own packages stop receiving updates while Debian's keep arrivingMEDIUMReview

All three are Manual. Which network is the management network is not written anywhere Hostveil can read, and a wrong allow-list locks you out of the hypervisor's interface. A second factor is a device a person holds. And moving off the enterprise repository is two changes in sequence, where doing only the first leaves the host with no Proxmox updates at all.

Single-node Kubernetes kube.

Not a cluster manager: the homelab pattern of one box running one node, with k3s or k0s. This domain runs only where one of them is installed, and reads what a single-host audit can see — the credentials the distribution writes to disk, and the arguments it starts the API server and kubelet with. What runs inside the cluster (RBAC, pod security, network policy) is the cluster’s business and needs a different tool.

k3s takes its settings from four layers and the last one wins: /etc/rancher/k3s/config.yaml, then config.yaml.d/*.yaml in name order (a key ending in + appends instead of replacing), then the installer’s environment file (/etc/systemd/system/k3s.service.env, or /etc/rancher/k3s/k3s.env on openrc), then the command line in the systemd unit or in /etc/init.d/k3s. Hostveil resolves all four rather than reading the first, because K3S_KUBECONFIG_MODE=644 on the install command — the line most tutorials copy — lands in the environment file, not in any config. A command line it could not read is reported as not audited, never as "no flags".

IDWhat it flagsSeverityFix
kube.kubeconfig-readableThe cluster-admin kubeconfig (/etc/rancher/k3s/k3s.yaml, or k0s’s admin.conf) is readable by every account — which makes every local user root, through a privileged pod. Group-readable is write-kubeconfig-group doing its job and is not flaggedMEDIUMReview
kube.token-readablek3s’s token or node-token, which lets a machine join the cluster, is readable by every accountMEDIUMReview
kube.anonymous-authanonymous-auth=true is set explicitly for the API server or the kubelet. Both distributions default it off; anonymous requests are still authorised by RBAC, which is why it is not higherMEDIUMReview
kube.secrets-unencryptedk3s stores Secrets unencrypted in its datastore — the default, and the first item in the k3s CIS hardening guideLOWReview

All four are Manual. k3s rewrites its kubeconfig at start from write-kubeconfig-mode, so a chmod would pass its re-check and be undone at the next restart; a readable token has to be rotated, not only tightened; and the other two change how the control plane starts, which takes a restart of the node every workload runs on.

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
fileperms.passwd/etc/passwd is writable by non-root usersHIGHAuto
fileperms.group/etc/group is writable by non-root usersHIGHAuto
fileperms.hostkeyAn SSH host private key is readable beyond rootHIGHAuto
fileperms.gshadow/etc/gshadow is too permissiveHIGHAuto
fileperms.sudoers/etc/sudoers is too permissiveHIGHAuto
fileperms.sudoers-dropinsA sudoers drop-in is too permissiveHIGHAuto
fileperms.cronA system cron definition is writable by non-rootHIGHAuto
fileperms.systemd-unitsA system service unit is writable by non-rootHIGHAuto
fileperms.docker-configRoot's Docker credentials are too permissiveHIGHAuto
fileperms.grub-configGRUB configuration permissions are too broadHIGHAuto
fileperms.grub2-configGRUB2 configuration permissions are too broadHIGHAuto
fileperms.at-allowThe at allowlist is too permissiveMEDIUMAuto
fileperms.at-denyThe at denylist is too permissiveMEDIUMAuto
fileperms.cron-allowThe cron allowlist is too permissiveMEDIUMAuto
fileperms.cron-denyThe cron denylist is too permissiveMEDIUMAuto
fileperms.crontabThe system crontab is too permissiveHIGHAuto
fileperms.passwd-backupThe passwd backup is writable by non-rootHIGHAuto
fileperms.ownerOne of the files above is not owned by root — permissions are only half of who can read itHIGHReview
fileperms.sshd-configsshd_config is writable by non-root usersMEDIUMAuto
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: 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
sysctl.syncookiesTCP SYN-flood protection is offMEDIUMAuto
sysctl.accept-redirectsICMP redirects can rewrite this host's routingMEDIUMAuto
sysctl.protected-linksSymlink/hardlink race protections are disabledMEDIUMAuto
sysctl.kptr-restrictKernel pointer addresses are visible to all usersLOWAuto
sysctl.dmesg-restrictAny user can read the kernel logLOWAuto
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
sysctl.send-redirectsThe host sends ICMP redirectsMEDIUMReview
sysctl.suid-dumpablePrivileged processes may write core dumpsMEDIUMAuto
sysctl.protected-fifosFIFO protections are weakMEDIUMAuto
sysctl.protected-regularRegular-file protections are weakMEDIUMAuto
sysctl.unprivileged-bpfUnprivileged users can load BPF programsMEDIUMAuto
sysctl.perf-eventsPerformance events are exposed to usersMEDIUMAuto
sysctl.icmp-broadcastsBroadcast pings are acceptedLOWAuto
sysctl.bogus-icmp-errorsBogus ICMP errors are not ignoredLOWAuto
sysctl.log-martiansSuspicious source addresses are not loggedLOWAuto
sysctl.aslrFull address-space randomization is disabledMEDIUMAuto
sysctl.core-uses-pidCore dump names omit the process IDLOWAuto
sysctl.ctrl-alt-delCtrl-Alt-Delete immediately reboots the hostLOWAuto
sysctl.bpf-jit-hardenBPF JIT hardening is incompleteMEDIUMAuto
sysctl.tty-ldisc-autoloadTTY use can autoload line disciplinesMEDIUMAuto
sysctl.ipv6-accept-redirects-allIPv6 redirects are accepted globallyMEDIUMAuto
sysctl.ipv6-accept-redirects-defaultNew IPv6 interfaces accept redirectsMEDIUMAuto
sysctl.ipv6-accept-source-route-allIPv6 source routing is accepted globallyMEDIUMAuto
sysctl.ipv6-accept-source-route-defaultNew IPv6 interfaces accept source routingMEDIUMAuto
sysctl.ipv6-send-redirectsThe host sends IPv6 redirectsLOWAuto
sysctl.ipv4-default-accept-redirectsNew IPv4 interfaces accept redirectsMEDIUMAuto
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
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 portHIGHReview
dockerd.socket-world-writableEvery local account can connect to the Docker socket, and so become rootHIGHReview
dockerd.api-tls-unverifiedThe API is encrypted but accepts any client — TLS without tlsverify authenticates the server, not the callerHIGHReview
dockerd.group-membersAccounts other than root hold the socket's group, which is root-equivalent with no password prompt and no audit recordMEDIUM / HIGHReview
dockerd.no-new-privilegesContainers can still gain privileges through setuid binariesMEDIUMReview
dockerd.userns-remapUser namespaces are not remapped, so container root is host rootLOWReview
dockerd.live-restoreRestarting the daemon stops every container, which is why daemon updates get deferredLOWReview

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 / LOWReview
systemd.protect-homeThe service can read /home, /root and /run/user — everyone’s SSH keys, cloud credentials and password databasesMEDIUM / LOWReview
systemd.private-tmpThe service shares /tmp with every other process, which is the ground symlink races live onLOWReview
systemd.private-devicesThe service can access host devicesMEDIUMReview
systemd.protect-kernel-tunablesThe service can change kernel tunablesMEDIUMReview
systemd.protect-kernel-modulesThe service can alter kernel modulesMEDIUMReview
systemd.protect-control-groupsThe service can alter control groupsMEDIUMReview
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 namespacesMEDIUMReview
systemd.lock-personalityThe service can change execution personalityLOWReview
systemd.memory-deny-write-executeThe service can create writable executable memoryMEDIUMReview

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, Hermes Agent and Goose — 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 authenticationHIGHReview
agent.gateway-exposedThe gateway is bound to a network-reachable addressHIGHReview
agent.secret-exposedA credentials file or directory is readable beyond its ownerHIGHAuto
agent.exec-unrestrictedThe agent may run shell commands with no approval step. For Goose that is GOOSE_MODE set to auto or not set at all, since auto is its defaultHIGHReview
agent.elevated-enabledElevated (host-level) command execution is enabledHIGHAuto
agent.sandbox-offAgent tools run unsandboxed, directly on the hostHIGHReview
agent.control-ui-insecureThe control UI has its auth or device-identity checks disabledHIGHAuto
agent.config-permsA config or state path is more permissive than upstream ships itMEDIUMAuto
agent.ssrf-private-networkThe agent browser may reach private-network addressesMEDIUMAuto

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. Goose's GOOSE_MODE is Manual for the same reason (an environment variable overrides the file) and because the usual case is a key that is missing, which Hostveil would have to insert. Goose's web interface takes its address from a command-line flag rather than a file, so its exposure is judged from the listening socket alone, attributed by process name rather than by port.

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 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.dr001 Review

Delete network_mode: host

✓ The container gets its own network namespace again, so it can no longer bind any port on the host or reach services that listen only on the host's loopback.

⚠ With host networking gone, whatever port the service listens on is no longer reachable from outside the container until you publish it under ports: — Hostveil cannot tell which ports those are. Services that discover devices on the LAN (Home Assistant, Plex/Jellyfin DLNA) also lose that. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.dr002 Auto

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.dr004 Review

Remove group and other access from /opt/example/.env

✓ The credentials in the env_file stop being readable by every other account on the host, which is the easiest way a secret leaks off a shared machine.

⚠ Another account that runs docker compose against this project — a deploy user, a CI runner — can no longer read the file and fails to start the service. Checking that the file is out of git and off-host backups is still yours to do. The old mode is checkpointed.

compose.ds001 Review

Delete privileged: true

✓ A compromise of this container stops being a compromise of the host: it loses direct access to every device and almost every kernel capability root has.

⚠ If the service really does need host devices or kernel access — a VPN client, a hardware monitor, Docker-in-Docker — it will fail to start or quietly lose that function. Check its logs after recreating it, and if it needs one specific capability, grant just that with cap_add. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds005 Review

Remove SYS_ADMIN from cap_add

✓ Takes away a capability that can be escalated into a container escape, so a compromise of this service stays inside it.

⚠ Services add capabilities on purpose: NET_ADMIN for a VPN or a firewall container, SYS_ADMIN for FUSE mounts, SYS_PTRACE for a debugger. If this one needs SYS_ADMIN, it will fail the operation that uses it once recreated. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds006 Auto

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

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.ds009 Review
  • Run app as 1000:1000 — the first regular user, which most images that support a non-root user are built for (recommended)

    ✓ A process that escapes this container lands on the host as an unprivileged UID instead of root.

    ⚠ Images that start as root to set up and then drop privileges themselves — postgres chowning its data directory, nginx binding port 80 — fail to start when a user is forced on them, and files already on a volume stay owned by whoever wrote them. Check the image's documentation for the UID it expects. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

  • Run app as 65534:65534 — nobody, for a service that writes nothing it needs to own

    ✓ A process that escapes this container lands on the host as an unprivileged UID instead of root.

    ⚠ Images that start as root to set up and then drop privileges themselves — postgres chowning its data directory, nginx binding port 80 — fail to start when a user is forced on them, and files already on a volume stay owned by whoever wrote them. Check the image's documentation for the UID it expects. This is a file edit with a checkpoint, so rolling it back restores the line exactly. 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.ds016 Review

Remove the /var/run/docker.sock volume

✓ The container loses the Docker API, which is root on the host for anything that can reach it — so compromising this service stops meaning compromising the machine.

⚠ Portainer, Traefik's Docker provider, Watchtower and similar tools exist to talk to that API and stop working without it. If this is one of them, a socket proxy that allows only the calls it needs is the way to keep it working. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds017 Review

Add :ro to the /var/run/docker.sock mount

✓ A compromise of this container can still read /var/run/docker.sock but can no longer plant SSH keys, cron jobs or anything else there to persist on the host.

⚠ If the service writes under that mount — a backup tool restoring files, a config manager — those writes start failing with a read-only filesystem error. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds018 Auto

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

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.ds020 Review

Delete pid: host

✓ The container stops seeing every process on the host, along with the credentials their command lines and environments tend to carry, and loses the ability to signal them.

⚠ A monitoring agent (node-exporter, netdata, a security scanner) uses this to observe the host; without it, it starts fine and reports only on itself. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds021 Review

Delete ipc: host

✓ The container can no longer read or tamper with shared memory that host processes and other containers use.

⚠ If the service shares memory with a process on the host on purpose, that sharing stops; if it only needs to share with one other container, ipc: "service:NAME" is the narrower setting. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds022 Review

Set read_only: true with tmpfs at /tmp and /run

✓ A process that breaks in can no longer replace the container's own binaries or leave tools behind in its filesystem; anything it writes outside the tmpfs mounts fails, and what it writes inside them is gone at the next restart.

⚠ Many images write somewhere other than /tmp and /run — a cache directory, a PID file, logs — and fail to start, or fail later, on a read-only root. Watch the logs after recreating it; each path it complains about needs a tmpfs or a volume. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds023 Review

Remove seccomp:unconfined from security_opt

✓ Docker's default syscall filter applies again, blocking the kernel interfaces a container escape usually goes through.

⚠ Someone disabled the filter for a reason — usually one syscall an application needs, such as a browser sandbox or a FUSE mount. That call will now fail with EPERM; if it does, a profile allowing just that syscall is the narrow fix. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds024 Review

Remove apparmor:unconfined from security_opt

✓ Docker's default AppArmor profile confines the container again, putting back a host-enforced limit on what a compromise inside it can read and execute.

⚠ If the service mounts filesystems or writes under /proc or /sys, the default profile will deny it and the service will fail where it does that. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

compose.ds026 Review

Delete userns_mode: host

✓ Root inside the container maps to an unprivileged host UID again, so a container escape no longer lands as host root.

⚠ Files on bind mounts that were written as host root become unreadable to the remapped root, which can stop a database or anything else that owns its data directory from starting. This is a file edit with a checkpoint, so rolling it back restores the line exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

SSH

ssh.allowagentforwarding Review

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 Review

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

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

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

Disable empty passwords

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

ssh.fingerprinthash Auto

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 Review

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 Review

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

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 Review

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

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

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

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 Review

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 Review

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 Review

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 Review

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

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

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

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

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

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.docker-bypass Review

Add the ufw-docker rules to /etc/ufw/after.rules and reload ufw

✓ Published container ports stop bypassing the firewall: from outside the private ranges they are reachable only where a ufw route allow rule says so, which is what ufw status already claims.

⚠ Every port a container publishes stops being reachable from the internet the moment ufw reloads — 8080/tcp (web) included — until you allow it with ufw route allow proto tcp from any to any port <container port>. Traffic from 10/8, 172.16/12 and 192.168/16 is still let through. If ufw refuses the rules, Hostveil puts the original file back and reloads again. The edit has a checkpoint; rolling it back reloads ufw and empties the DOCKER-USER chain, which a reload alone leaves as it is.

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

Turn on apply_updates in dnf-automatic's config, install dnf-automatic and enable its timer

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

⚠ Starts dnf-automatic's timer, which installs updates on its own from then on — and an update can restart the service it belongs to. The config edit has a checkpoint; rolling it back also stops the timer, but leaves the package installed.

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.origin-excluded Review

Add their origin patterns to /etc/apt/apt.conf.d/52hostveil-origins

✓ Updates from these repositories install on the same schedule as the distribution's own, instead of waiting for someone to notice them.

⚠ From then on these repositories update the host unattended, so you are trusting them as much as the distribution. If one ships a kernel and Unattended-Upgrade::Automatic-Reboot is on, its kernel updates will reboot the machine.

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.reboot-required Review

Reboot in one minute (shutdown -r +1)

✓ The host starts running the patched kernel and libraries it already has on disk, which is the only way the vulnerabilities those updates fixed actually stop applying to it.

⚠ Every service on this host goes down until the machine is back, and this session is disconnected. Services without a restart policy, and anything started by hand, stay down after the boot. If the new kernel fails to boot, getting the machine back needs console access. Run shutdown -c within the minute to cancel. There is no checkpoint: a reboot cannot be rolled back.

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

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.exposed-admin Review

Deny 6379/tcp ahead of every other ufw rule

✓ app stops being reachable from any other machine, while everything on this host keeps reaching it as before.

⚠ Every remote client of app is cut off — an application server on another machine, a replica, a backup job, your own desktop client — including ones a subnet allow rule used to let in. An existing allow 6379/tcp is deleted first, because ufw will not put a deny for the same port ahead of it. There is no checkpoint: undo it with ufw delete deny 6379/tcp, and ufw allow 6379/tcp if you had that rule. Binding the service to 127.0.0.1 in its own configuration is the cleaner long-term fix.

ports.exposed-datastore Review

Deny 6379/tcp ahead of every other ufw rule

✓ app stops being reachable from any other machine, while everything on this host keeps reaching it as before.

⚠ Every remote client of app is cut off — an application server on another machine, a replica, a backup job, your own desktop client — including ones a subnet allow rule used to let in. An existing allow 6379/tcp is deleted first, because ufw will not put a deny for the same port ahead of it. There is no checkpoint: undo it with ufw delete deny 6379/tcp, and ufw allow 6379/tcp if you had that rule. Binding the service to 127.0.0.1 in its own configuration is the cleaner long-term fix.

ports.redis-bind Review

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 Review

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

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

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

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 Review

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

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

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 Review

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.uid0 Review
  • Lock and expire alice (usermod --lock --expiredate 1) (recommended)

    ✓ The account can no longer log in by any route — password, SSH key or su — so a backdoor planted under it stops working, while the entry, its files and its history stay in place for you to investigate.

    ⚠ If something on this host deliberately logs in as alice — a recovery procedure, a monitoring agent, a second admin name for root — it stops working. There is no checkpoint: exec fixes are not file-backed, so Hostveil cannot undo this. To reverse it: usermod --unlock --expiredate '' alice.

  • Delete alice (userdel), keeping its home directory

    ✓ Removes the second root entirely, so nothing about it can be re-enabled later by someone who knows it is there.

    ⚠ This cannot be undone: the account entry, its group and its mail spool are gone, and every file it owned is left owned by UID 0 with nothing recording that it was alice's. Prefer locking first if you have not yet worked out how the account got there. There is no checkpoint: exec fixes are not file-backed, so Hostveil cannot undo this.

accounts.weak-password-hash Review

Expire alice's password (passwd --expire)

✓ At the next login alice has to set a new password, which is stored with the system's current hashing method instead of the DES or MD5 hash that is cheap to crack today.

⚠ The next login as alice stops at a password change. An account that only logs in non-interactively (a script using a password over SSH, an FTP account) fails instead of prompting. The weak hash stays in /etc/shadow until the change happens, and the new one is only strong if the system's ENCRYPT_METHOD is SHA512 or yescrypt. There is no checkpoint: exec fixes are not file-backed, so Hostveil cannot undo this.

File perms

fileperms.at-allow Auto

Tighten /etc/shadow to 0640

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

fileperms.at-deny Auto

Tighten /etc/shadow to 0640

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

fileperms.compiler Review

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.

⚠ Development hosts legitimately need unprivileged compiler access.

fileperms.cron Auto

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

Tighten /etc/shadow to 0640

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

fileperms.cron-deny Auto

Tighten /etc/shadow to 0640

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

fileperms.crontab Auto

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

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

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

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

Tighten /etc/shadow to 0640

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

fileperms.gshadow Auto

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

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.owner Review

Set the owning user of each file to root, keeping its group

✓ The account that owned these files loses the access ownership gave it — every password hash, in /etc/shadow's case — and the permission bits mean what they appear to again.

⚠ If a service runs as that account and reads one of these files on purpose, it stops being able to. And if several files changed owner at once, something put them there — a restore run as the wrong user, an archive extracted with its own uids — and other files are likely affected too. The previous owner is checkpointed and a rollback restores it.

fileperms.passwd Auto

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

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

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

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

Tighten /etc/shadow to 0640

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

fileperms.sudoers-dropins Auto

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

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.auth-disabled Review

Set kernel.kptr_restrict to 1

✓ Nothing on the network can drive the agent any more; only processes on this host, and you through a tunnel, can reach it.

⚠ Once the gateway restarts it listens on loopback only, so every client that reached it over the network — your laptop, a phone app, another machine's integration — loses it until you reach it over an SSH tunnel or a tailnet. Without network exposure, no authentication is the single-user default OpenClaw treats as legitimate, which is what clears this finding. The edit has a checkpoint and rolls back exactly.

agent.config-perms Auto

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

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

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

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.gateway-exposed Review

Set kernel.kptr_restrict to 1

✓ Nothing on the network can drive the agent any more; only processes on this host, and you through a tunnel, can reach it.

⚠ Once the gateway restarts it listens on loopback only, so every client that reached it over the network — your laptop, a phone app, another machine's integration — loses it until you reach it over an SSH tunnel or a tailnet. The edit has a checkpoint and rolls back exactly.

agent.sandbox-off Review

Set kernel.kptr_restrict to 1

✓ Every session except your own direct chat runs its tools in a container, so a prompt injected through a group, a channel or a message from a stranger reaches a sandbox instead of your real filesystem.

⚠ Sandboxed tools run in a container, so OpenClaw needs Docker on this host and pulls a sandbox image the first time; tools that relied on reaching the host's own files, network or devices stop being able to. "all" also moves your own direct chat into the sandbox. The edit has a checkpoint and rolls back exactly.

agent.secret-exposed Auto

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

✓ Same protection as send-redirects, for IPv6.

sysctl.kptr-restrict Auto

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

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 Review

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 Review

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 Review

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 Review

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 Review

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

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

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

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

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

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

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

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

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

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.

Dockerd

dockerd.api-tls-unverified Review
  • Set "tlsverify": true in /etc/docker/daemon.json and restart Docker (recommended)

    ✓ The API keeps listening, but only clients holding a certificate your CA signed can use it, which turns encryption into authentication.

    ⚠ The daemon needs "tlscacert" naming the CA that signs your client certificates; without it Docker refuses to start. Every client that connects today without a certificate is refused afterwards. Restarting Docker stops every container and starts the ones with a restart policy again. If the daemon refuses to start with the new file, Hostveil puts the old one back and starts it again. The file edit has a checkpoint, and rolling it back restarts Docker under the original.

  • Remove tcp://0.0.0.0:2375 from hosts in /etc/docker/daemon.json and restart Docker

    ✓ Nothing on the network can reach the Docker API any more, which closes a port that is root on this host for anyone who can connect to it.

    ⚠ Anything that administers this daemon over TCP from another machine — a Portainer or Docker context on your laptop, a CI runner, a remote agent — loses it. DOCKER_HOST=ssh://user@host is the supported replacement and needs no open port. Restarting Docker stops every container and starts the ones with a restart policy again. If the daemon refuses to start with the new file, Hostveil puts the old one back and starts it again. The file edit has a checkpoint, and rolling it back restarts Docker under the original.

dockerd.api-unauthenticated Review

Remove tcp://0.0.0.0:2375 from hosts in /etc/docker/daemon.json and restart Docker

✓ Nothing on the network can reach the Docker API any more, which closes a port that is root on this host for anyone who can connect to it.

⚠ Anything that administers this daemon over TCP from another machine — a Portainer or Docker context on your laptop, a CI runner, a remote agent — loses it. DOCKER_HOST=ssh://user@host is the supported replacement and needs no open port. Restarting Docker stops every container and starts the ones with a restart policy again. If the daemon refuses to start with the new file, Hostveil puts the old one back and starts it again. The file edit has a checkpoint, and rolling it back restarts Docker under the original.

dockerd.group-members Review

Remove ci-runner from docker (gpasswd -d)

✓ ci-runner can no longer become root on this host through the Docker API, which takes a password-free, unlogged root grant away from an account that may not need it.

⚠ If ci-runner is how you administer containers, docker stops working for it without sudo; if it is a service — Portainer's agent, Watchtower, a CI runner — that service stops being able to manage containers. Do not remove the account you are using right now until you have another route in. Existing sessions keep the group until they log out. There is no checkpoint: to reverse it, usermod -aG docker ci-runner.

dockerd.live-restore Review

Set "live-restore": true in /etc/docker/daemon.json and reload Docker

✓ Containers keep running while the daemon restarts, so upgrading Docker stops being an outage and stops being the update that gets put off.

⚠ Docker picks this setting up on a reload, so applying it stops nothing. It is unsupported in swarm mode, and a daemon that cannot use it keeps running without it. If the daemon refuses the new file, Hostveil puts the old one back and reloads again. Rolling it back restarts Docker, because a reload does not turn the setting off again; with live-restore still on at that moment, the containers keep running through it.

dockerd.no-new-privileges Review
  • Set "no-new-privileges": true in /etc/docker/daemon.json and restart Docker now (recommended)

    ✓ Every container started from now on runs with no-new-privileges, so a setuid binary inside one can no longer turn a foothold in an application into root in its container.

    ⚠ A container that relies on setuid escalation — sudo inside the container, an image whose entrypoint switches users through a setuid helper — fails once recreated, and needs security_opt: ["no-new-privileges:false"] to opt back out. Restarting Docker stops every container on the host and starts the ones with a restart policy again; anything started by hand stays down. If the daemon refuses to start with the new file, Hostveil puts the old one back and starts it again. The file edit has a checkpoint, and rolling it back restarts Docker under the original.

  • Set "no-new-privileges": true in /etc/docker/daemon.json; restart Docker yourself later

    ✓ Every container started from now on runs with no-new-privileges, so a setuid binary inside one can no longer turn a foothold in an application into root in its container.

    ⚠ A container that relies on setuid escalation — sudo inside the container, an image whose entrypoint switches users through a setuid helper — fails once recreated, and needs security_opt: ["no-new-privileges:false"] to opt back out. Nothing changes until Docker restarts, and the finding is reported until then. The file edit has a checkpoint.

dockerd.socket-world-writable Review

Set SocketMode=0660 in /etc/systemd/system/docker.socket.d/99-hostveil.conf and chmod the live socket

✓ Only root and the socket's group can reach the Docker API, so an ordinary account or a compromised service on this host can no longer become root through it.

⚠ Any account or service that reached Docker only because the socket was world-writable — not through the group — loses access now. Add it to the group if it should keep it. The drop-in is a new file with a checkpoint; rolling back deletes it, but the live socket keeps mode 0660 until Docker next recreates it.

dockerd.userns-remap Review
  • Set "userns-remap": "default" in /etc/docker/daemon.json and restart Docker now (recommended)

    ✓ Root inside a container becomes an unprivileged UID on the host, so an escape or a careless bind mount no longer lands as real root.

    ⚠ This is the most disruptive daemon setting there is. Docker keeps remapped images and containers in a separate storage directory, so every existing container and image disappears from docker ps -a and docker images until it is turned off again, and pulls start from nothing. Bind-mounted host files owned by root become unwritable to the remapped root. Containers using privileged mode, host networking or host PID cannot run under it at all. Restarting Docker stops every container on the host and starts the ones with a restart policy again; anything started by hand stays down. If the daemon refuses to start with the new file, Hostveil puts the old one back and starts it again. The file edit has a checkpoint, and rolling it back restarts Docker under the original.

  • Set "userns-remap": "default" in /etc/docker/daemon.json; restart Docker yourself later

    ✓ Root inside a container becomes an unprivileged UID on the host, so an escape or a careless bind mount no longer lands as real root.

    ⚠ This is the most disruptive daemon setting there is. Docker keeps remapped images and containers in a separate storage directory, so every existing container and image disappears from docker ps -a and docker images until it is turned off again, and pulls start from nothing. Bind-mounted host files owned by root become unwritable to the remapped root. Containers using privileged mode, host networking or host PID cannot run under it at all. Nothing changes until Docker restarts, and the finding is reported until then. The file edit has a checkpoint.

Services

systemd.lock-personality Review

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.memory-deny-write-execute Review

Set MemoryDenyWriteExecute=yes for app

✓ Stops the service creating memory that is both writable and executable, which is how injected code usually gets to run.

⚠ JIT runtimes — Node.js, Java, .NET, PHP with JIT, LuaJIT, browsers — need exactly that memory and crash or refuse to start after the restart. The drop-in is a new file with a checkpoint, so rolling it back deletes it; restart the service while you are watching.

systemd.no-new-privileges Review

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.private-devices Review

Set PrivateDevices=yes for app

✓ Gives the service a minimal /dev with no physical devices, so a compromise of it cannot reach disks, GPUs or other device nodes directly.

⚠ A service that uses hardware — GPU transcoding, a USB or serial device, a TUN interface for a VPN — loses it after the restart. The drop-in is a new file with a checkpoint, so rolling it back deletes it; restart the service while you are watching.

systemd.private-tmp Review

Set PrivateTmp=yes for app

✓ Gives the service a /tmp of its own, so another local process can no longer race it with a predictable temporary file name.

⚠ Two services that hand each other files through /tmp stop seeing each other's files, and anything this service left in /tmp is not there after the restart. The drop-in is a new file with a checkpoint, so rolling it back deletes it; restart the service while you are watching.

systemd.protect-clock Review

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-control-groups Review

Set ProtectControlGroups=yes for app

✓ Makes the cgroup tree read-only for the service, so a compromise of it cannot loosen the limits that isolate other workloads.

⚠ Container runtimes and anything that creates or manages cgroups itself (Docker, containerd, Kubernetes, LXC) stop working after the restart. The drop-in is a new file with a checkpoint, so rolling it back deletes it; restart the service while you are watching.

systemd.protect-home Review

Set ProtectHome=yes for app

✓ Hides /home, /root and /run/user from the service, so a compromise of it cannot read anyone's SSH keys or cloud credentials.

⚠ A service whose data or configuration lives in a home directory — a media server pointed at ~/Videos, a sync client — cannot reach it after the restart and fails or comes up empty. The drop-in is a new file with a checkpoint, so rolling it back deletes it; restart the service while you are watching.

systemd.protect-kernel-logs Review

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 Review

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.protect-kernel-tunables Review

Set ProtectKernelTunables=yes for app

✓ Makes /proc/sys and /sys read-only for the service, so a compromise of it cannot change kernel networking or security settings.

⚠ A service that manages kernel settings on purpose — a VPN enabling IP forwarding, a network manager, a container runtime — cannot after the restart. The drop-in is a new file with a checkpoint, so rolling it back deletes it; restart the service while you are watching.

systemd.protect-system Review

Set ProtectSystem=full for app

✓ Mounts /usr, /boot and /etc read-only for this service alone, so a compromise of it cannot replace a binary or edit a login file.

⚠ A service that writes its own configuration under /etc or updates files under /usr — a package manager, a self-updating agent — fails when it tries. The drop-in is a new file with a checkpoint, so rolling it back deletes it; restart the service while you are watching.

systemd.restrict-namespaces Review

Set RestrictNamespaces=yes for app

✓ Stops the service creating namespaces, removing kernel attack surface that most daemons never use.

⚠ Container runtimes, sandboxes, browsers and anything else that isolates its children with namespaces fail to start them after the restart. The drop-in is a new file with a checkpoint, so rolling it back deletes it; restart the service while you are watching.

systemd.restrict-suid-sgid Review

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.admin-api-exposed Review

Set admin localhost:2019 in /etc/ssh/sshd_config, then validate and reload Caddy

✓ The API that can replace the proxy's whole configuration stops answering anyone but processes on this host.

⚠ Whatever calls the admin API from elsewhere — a deploy script, a config manager, a certificate sidecar on another machine — stops being able to, and the proxy keeps serving until something needs to change and then cannot. If the proxy refuses the new configuration, Hostveil puts the original file back and reloads again. The edit has a checkpoint and rolls back exactly.

proxy.directory-listing Review

Set every autoindex on; in /etc/ssh/sshd_config to off, then reload nginx

✓ Directories without an index file stop listing what is in them, so a stray backup or .env beside the site is no longer one click away.

⚠ A directory somebody meant to be browsable — a download mirror, a shared folder — stops listing and answers 403 instead. Listing is sometimes on for one location on purpose; if so, put it back for that location alone. If the proxy refuses the new configuration, Hostveil puts the original file back and reloads again. The edit has a checkpoint and rolls back exactly.

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.

proxy.tls-deprecated-protocols Review

Set every ssl_protocols in /etc/ssh/sshd_config to TLSv1.2 TLSv1.3, then reload nginx

✓ Connections can no longer be negotiated down to TLS 1.0 or 1.1, which every current browser already refuses, so nothing that can connect today loses anything.

⚠ A client that only speaks TLS 1.0 or 1.1 — a very old Android or Java, an embedded device — stops connecting. If the proxy refuses the new configuration, Hostveil puts the original file back and reloads again. The edit has a checkpoint and rolls back exactly.

proxy.traefik-api-insecure Review
  • Remove --api.insecure=true and recreate app now (recommended)

    ✓ The unauthenticated dashboard, and the map of every router and backend it shows, stops being served to anyone who reaches port 8080.

    ⚠ The dashboard is gone until you expose api@internal through a router with authentication. The edit has a checkpoint and rolls back exactly. Recreating app drops every connection through this proxy for the seconds it takes to start.

  • Remove --api.insecure=true; recreate app yourself

    ✓ The unauthenticated dashboard, and the map of every router and backend it shows, stops being served to anyone who reaches port 8080.

    ⚠ The dashboard is gone until you expose api@internal through a router with authentication. The edit has a checkpoint and rolls back exactly. This edits the file, not the running container: docker compose up -d app is what puts the change into force.

Proxmox

proxmox.enterprise-repo-unsubscribed Review

Rewrite /etc/apt/sources.list.d/pve-enterprise.list to the pve-no-subscription repository

✓ apt can reach Proxmox's packages again, so the kernel, QEMU and the management stack start receiving the security updates that have been silently refused.

⚠ The no-subscription repository is the one Proxmox describes as less tested than enterprise and not recommended for production. The next apt full-upgrade may bring a lot of Proxmox packages at once, after months of none — run it while you can reboot. If you have a subscription, add the key instead (pvesubscription set <key>) and roll this back. The edit has a checkpoint.

Kubernetes

kube.anonymous-auth Review

Write /etc/rancher/k3s/config.yaml.d/99-hostveil-anonymous-auth.yaml and restart k3s

✓ Requests with no credential are refused instead of reaching RBAC as system:anonymous, so a stray binding for anonymous users stops being the whole cluster.

⚠ Anything that talks to the API server or the kubelet without credentials — a monitoring scrape, a health check against a path other than /livez, /readyz or /healthz — starts getting 401s. The drop-in is a new file with a checkpoint; rolling it back deletes it and restarts k3s. Restarting k3s makes the API unavailable for the seconds it takes to start; running pods keep running. If k3s will not start with the new file, Hostveil removes it and starts k3s again.

kube.kubeconfig-readable Review

chmod /var/run/docker.sock to 0600

✓ Local accounts and services lose the cluster-admin credentials, and with them the privileged pod that is root on this host.

⚠ kubectl stops working for anyone who used this file without sudo — give them a kubeconfig of their own. k3s writes this file again at every start from its write-kubeconfig-mode setting, so this lasts until k3s restarts. Make it permanent by setting write-kubeconfig-mode: "0600" where the current mode is set, command: --api.insecure=true. The old mode is checkpointed.

kube.secrets-unencrypted Review

Write /etc/rancher/k3s/config.yaml.d/99-hostveil-secrets-encryption.yaml, enable encryption, rotate the keys, and restart k3s

✓ Every Secret in the datastore is encrypted, the ones already there included, so a copied /var/lib/rancher/k3s or a shipped snapshot no longer hands over every password the cluster holds.

⚠ This cannot be rolled back: once Secrets are rewritten encrypted, removing the setting leaves them unreadable, so the way back is k3s secrets-encrypt disable and a rotation, by hand. It restarts k3s twice, which makes the API unavailable for the seconds each takes, and rewrites every Secret; running pods keep running. If a step fails, Hostveil stops there and says which, rather than undoing the earlier ones.

kube.token-readable Review

chmod /var/run/docker.sock to 0600

✓ No local account can read the token any more, so none of them can hand the cluster to a machine of their choosing from here on.

⚠ It has already been readable, so anything that copied it can still join a node. Rotate it after this (k3s token rotate), which hostveil does not do because it changes what every joining node has to present. The old mode is checkpointed.