Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 52 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ A RADIUS server only logs the requests that *reach* it. A huge class of 802.1X f

```console
$ export AUTHHOUND_SECRET='shared-secret' # kept out of shell history and `ps`
$ authhound-probe radius test --server radius.corp.com --peap alice
$ authhound-probe radius test --server radius.corp.com --peap alice --server-name radius.corp.com
Enter password for alice:

Testing RADIUS server radius.corp.com:1812 (as NAS "authhound-probe")
Expand All @@ -21,12 +21,12 @@ PASS RADIUS server answered in 23ms
PASS Shared secret is correct (reply signature verified)
PASS Server signs its replies with Message-Authenticator (BlastRADIUS-hardened)
PASS PEAP-MSCHAPv2 authentication succeeded for alice
PASS Server certificate valid for 214 more days, chain looks complete (TLS 1.2)
PASS Server certificate valid for 214 more days, name matches "radius.corp.com", chain looks complete (TLS 1.2)

Verdict: 5 passed, 0 failed, 0 warnings
```

Like `eapol_test` or `radtest`, but the output is readable — one command, no `wpa_supplicant` config file. Add `--json` for scripting, `--nas-port-type ethernet|wireless|virtual` to match how your real NAS presents itself, and `--server-name` to set the expected certificate name.
Like `eapol_test` or `radtest`, but the output is readable — one command, no `wpa_supplicant` config file ([honest comparison](docs/COMPARISON.md)). Add `--json` for scripting, `--nas-port-type ethernet|wireless|virtual` to match how your real NAS presents itself, and `--server-name` to validate the server certificate's name the way your clients will. Common questions: [FAQ](docs/FAQ.md).

## Step 0 — register the probe on your server (one time)

Expand Down Expand Up @@ -66,7 +66,7 @@ Skipped this step? The probe notices: on a first-run timeout it prints this exac
| **EAP-TTLS (PAP)** | A real login inside the TTLS tunnel using inner PAP. Because the password is checked in cleartext (safe inside the tunnel), TTLS-PAP works against *any* backend — including hashed stores that MSCHAPv2 can't use. If PEAP-MSCHAPv2 fails but this passes, the directory can't produce an NT hash. |
| **EAP-TLS** | Certificate-based login (no password): presents a client certificate and reports whether the server accepts it — with a plain-English reason on failure (untrusted CA, expired cert, policy reject). See [EAP-TLS: preparing a client certificate](#eap-tls-preparing-a-client-certificate). |
| **Authorization / VLAN** | On any successful login, decodes and prints the authorization the Access-Accept returned (VLAN, Filter-Id, Session-Timeout, vendor attributes) — and lets you **assert** on it with `--expect-vlan` / `--expect-attr` so "auth works, wrong VLAN" fails loudly. See [Verifying policy](#verifying-policy-not-just-connectivity). |
| **Server certificate** | Establishes the PEAP/TLS tunnel over RADIUS, captures the server's certificate, and flags **expiry**, an incomplete intermediate chain, and the negotiated TLS version. The "Wi-Fi died overnight" outage, caught early. |
| **Server certificate** | Establishes the PEAP/TLS tunnel over RADIUS, captures the server's certificate, and flags **expiry**, an incomplete intermediate chain, a **name mismatch** against `--server-name` (FAIL — clients validating that name would reject the handshake), and the negotiated TLS version. Without `--server-name`, name validation is **skipped and reported as a WARN** — the probe never claims "valid" for a name it didn't check. The "Wi-Fi died overnight" outage, caught early. |
| **Path MTU / fragmentation** (`--mtu`) | Finds the largest RADIUS packet that survives the round trip. Pinpoints the invisible failure where a firewall or VPN drops large / IP-fragmented UDP, so the multi-kilobyte EAP-TLS certificate flight never arrives and 802.1X silently stalls — while every server-side log looks clean. |
| **RadSec** (`radsec test`) | Checks a RADIUS/TLS endpoint on TCP/2083: reachability, TLS handshake, server certificate, and a RADIUS exchange over the tunnel. For modern deployments and UDP→RadSec migration readiness. |

Expand Down Expand Up @@ -209,7 +209,7 @@ $ authhound-probe radsec test --server radius.corp.com \
| `--count N` | Run the checks `N` times (2–50) and report aggregate statistics — see [Chasing intermittent failures](#chasing-intermittent-failures). |
| `--interval DURATION` | Pause between `--count` iterations (default `2s`; a hard-coded safety floor applies). |
| `--nas-port-type wireless\|ethernet\|virtual` | How the probe presents itself, so server policies match (default `wireless`). |
| `--server-name NAME` | Expected server-certificate name (TLS SNI). |
| `--server-name NAME` | Name the server certificate must be valid for (also sent as TLS SNI). Mismatch = **FAIL**; omitted = name validation skipped, reported as a **WARN** — see [Certificate name validation](#certificate-name-validation---server-name). |
| `--nas-id NAME` | NAS-Identifier to send (default `authhound-probe`). |
| `--timeout DURATION` | Per-request timeout (default `5s`). |
| `--bind IP[:port]` | Source IP to send from, for pinning the outgoing interface on a multi-homed host — see [Binding a source interface](#binding-a-source-interface---bind). |
Expand Down Expand Up @@ -396,6 +396,39 @@ the probe prints on a timeout automatically reflects the bound IP, so you can
paste it as-is. (If NAT sits between this host and the server, the server still
sees the post-NAT address — register that instead.)

### Certificate name validation (`--server-name`)

Your clients don't just check that the RADIUS server's certificate is unexpired
— a correctly configured 802.1X profile also validates the certificate's
**name**. A server presenting a healthy certificate with the *wrong* name still
breaks every client that validates it. So the probe treats the name as part of
the certificate verdict:

- **`--server-name radius.corp.com` and the certificate matches** (SAN rules,
wildcards included — the same matching real clients do): PASS, and the
summary says the name matched.
- **`--server-name` given but the certificate doesn't match**: **FAIL**, with
the names the certificate is *actually* valid for, so you can tell "wrong
cert selected on the server" from "my expected name is stale". A certificate
with no SAN at all fails too — modern clients don't fall back to the CN.
- **`--server-name` omitted**: the probe cannot know what name your clients
expect, so name validation is **skipped — and reported as a WARN**, never
folded into a silent "valid". Expiry and chain are still checked; the WARN
tells you exactly what to add. In `--json`, `fields.name_validation` is
`"match"`, `"mismatch"`, or `"skipped"` on the `server-cert` / `radsec-cert`
results.

Note the division of labour: in the authentication checks (PEAP/TTLS/EAP-TLS),
`--server-name` is only sent as TLS SNI — the probe deliberately completes
those handshakes even against a broken certificate, because its job is to
diagnose rather than refuse. The name *verdict* lives in the `server-cert`
(and `radsec-cert`) check. The full TLS posture, including why verification is
capture-and-report by design, is documented in
[SECURITY.md](SECURITY.md#tls-and-certificate-posture--an-honest-note).

Under `--strict`, the skipped-name WARN exits `1` — deliberate: a scheduled
monitor should be told which name to pin, not silently skip the check forever.

### Where credentials come from

This tool is meant to run on shared jump boxes, so it never *requires* a secret or
Expand Down Expand Up @@ -480,17 +513,10 @@ Expand-Archive -Path authhound-probe.zip -DestinationPath authhound-probe -Force
any directory, move the `.exe` somewhere on your `PATH` — e.g.
`C:\Windows\System32` for all users, or a folder you add to your user `Path`.)

Or via a package manager — see [`packaging/`](packaging/) for the manifests and
maintainer notes:

```powershell
# Scoop (from the AuthHound bucket)
scoop bucket add authhound https://github.com/authhound/scoop-bucket
scoop install authhound-probe

# winget
winget install authhound.probe
```
Scoop and winget packages are **in progress** — the manifests are maintained in
[`packaging/`](packaging/), but the Scoop bucket and the winget submission
haven't been published yet. Until they land, use the PowerShell download above.
(This note gets replaced with the install one-liners once they're live.)

**Go:**

Expand Down Expand Up @@ -578,7 +604,7 @@ The probe is a single `.exe` with no runtime, so it drops straight into Task Sch
# the command line (it would be visible in the task definition).
$action = New-ScheduledTaskAction `
-Execute 'C:\Tools\authhound-probe.exe' `
-Argument 'radius test --server nps.corp.local --peap svc-radius-probe --json --strict' `
-Argument 'radius test --server nps.corp.local --server-name nps.corp.local --peap svc-radius-probe --json --strict' `
-WorkingDirectory 'C:\Tools'
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15)
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Expand All @@ -589,7 +615,7 @@ Or the classic one-liner with `schtasks`, redirecting output to a rolling log:

```powershell
schtasks /Create /TN "AuthHound RADIUS probe" /SC MINUTE /MO 15 /RU SYSTEM /RL HIGHEST /TR ^
"cmd /c C:\Tools\authhound-probe.exe radius test --server nps.corp.local --peap svc-radius-probe --json --strict >> C:\Tools\probe.log 2>&1"
"cmd /c C:\Tools\authhound-probe.exe radius test --server nps.corp.local --server-name nps.corp.local --peap svc-radius-probe --json --strict >> C:\Tools\probe.log 2>&1"
```

Supply credentials via the environment for the task's account (`AUTHHOUND_SECRET`, `AUTHHOUND_PASSWORD`) or `--secret-file`/`--password-file` pointing at a file only that account can read — see [Where credentials come from](#where-credentials-come-from). Because the probe exits `1` on failure (or on a warning under `--strict`), Task Scheduler's **Last Run Result** reflects health directly: `0x0` healthy, `0x1` something needs attention. Point your existing task-result monitoring at that, or tail `probe.log`.
Expand All @@ -605,7 +631,10 @@ Output is colourised only when it's going to a real terminal that can render it,

## Safety

Built to be safe to run against production, and to pass an enterprise security review:
Built to be safe to run against production, and to pass an enterprise security
review. The one-page threat model — what the probe sends, what it stores
(nothing), what it never does, and how to report a vulnerability — is
[SECURITY.md](SECURITY.md); the short version:

- **Read-only.** Sends Access-Requests and reads the replies — it never changes anything on the server, and never captures packets.
- **Credentials stay local, and off the command line.** The shared secret and any password are used only to build the RADIUS packets. They are never written to output, `--json`, logs, or error messages, and nothing is ever sent anywhere (there is no telemetry). So they don't leak into shell history or `ps` on a shared box, they're read from a file (`--secret-file` / `--password-file`, refused if world-readable), an environment variable, standard input, or a no-echo prompt — see [Where credentials come from](#where-credentials-come-from). The plain `--secret`/`user:pass` forms still work but warn on a terminal.
Expand Down Expand Up @@ -645,7 +674,10 @@ Interop reports are especially valuable: run the probe against your RADIUS — a

## See also

Already have a log to read? Paste FreeRADIUS debug output or a Windows NPS event into the free [RADIUS log analyzer](https://authhound.com/analyzer) for a plain-English diagnosis.
- **[FAQ](docs/FAQ.md)** — timeouts, NPS, credentials, scripting.
- **[authhound-probe vs eapol_test vs radtest](docs/COMPARISON.md)** — a factual comparison, including when the classics are the better tool.
- **[SECURITY.md](SECURITY.md)** — the one-page threat model and vulnerability reporting.
- Already have a log to read? Paste FreeRADIUS debug output or a Windows NPS event into the free [RADIUS log analyzer](https://authhound.com/analyzer) for a plain-English diagnosis.

## License

Expand Down
122 changes: 117 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,123 @@
# Security

You run this tool inside your network with your RADIUS shared secret. That
only works if you can trust what's in the binary — this page documents how we
keep that trust checkable rather than asking you to take our word for it.
You run this tool inside your network with your RADIUS shared secret, often on
a shared jump box. That only works if you can answer "what does this thing do
to my network and my secrets" without reading the code. This page is that
answer — the threat model first, then how to report a problem, then how we
keep the supply chain checkable.

<!-- Vulnerability reporting / disclosure contact section maintained
separately (WS-10); the sections below cover supply chain. -->
## What the probe sends

- **RADIUS traffic to the server you name, and nothing else.** Access-Request
and Status-Server packets out (UDP/1812 for `radius test`, TLS on TCP/2083
for `radsec test`), replies in. Exactly what a switch or access point would
send — the probe *is* a NAS as far as the server can tell.
- **No telemetry, no phone-home, no update check.** The free tool never
contacts authhound.com or any other host. The only network peer of a run is
the RADIUS server on the command line. (`connect`, the paid tier, is
explicitly opt-in and prints what it would do; in this open-source tool it
does nothing else.)
- **Bounded rate.** A hard-coded rate ceiling in the runner caps how fast
packets leave, and `--count` is capped at 50 iterations with an enforced
interval floor. There is no flag, environment variable, or config file that
raises either — the probe cannot be turned into a load generator or used to
hammer someone else's server.

## What it stores

Nothing. The probe is a one-shot process:

- no config files written, no cache, no history, no state directory;
- no daemon or watch mode — when the run ends, the process and everything it
knew are gone;
- secrets read from a file (`--secret-file` / `--password-file`) stay on the
file you own; the probe only ever reads them.

## What it never does

- **Never captures packets** or opens raw sockets — it sends its own requests
and reads its own replies, nothing else on the wire.
- **Never changes anything on the RADIUS server.** Authentication requests
are read-only from the server's point of view; the probe has no
provisioning, CoA, or accounting-write capability.
- **Never completes a second factor.** If the server issues an MFA challenge,
the probe reports that boundary and stops — completing a push/OTP from an
unattended tool would mean holding a live MFA secret, which it refuses to do.
- **Never proxies or forwards authentication** for anything else.
- **Never sends your secrets anywhere** except inside the RADIUS/EAP exchange
they are for (see below).

## How credentials are handled

- **Inputs stay off the command line by default.** The shared secret and any
password can come from a file (refused if group/world-readable on unix), an
environment variable, standard input, or a no-echo interactive prompt — the
inline `--secret`/`user:pass` forms work but print a warning on a terminal,
because they leak into shell history and `ps`. Precedence and details:
[README — Where credentials come from](README.md#where-credentials-come-from).
- **Secrets are used only to build protocol messages.** The shared secret
signs/validates RADIUS packets; passwords are hidden per RFC 2865 (PAP) or
used inside the TLS tunnel (PEAP/TTLS) or as MSCHAPv2 responses — never
transmitted in the clear outside the protocol that carries them.
- **Secrets never appear in output.** Not in text output, `--json`, hints,
error messages, or panics. This is enforced by tests (including a dedicated
leak test in `cmd/authhound-probe/leak_test.go`) and asserted again by the
Docker smoke test, which greps every output surface for the secret.
- **The EAP-TLS/RadSec private key** is read from disk only to complete the
TLS handshake; it is never transmitted (TLS never sends private keys) or
logged.
- Environment variables are convenient but readable by other processes running
as the same user — for hostile multi-user boxes, prefer a `chmod 600` file
or the prompt.

## TLS and certificate posture — an honest note

The probe's TLS handshakes (PEAP/TTLS/EAP-TLS tunnels and RadSec) deliberately
run with certificate verification disabled at the TLS layer, because the probe
is a *diagnostic*: its job is to capture the certificate the server presents
and report on it — expiry, chain completeness, name — even (especially) when
that certificate is broken. A normal TLS client would abort on the broken cert
and tell you nothing.

What that means in practice:

- The dedicated certificate checks (`server-cert`, `radsec-cert`) do the
verification *as reporting*: expiry and chain problems FAIL/WARN, and the
certificate name is validated against `--server-name` — a mismatch is a
FAIL. **If `--server-name` is omitted, name validation is skipped and the
probe says so with a WARN** (`name_validation: "skipped"` in `--json`); it
never reports an unqualified "valid" for a name it didn't check.
- The credential-carrying checks (PEAP/TTLS) therefore complete their exchange
even against a server presenting an untrusted certificate. The inner
credential is protected by the method itself (MSCHAPv2 challenge-response
never sends the password; TTLS-PAP sends it only inside the tunnel), but a
sufficiently positioned attacker who can intercept RADIUS *and* knows your
shared secret could present their own tunnel endpoint. Use a dedicated
least-privilege test account — the README says this everywhere credentials
come up — and treat `--server-name` plus the server-cert check as your
impersonation tripwire.

## Verify what you downloaded

Every release artifact is checksummed and keyless-signed with Sigstore cosign,
bound to this repo's tagged release workflow via OIDC and logged in the public
Rekor transparency log — you can prove a binary came from our CI and was not
tampered with, without trusting a long-lived key. Step-by-step commands
(including Windows): [README — Verify your download](README.md#verify-your-download).
Every archive also ships an SPDX SBOM, so when a CVE drops you can check your
exposure in seconds.

## Reporting a vulnerability

- Use GitHub private vulnerability reporting — [Security →
"Report a vulnerability"](https://github.com/authhound/probe/security/advisories/new)
on this repo. It reaches the maintainer privately and tracks the fix.
- Please don't open a public issue for anything you believe is exploitable.

You'll get an acknowledgement within 72 hours. Confirmed vulnerabilities get a
GitHub Security Advisory and a patch release; the SBOMs let you determine your
exposure without waiting on us. We won't take legal action against good-faith
research done against your own infrastructure.

## Dependencies & supply chain

Expand Down
Loading
Loading