A Linux authenticated scan is only as good as the account behind it. Hand a scan service account full root and you have created a standing, high-value credential that sits in a scanner configuration file, connects to every host in the fleet, and rarely gets rotated. Hand it too little and the scanner quietly falls back to remote banner grabbing, a method that misses distro-backported patches and buries real findings under false positives. What follows is the credential setup that sits between those two failure modes: which key material to generate, what the scan account’s file access actually needs to cover, how to write sudo rules that fall well short of blanket root, why SELinux and AppArmor can make a scan look complete while it silently returns partial data, and how to catch a scan that reports authenticated true while it saw less than half of what it should have.
Key-based vs password auth for scan accounts, and why keys win at scale
A password-based scan account means a secret typed or stored in every scanner configuration that targets it, and that secret has to be identical, or centrally synced, across however many hosts share the account. Rotating it means touching every target at once, and a debug log from a misconfigured scan job will happily print the password in cleartext the first time authentication fails. None of that scales past a handful of hosts.
SSH key auth removes the shared-secret problem. Generate a dedicated keypair for the scan service, ed25519 unless a legacy OpenSSH build in the fleet forces RSA, and keep the private key in the scanner’s credential store rather than on disk in plaintext. Push the public key into the scan account’s authorized_keys file, and constrain what that key is allowed to do independent of anything the operating system’s own access controls enforce. A typical restricted entry reads:
from=”10.20.0.0/16″,no-port-forwarding,no-agent-forwarding,no-x11-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA… svc-vulnscan@scanner
The from= restriction means a leaked private key is useless from outside the scanner’s own subnet. Disabling the forwarding options closes off capabilities the credential never needs for scanning and that an attacker would want most if the key were stolen. This access model mirrors the general credential-hardening guidance in SITEY’s security documentation: a service credential should be restricted to exactly the capability it exists for, nothing adjacent.
Rotate the keypair on a fixed schedule, quarterly is reasonable for most fleets, and immediately whenever someone with access to the scanner host or its credential store leaves the team. Because each authentication event logs a key fingerprint rather than a password, sshd’s own log line ties a login back to a specific scanner instance instead of a shared secret that could have come from anywhere.
Building a least-privilege scan user: what it actually needs to read
Create a dedicated local account, something like svc-vulnscan, with its own UID separate from any human account. It needs a real login shell, /bin/bash or /bin/sh, not /usr/sbin/nologin, because the scanner executes a sequence of commands over the SSH session even though no interactive human ever logs in as that user.
Resist the urge to add the account to adm, sudo, or wheel wholesale. Most of what a baseline vulnerability and CIS-benchmark style scan needs is already readable without escalation: /etc/os-release, /proc/version, and most files under /etc that govern service configuration, sshd_config and most sysctl drop-ins among them, ship world-readable by default. Checks that only need to confirm a file’s permission bits, for example verifying that a private key is mode 600, work with a plain stat call and need no read access to the file’s contents at all. Reserve group membership and sudo grants for the things that are genuinely root-only: the package database on hardened images, /etc/shadow, and command output that requires a running daemon’s internal state, such as sshd -T for the effective, not just configured, SSH daemon settings.
Sudo rules scoped to package and config commands instead of blanket NOPASSWD
The fastest way to turn one scan credential into a fleet-wide root compromise is a rule like svc-vulnscan ALL=(ALL) NOPASSWD: ALL. It works on day one and then sits there as the single largest privilege escalation path in the environment, because every host the scanner touches now has an account that can run anything as root without a password, reachable by anyone who gets that one SSH key.
A scoped sudoers drop-in file, /etc/sudoers.d/vulnscan, does the same job for the handful of commands a scan actually issues. A minimal version, as separate lines in the file, looks like this:
- Cmnd_Alias VULNSCAN_RPM = /usr/bin/rpm -qa, /usr/bin/rpm -qi *
- Cmnd_Alias VULNSCAN_DPKG = /usr/bin/dpkg -l, /usr/bin/dpkg -s *
- Cmnd_Alias VULNSCAN_READ = /usr/bin/cat /etc/shadow, /usr/bin/sysctl -a, /usr/sbin/sshd -T
- svc-vulnscan ALL=(root) NOPASSWD: VULNSCAN_RPM, VULNSCAN_DPKG, VULNSCAN_READ
- Defaults:svc-vulnscan !requiretty
Two details break this in practice more often than anything else. First, sudo matches the exact path the scanner invokes, not a shell-resolved command name. If a distro symlinks /bin/rpm to /usr/bin/rpm and the scanner calls /bin/rpm while the sudoers file only lists /usr/bin/rpm, sudo denies it, and the resulting error looks like a generic permissions failure rather than a path mismatch. Confirm the exact path with which rpm or type rpm as the scan account before writing the rule. Second, sudo’s wildcard matching is not shell globbing, so a rule written to match a command with variable arguments needs to be tested with sudo -l as the scan account to confirm it actually matches the invocation the scanner sends, not just something that looks equivalent on paper.
Scan orchestration platforms that manage credentials across many hosts, SITEY among them, typically keep this sudoers template per scanner integration and per OS family, since the exact command paths and package manager differ between RHEL and Debian derivatives, and a single global block does not survive contact with a mixed fleet.
SELinux, AppArmor and restricted shells that silently truncate results
Sudoers permission is necessary but not sufficient. On an SELinux-enforcing host, a command can pass the sudoers check and still be blocked by type enforcement, and the failure mode is the dangerous kind: the command exits 0 with truncated or empty output instead of a visible error. This shows up most often when a package database or config directory has been relocated or bind-mounted without restorecon being run afterward, leaving it with the wrong SELinux context for the process trying to read it.
Diagnose this with ausearch -m avc -ts recent on the target host, looking for denials logged at the scan’s timestamp. If temporarily setting SELinux to permissive mode resolves the missing data, the fix is a targeted policy change, semanage fcontext plus restorecon on the specific path, not disabling enforcing mode across the fleet to accommodate the scanner.
AppArmor causes the same class of problem on Debian and Ubuntu hosts that ship a hardened profile for dpkg or apt, restricting the commands to a fixed set of search paths. Check aa-status for whether the relevant profile is in enforce or complain mode, and grep the kernel log for DENIED entries around the scan window.
Restricted shells are a simpler but equally common trap. Some hardened baselines set every service account’s login shell to rbash or a similarly wrapped shell as a blanket policy. A scanner that issues multi-stage commands, anything involving a pipe, a subshell, or a working-directory change, will fail partway through with a restricted-command error, and the scan will report partial results with no obvious cause visible in the summary. Confirm this by running the scanner’s actual command sequence manually over a non-interactive SSH session and comparing the output count against a known-good reference host, rather than trusting the aggregate finding count in the scan report.
Distro package database access and why missing it downgrades findings to banner checks
On most default RHEL, CentOS, and Rocky installs, /var/lib/rpm is readable by any local user, so rpm -qa works without sudo. Debian and Ubuntu ship /var/lib/dpkg/status world-readable by default too. Hardened baselines change this deliberately, tightening the package database to root-only as part of restricting access to sensitive system inventory data. When that hardening is in place and the scan account has no sudo path to the database, the scanner cannot read the exact name, version, and release of every installed package.
What happens next is not a graceful degradation, it is a change in the kind of evidence the scan produces. The scanner falls back to grabbing service banners over the network, the OpenSSH version string, an HTTP Server header, an SMTP greeting, for the very hosts that were supposed to be authenticated. Banners report the upstream project version, but distro security teams routinely backport a CVE fix into an older upstream version number without bumping it, to preserve compatibility for everything else on the system. A banner-only check sees an old-looking version string and flags a CVE that a package-level check would have cleared, because the vendor’s backport is visible only in the package’s own version suffix, not in the daemon’s self-reported banner. That is not a rounding error in scan accuracy, it is the specific gap between a real finding and a false positive that someone then has to manually clear on every affected host.
The tell that this has happened is visible in the scan output itself: findings that read generically, an outdated-version warning rather than naming the exact installed package and version, or a host whose finding detail suddenly gets thinner between two scan windows using the same scan definition. Platforms that consolidate results across scanner integrations, SITEY included, can surface that shift directly, since a host moving from package-level detail to banner-only signatures is a sign that scan credentials or a hardening baseline changed, not that the host’s actual exposure changed overnight.
Diagnosing partial credential success: the half-authenticated scan problem
The most misleading outcome is not an authentication failure, it is a partial success that the scan report shows as complete. SSH login works, the scanner marks the host authenticated true, and a subset of checks that depend on sudo, an SELinux-cleared path, or the package database silently return nothing instead of an error. The badge on the report says authenticated. The actual coverage might be under half of what a fully provisioned account would return.
The table below covers where this specific failure tends to originate and how to confirm each cause on a live host.
| Layer | Symptom | How to confirm |
|---|---|---|
| SSH authentication | Connection refused or denied outright | Manual ssh attempt as the scan account, check sshd’s auth log for the matching entry |
| Sudoers | Command exits 1 with a not-allowed message | Run sudo -l as the scan account and compare against the intended sudoers file |
| SELinux | Command exits 0 with empty or truncated output | ausearch -m avc -ts recent for denials at the scan timestamp |
| AppArmor | Command exits 0 with empty output, similar to SELinux | aa-status plus a kernel log grep for DENIED entries |
| Restricted shell | Multi-stage command fails partway through | Replay the exact command sequence over a non-interactive SSH session |
| Package database permissions | Findings shift from package-level detail to generic banner text | Direct permission check on /var/lib/rpm or /var/lib/dpkg/status as the scan account |
Set a coverage threshold rather than trusting the binary authenticated flag on its own. If a host returns fewer than roughly ninety percent of the check IDs that a known-good reference host on the same OS and hardening profile returns, treat that host as partially authenticated and route it back for credential review before anyone trusts its clean results. An automated validation step that compares returned data against the expected check set for a host’s OS and profile, the kind of step SITEY runs during AI validation and triage before a finding is scored, catches this class of problem without someone manually replaying commands on every host in the fleet after every credential or baseline change.
About SITEY
SITEY is an autonomous vulnerability management platform. It discovers, validates, prioritizes, remediates and re-tests vulnerabilities through an eight-phase automated pipeline, unifying output from 17 integrated scanners. SITEY is self-hosted: it runs in your own infrastructure and your findings are stored there. Outbound connections are limited to licence activation and the optional services you enable, such as an AI provider, CVE enrichment and patch catalogues. Pricing is 599 USD per month or 5,999 USD for a perpetual lifetime license. See pricing or how the platform works.