Troubleshooting
Common errors, their root causes, and how to resolve them. Most of what looks like a failure is Obexum being honest — a check it could not run safely is reported, never silently dropped.
PASS, FAIL, SKIP (a
precondition was not met), or ERROR (the check could not
complete). Obexum is read-only during a scan —
it probes, it never changes the target. Almost every issue below is a
SKIP or an ERROR, not a broken scan.
1. Connectivity & authentication
Obexum reaches a target one of two ways: locally
(the host running the binary, local://localhost) or over
SSH (ssh://user@host:port). Windows
targets are reached over OpenSSH with PowerShell as the
default shell — the same SSH transport as Linux/BSD,
not WinRM. If a scan never gets past host resolution, the error is in
the SSH layer.
SSH key cannot be read or parsed
# ssh: read key: open /home/you/.obexum/keys/obexum_ed25519: no such file or directory
# ssh: parse key: ssh: this private key is passphrase protected
- read key — the key path does not exist.
Check
target.key_pathin your config (default~/.obexum/keys/obexum_ed25519) or the--keyyou registered on the target. Generate one withssh-keygen -t ed25519 -f ~/.obexum/keys/obexum_ed25519 -N "". - parse key — the key is encrypted but no
passphrase was supplied. Set
target.key_passphrase, or use an unencrypted audit key dedicated to Obexum.
Dial / connection failures
# ssh dial 10.0.0.5:22: dial tcp 10.0.0.5:22: connect: connection refused
# ssh dial 10.0.0.5:22: ... i/o timeout
- connection refused — nothing is listening
on that host/port. Confirm sshd (or the Windows OpenSSH Server
service) is running and the port matches (
--port, default 22). - i/o timeout — a firewall or wrong address
is dropping the connection. The dial timeout is 15 seconds.
Test reachability the plain way first:
ssh -i ~/.obexum/keys/obexum_ed25519 user@host.
Host key / known_hosts mismatch
By default Obexum uses trust-on-first-use: it learns
and pins the host key into ~/.ssh/known_hosts the first
time it sees a host. If you set target.strict_host_key: true
and the key is not already pinned — or the host was rebuilt and
its key changed — the connection fails with a
knownhosts key error. Remove the stale line for that host
from ~/.ssh/known_hosts (or point
target.known_hosts_path at a fresh file) and re-run.
Authentication rejected
# ssh dial 10.0.0.5:22: ssh: handshake failed: ssh: unable to authenticate
The public key is not authorised on the target. On Linux/BSD copy it
with ssh-copy-id -i ~/.obexum/keys/obexum_ed25519.pub user@host.
On Windows, the public key must be in the target user's
authorized_keys (for an admin account that is
C:\ProgramData\ssh\administrators_authorized_keys), and
the OpenSSH default shell should be PowerShell. Confirm you can run
ssh user@host powershell.exe -NoProfile -Command "$PSVersionTable.PSVersion.Major"
by hand — Obexum decides a target is Windows by exactly that
probe.
2. “Permission denied” / needing root or Administrator
Many checks read state that only a privileged account can see —
/etc/shadow, audit policy, the local Administrators group,
firewall configuration, BitLocker/LUKS status. Obexum detects your
privilege level once during host resolution:
- Linux / BSD — root is
id -u == 0. - Windows — “root” means the session account is a member of the local Administrators group (an elevated token). A standard user, or an admin running without elevation, reads as non-privileged.
Checks that declare a root prerequisite and do not have it
are reported as SKIP — requires root, not as errors
and never as findings. They simply do not run, which means a
non-privileged scan undercounts your real exposure.
sudo for a local scan, or use an SSH user
that is root (or has unrestricted sudo). On Windows use an
account in the local Administrators group; for Active Directory checks
on a Domain Controller that account also needs the matching domain
privileges. The scan summary's SKIP count and the
per-check audit: line on stderr tell you how much
coverage you lost.
3. Lots of checks showing SKIP
A large SKIP count is usually correct behaviour, not a
bug. Obexum only runs a check when its preconditions hold on
this host. A check that inspects PostgreSQL is meaningless on
a box with no PostgreSQL, so it skips with a visible reason instead of
inventing a finding. The skip reason is recorded for the report's
coverage section — nothing is hidden.
The reasons you will see, and what each means:
| Skip reason | Meaning |
|---|---|
requires root | The check needs root / Administrator and the scan is not privileged (see §2). |
missing command X | A tool the check relies on (e.g. auditctl, nft, Get-MpPreference) is not installed/available on the target. |
missing file X | A config file or path the check inspects does not exist — usually the feature is not installed. |
requires pkg manager X (have Y) | A package-state check that only applies to a different package manager (e.g. a dpkg check on an rpm host). |
no listener on port P | A service-specific check whose port is not being listened on, so the service is not running. |
platform X not in [...] | The check targets a different OS family (a Debian-only check on RHEL, a Linux check on Windows, etc.). |
So “hundreds of skips” on a minimal server is expected: the catalogue spans Windows AD, multiple Linux families and BSD, and most of it does not apply to any single host. If a check you expected to run is skipping, read its reason — it will name the missing command, file, port or privilege you need to provide.
4. Reporting a suspected false positive
Obexum's contract is zero false positives on a clean baseline, and every finding ships with its evidence so you can check our work. Each finding's evidence trail is the literal command(s) Obexum ran and the literal output it got back — that is what makes a finding falsifiable. Before reporting, inspect it:
# list the rule_ids + counts in the latest engagement
obexum findings list
# show one finding's full evidence (commands + raw output)
obexum findings show AUD-WIN-ADCS-001
If you have applied a fix and the finding should now be gone, the fastest way to confirm (and to catch a stuck detection) is the verify-loop. The portal's “Verify” panel on each finding issues a short-lived token; Obexum re-runs just that one rule and reports back whether it still fires:
# re-run a single rule against the host after remediation
obexum verify --upload-token=$VERIFY_JWT
If a check fires on a configuration you are certain is correct, that is a finding we want. Email security@obexum.com (probe behaviour / a check that fires when it should not) or docs@obexum.com (everything else), and attach:
- The finding's rule_id and the
obexum findings show <rule-id>output, which carries the exact command + output that triggered it. - The
findings.jsonfrom the engagement directory (see §6) — it has the full evidence and host context. - The target OS and version, and whether the scan was privileged.
5. Reading errors in scan output
A handful of message shapes show up on stderr during a scan. Knowing which are fatal and which are advisory saves a lot of worry:
| Message | Severity |
|---|---|
scanner error: ... | Advisory. One bundled engine (e.g. an external scanner) failed; the scan continues with the rest. |
audit engine warning: ... / ... warning: ... | Advisory. An optional stage (enrichment, rules, tracker) had a problem and was skipped; findings are unaffected. |
A check ending in ERROR | Inconclusive, never a fail. The check could not complete (parse error, transport hiccup, a per-check 60s timeout). Re-run; if it persists, send us the rule_id. |
check panicked: ... | A single check hit an internal error. It is isolated — the run does not crash — and surfaces as that check's ERROR. Worth reporting. |
upload: ... | Advisory. The scan succeeded and the local report was written; only the portal upload failed. Your audit artifact is safe on disk. |
The end-of-run summary is the authoritative tally — severity
counts plus the patch-posture block. An ERROR is treated
as “inconclusive”, so it is counted separately and never
inflates your FAIL numbers.
For more detail on what a check did or why it skipped, raise the log level:
obexum scan --target ssh://Administrator@10.0.0.5 --log-level debug
6. Where logs and the engagement directory live
Logs are written to stderr, not to a
separate log file. They are human-readable by default; set
logging.format: json (or scope it with
--log-level trace|debug|info|warn|error) for structured
output you can pipe into a collector. To keep a copy of a run, redirect
stderr yourself:
obexum scan --target ssh://Administrator@10.0.0.5 2> scan-$(date +%F).log
Reports and findings are written under the engagement
directory. With the defaults from obexum init, each scan
gets its own folder:
~/.obexum/scans/<scan_id>/
├── findings.json # every finding + full evidence trail
├── findings.html # the report you open in a browser
└── manifest.json # index of the run (target, counts, files)
The obexum findings list / show commands read
this directory and default to the most recent engagement; pass
--scan <scan_id> to inspect an older one. The location
is controlled by output.engagement_dir (and the flat
report copy by output.path) in your config — if you
cannot find your reports, check those two keys. JSON is always written
to the engagement directory even if it is not in
output.formats, so the evidence trail is never missing.
Still stuck?
- CLI reference — every command and flag.
- Configuration — the full
config.yamlschema. - Operational runbook — day-two operations.
- Email docs@obexum.com with the rule_id, the
findings.json, and your target OS — or security@obexum.com for a suspected false positive.