Security testing
How Gardience is tested for security before each release, what the latest round found, and what was fixed. This is our own testing with standard open-source tools, not an independent third-party penetration test.
The release check (tools/release-check.sh) runs this whole test against a fresh install of the exact bundle being released, and the release is refused if any of it fails: a high finding, a medium finding not on the accepted list below, an internal port reachable from outside, a weak TLS setup, any nuclei match, or a tool that did not complete. The release key signs a bundle only through tools/release.sh, which refuses unless the full release check passed on that exact, committed source. You can run the same test against an install you own with tools/pentest/run.sh.
Latest run from the internet: 17 October 2026, against Gardience 1.2.0 on AWS (an m7g.xlarge managing 1,002 agents), with REMOTE_HOST set: native nmap, testssl.sh, nuclei (14,039 templates, 0 matches) and a signed-in ZAP API scan (111 rule sets, 0 failures). Every release check also runs the scan against a fresh install. The run described below: 16 October 2026, Gardience 1.2.0, a fresh install from the customer bundle (test ports 9643/9680 for the console and 16505/16506 for agents; the defaults are 443/80 and 4505/4506), signed in as an administrator.
Tools and coverage
| Tool | What it checks | Result |
|---|---|---|
| OWASP ZAP API scan (authenticated) | Active attacks (injection of every kind, XSS, path traversal, command injection, SSRF, header and parameter tampering) against all 81 console and API paths (89 operations), described to ZAP as an OpenAPI file generated from the running app and called with an admin session | 0 failures, 112 rule sets passed |
| OWASP ZAP baseline | Passive review of every page and response | No high findings; header items fixed (below) |
| nuclei (14,039 templates, updated at each run) | Known CVEs, exposed panels, default credentials, misconfigurations | 0 matches |
| testssl.sh 3.2.4 | TLS protocols, ciphers and known TLS attacks | TLS 1.2 and 1.3 only, forward secrecy, AEAD ciphers only, server cipher order, HSTS; not vulnerable to Heartbleed, ROBOT, CRIME, BREACH, POODLE, SWEET32, FREAK, DROWN, LOGJAM, BEAST, LUCKY13 or RC4 |
| nmap | Which ports the server exposes, plus the internal service ports | Only the console (HTTPS, and HTTP that redirects) and the two Salt agent ports are open; salt-api (8000), the Gardience service (9400) and the playbook runner (9500) are closed from outside |
| Bandit | Static analysis of all Gardience Python code and Salt modules (a gate in CI) | 0 high severity |
| Trivy | Known vulnerabilities in the three container images the release builds (OS packages and Python, including vendored copies) | 0 fixable high or critical, with one stated exception: a finding in a library bundled inside pip itself (pip/_vendor) passes only when the image's installed package of that name is a different, fixed version. pip runs only during a project sync, with its cache off and no proxy settings. Each accepted finding is printed in the release check log. The Salt master image ships without pip. |
| pip-audit | Known vulnerabilities in every Python package each built image contains (read from the image, not re-resolved from the requirements files) | 0 |
OWASP WSTG attack checks (tools/pentest/wstg.py, every release) | The attacks a pen tester runs by hand, by WSTG chapter: leakage, headers, CORS, enumeration, default accounts, brute force per address and per account, cookie flags, CSRF, token handling, every API route without signing in and as a viewer, governed admin limits, broken bodies on every route, path traversal, SSRF, reflected XSS, open redirects, oversized bodies, caching. Map: PENTEST-READINESS.md | All pass |
Salt master exploits (tools/pentest/salt-cves.py, every release) | CVE-2020-11651 and CVE-2020-11652 requests as the Metasploit modules send them, against the live master | All 6 refused |
Stored XSS from a compromised machine (tools/tests/ui/xss.sh) | Plants script-carrying markup in a live agent's facts, then checks 24 console pages in Chrome | Shown as text; never an element, never run |
| Our own attack tests (every release) | SSO response forgery, replay and theft; viewer and operator permission limits; playbook sandbox escape between runs; audit-log forgery, deletion and truncation; minion-forged job events; hostile drift-reactor events; tampered and replayed release bundles; split-brain fencing in the HA drill | All rejected (see the release gate) |
Found by the pen-test readiness checks and fixed (17 October 2026)
- Cross-site request forgery on Salt's API (high). salt-api accepted a form post carrying only the session cookie, so a malicious page could send Salt calls as a signed-in user. Changes to Salt's API now need the console's token header, which a browser never adds to another site's request, and the cookie is Secure, HttpOnly and SameSite=Strict.
- Server-side request forgery through admin settings (medium). The webhook accepted any internal
http://gardience:address, including internal-only endpoints, and the webhook and ticket check could target the stack's own services. One guard now refuses loopback, link-local, cloud metadata, multicast and the stack's services, at save and at call time, connects to the checked address, and does not follow redirects. - Break-glass opening could go unaudited (medium). An opening shorter than the 30-second check left no record. Every opening and closing is now logged with the account and always audited.
- Formula injection in CSV exports (low). Text cells starting with = + - @ are prefixed with an apostrophe.
- No per-account sign-in throttle (low). 10 failures in 15 minutes now pause that account from any address.
- No Cache-Control on console and API answers (low). Now
no-store. - A viewer could flood the fleet with jobs (medium). Each click on "refresh inventory" or "check drift" started a new fleet-wide job. Both now run one at a time, however often anyone clicks.
- A viewer could post test messages into the team's chat channel (low). Admins only now.
- A read-only admin could read the LDAP bind password (medium, found by a separate review). Salt's config read returned the master's whole configuration. Every secret in it is now blanked (harden.py).
- Salt's web server version in error pages (low). Sign-in failures and Salt API errors showed "CherryPy 18.10.0". The front door and the sign-in now answer errors themselves, with the same status codes.
- Backups were unencrypted unless a passphrase was given (medium for an audit). Now encrypted by default with the server's own backup key;
--plaintextonly when asked for by name. - No second factor for local accounts (an audit exception). One-time codes from an authenticator app, enforceable per role.
- A fixable high CVE in the web server image (libexpat, CVE-2026-77214). The image now takes Alpine's security updates at build; CI fails the build on any fixable high or critical.
- Wrong-shaped request bodies caused server errors (low). A JSON list or string where an object belongs gave a 500 on 12 routes, and a body nested thousands of levels deep exhausted the parser. Every route now answers 400.
Found by a separate code review and fixed (October 2026)
An adversarial code review, run separately from development (not a third-party penetration test), read the code and test logs and reported what our own tests had missed. Each finding was verified against the code, and the confirmed ones fixed and where possible covered by a new test:
- Command injection through the drift reactor. A compromised machine could send a crafted service name and have the Salt master run a command on every machine. The reactor now accepts only plain names of services an admin set up for watching, and only on the machine that reported it (tools/tests/reactor/run.sh).
- A user named "automation" could run playbooks unchecked. Service identities are now a code-only type, the internal names are reserved for sign-in and account creation, read-only roles cannot create change keys, and only admins can create keys that skip approval.
- Warm standby could take the fleet during a primary restart, and fencing was weak. The standby now runs no Salt master until promoted; primary and standby publish a signed role and generation, a returning old primary fences itself until an operator acts, and sync jobs refuse to overwrite a newer server (HA drill: restart, failover, fencing, refused sync, failback).
- Change windows did not apply to workflows, schedules, held work released on reconnect or drift auto-correct. They do now; only an admin's recorded override bypasses them.
- Release gate could pass on failures. Every suite's exit code and result line now count, a gate run must reach its end marker, wrong results fail, and a release must come from a committed tree.
- Minions could imitate job events (fake audit entries, fake fix results). Only master-published job starts and genuine returns from the named machine are trusted.
- Secrets could silently not reach machines at scale. Vault lookups are cached and indexed; a failed lookup now fails the Salt pillar loudly instead of returning nothing.
- Inconsistent backups, unauthenticated encryption, plaintext pre-upgrade backups, a leaky support bundle, audit writes that could fail silently, SIEM gaps, queue items dropped past 300, events visible to every role, a login rate-limit bypass, unaudited sign-ins, fleet-wide fixes without a second person, exports that froze the console, and heavy 5-minute scans. Fixed; details in the release notes. A second pass of the review found problems in these fixes (readiness that never recovered, standby upgrades, key rollback on promotion, unscored nodes counted as compliant, forged batched events, test builds signed with the release key, a broken two-person fix approval); those were fixed in turn and are covered by the HA drill, the unit tests and CI.
Found and fixed in the test itself (this round)
- The TLS scan's container image cannot run on ARM servers; on one run it printed an error instead of scanning, and the old script counted "no weak protocol found" as a pass. The script now uses a native testssl.sh where installed and fails unless the scan actually completed.
- The nuclei step misread an empty result file as a finding. It now also fails if the template set did not download, so "0 matches" always means a real scan.
Found and fixed in the product (previous round)
- Malformed requests caused server errors (28 endpoints). An empty or non-JSON body raised an unhandled error (HTTP 500). Now: invalid JSON returns 400, permission problems return 403, and any unexpected error returns a generic message with a reference number while the details go only to the server log.
- Sign-in passthrough relayed raw input and Salt errors. It now accepts only well-formed sign-ins and never passes a Salt error page through.
- Cross-origin headers. salt-api sent
Access-Control-Allow-Origin: *on console pages; nginx now strips it. - Security headers on every response, including 404s: HSTS, Content-Security-Policy, Permissions-Policy, Cross-Origin-Opener/Resource/Embedder-Policy, nosniff, frame denial, no referrer.
- TLS: older CBC cipher suites removed (Mozilla "intermediate" profile), server cipher order enforced, session tickets off.
- nginx image: the previously pinned image carried 43 fixable high/critical CVEs; moved to nginx 1.30.6, pinned by digest, now scanned every release.
- Static analysis: a lab-only mock file path moved out of world-writable /tmp; the gdn command-line tool can now trust a specific server certificate (
--ca) instead of turning verification off; event-stream connections time out if they cannot connect; the vault pillar module refuses non-HTTP URLs.
Accepted, with reasons
- CSP allows inline styles (
style-src 'unsafe-inline'). The console and the Salt view use inline styles. Scripts are locked to the server and a per-response nonce, so injected markup cannot run code; style injection alone cannot read data or act as the user. - The Salt view's bundled jsonpath library uses
evalon query expressions typed by the signed-in user in their own browser. Low risk; tracked for replacement. - "Source code disclosure" at /gardience/cli/gdn is the gdn command-line tool, published there on purpose for users to download.
- Self-signed certificate on a fresh install. Replace it with your own (installer
--tls-cert/--tls-key). - "PII Disclosure" (credit card) on the audit pages when a run of digits inside one of the audit chain's SHA-256 hashes happens to pass the card-number checksum (seen 2026-10-28:
67446496357042, inside a hash on /gardience/audit/verify). The check fetches the page again and accepts it only if every occurrence of each flagged number sits inside a hex run of 32 or more characters; the same number anywhere else still fails.
Not covered by this report
An independent third-party penetration test (see PENTEST-READINESS.md for what one would try and what stops it). Customers who require one can run their own against a trial install; we fix and retest anything found.