diff --git a/CHANGELOG.md b/CHANGELOG.md index 5144ca0..2ddaf37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,10 @@ Kerberos: there is no message protection — the payload travels as plaintext SOAP. The scheme is accepted over both transports, but it must be used over **HTTPS** in practice, where TLS protects the credential and the payload (over plain HTTP both travel in the clear). -* A domain-qualified user name (`DOMAIN\user`) keeps its domain prefix on the wire; the server - must have the `Basic` setting enabled on the WinRM service (`winrm/config/service/auth`). +* On Windows, WinRM accepts Basic for **local accounts only**, addressed by their **bare user + name**: a domain account — or even a local account written as `MACHINE\user` — is rejected + with a 401 (verified against a real host). The server must have the `Basic` setting enabled + on the WinRM service (`winrm/config/service/auth`). * `BASIC` joins `AuthenticationEnum` (legacy API) and participates in the ordered-fallback list like the other schemes. diff --git a/src/main/java/org/metricshub/winrm/light/BasicAuthScheme.java b/src/main/java/org/metricshub/winrm/light/BasicAuthScheme.java index 625fd3b..216d0a2 100644 --- a/src/main/java/org/metricshub/winrm/light/BasicAuthScheme.java +++ b/src/main/java/org/metricshub/winrm/light/BasicAuthScheme.java @@ -55,10 +55,12 @@ final class BasicAuthScheme extends PlaintextSoapAuthScheme { private final char[] password; /** - * @param username the account name (a domain-qualified name keeps its domain prefix, which is - * how the server locates the account). The caller passes an already whitespace-stripped - * account (the service rebuilds it from the endpoint's normalized domain/username parts), - * so it is used verbatim here. + * @param username the account name, used verbatim. A qualified name keeps its prefix on the + * wire, but Windows WinRM accepts Basic for bare local account names only and 401s any + * {@code DOMAIN\}/{@code MACHINE\}-qualified form (the qualified form is still sent + * as-is for non-Microsoft WSMan services). The caller passes an already + * whitespace-stripped account (the service rebuilds it from the endpoint's normalized + * domain/username parts). * @param password the account password, kept as {@code char[]} so the caller owns the single * wipeable copy of the secret */ diff --git a/src/main/java/org/metricshub/winrm/light/LightWinRMService.java b/src/main/java/org/metricshub/winrm/light/LightWinRMService.java index ab4370d..400d5ab 100644 --- a/src/main/java/org/metricshub/winrm/light/LightWinRMService.java +++ b/src/main/java/org/metricshub/winrm/light/LightWinRMService.java @@ -290,7 +290,9 @@ private static AuthScheme resolveAuthScheme( // over both transports. Rebuild the account from the whitespace-normalized // domain/username parts (the same account NTLM/Kerberos use), domain-qualified when // the endpoint was given one — the endpoint's raw username is kept verbatim for - // public-API stability and is intentionally NOT used here. + // public-API stability and is intentionally NOT used here. Windows WinRM accepts + // Basic for bare local account names only (a qualified name gets a 401), but the + // qualified form is passed through unchanged for non-Microsoft WSMan services. final String basicAccount = domain != null ? domain + "\\" + username : username; schemes.add(new BasicAuthScheme(basicAccount, password)); } else { diff --git a/src/site/markdown/authentication.md b/src/site/markdown/authentication.md index a20c745..7bb04ac 100644 --- a/src/site/markdown/authentication.md +++ b/src/site/markdown/authentication.md @@ -100,16 +100,19 @@ handshake and no message protection, so the payload travels as plaintext SOAP. I transports, but over plain HTTP the credential and the data are sent **in the clear**: use Basic over HTTPS only, where TLS protects both. -The credential is the user name, **with all whitespace removed** before it is sent: a -`DOMAIN\user` account is rebuilt as `DOMAIN` + `\` + the account (the backslash is where the domain -and account are split on), and a bare name is sent as-is. Windows account names contain no -whitespace, so the removal is a no-op in practice — but if you supply one, the header carries the -whitespace-stripped account, not the exact string you typed. +On Windows, WinRM accepts Basic for **local accounts only**, addressed by their **bare user +name**: a domain account is rejected, and so is a *local* account written with a qualifying +prefix — `MACHINE\user` or `DOMAIN\user` gets a `401` even when the password is correct +(verified against a real host). Use `credentials("user", password)`, not +`credentials("MACHINE\\user", password)`. The client itself does not reject a qualified name — +some non-Microsoft WSMan services accept one — and sends the account with all whitespace +removed: a `DOMAIN\user` value is rebuilt as `DOMAIN` + `\` + the account, and a bare name is +sent as-is. ```java try (WinRMClient client = WinRMClient.builder("server.example.com") .https() - .credentials("DOMAIN\\Administrator", password) + .credentials("Administrator", password) // Windows: a bare LOCAL account name .authentication(AuthScheme.BASIC) .build()) { ... @@ -124,6 +127,14 @@ TLS provides the confidentiality. Over plain HTTP — which, as noted, should no service would additionally have to set `AllowUnencrypted=true` (otherwise it refuses the unprotected SOAP), which is exactly what the HTTPS recommendation exists to avoid. +Note that most server-side refusals above surface as the same `401`: a Basic authentication +error can mean a wrong password, but also a domain-qualified or domain account, or `Basic` +disabled on the service — check the configuration before suspecting the credential. Unencrypted +HTTP with `AllowUnencrypted=false` is refused too, but not always as a `401`: depending on the +Windows version, the service may authenticate the credential and then reject the unprotected +SOAP with a WSMan fault (a `401` was observed on Server 2008 R2, a fault is reported on later +versions), so that misconfiguration can surface as either an authentication error or a fault. + ## Authentication failures A rejected credential (after every scheme of the fallback list was tried) surfaces as a diff --git a/src/site/markdown/cli.md b/src/site/markdown/cli.md index 3b2dbf5..581545c 100644 --- a/src/site/markdown/cli.md +++ b/src/site/markdown/cli.md @@ -82,6 +82,8 @@ requires `--kerberos-kdc`. `--basic` authenticates with HTTP Basic, sending the credential in the `Authorization` header of every request. It has no message protection, so the credential and payload are plaintext over HTTP — combine it with `--https` (and `--https-permissive` for self-signed hosts) so TLS protects them. +On Windows, use a **bare local account name** with `-u`: the WinRM service rejects Basic for +domain accounts and for any `DOMAIN\`-qualified name. See [Authentication](authentication.html) for how NTLM, Kerberos, and Basic work on the wire. diff --git a/src/site/markdown/migrating-from-winrm4j.md b/src/site/markdown/migrating-from-winrm4j.md index 66b2b4b..0d82657 100644 --- a/src/site/markdown/migrating-from-winrm4j.md +++ b/src/site/markdown/migrating-from-winrm4j.md @@ -139,7 +139,10 @@ the switch: protection: it accepts the scheme over both transports, so **use `https()` with it** — without TLS, the credential and payload travel in the clear and the client will not stop you. The host must have the `Basic` setting enabled on the WinRM service and be reachable over HTTPS - ([Preparing the Windows Host](preparing-the-host.html)). NTLM remains the recommended scheme. + ([Preparing the Windows Host](preparing-the-host.html)), and the credential must be a **bare + local account name** — Windows rejects Basic for domain accounts and for any + `DOMAIN\`/`MACHINE\`-qualified name ([Authentication](authentication.html)). NTLM remains the + recommended scheme. * **Kerberos requires HTTPS.** winrm4j runs Kerberos over plain HTTP; this client refuses at `build()`, because it does not implement Kerberos message encryption — without TLS the payload would travel unprotected. Connect with `https()` and by the FQDN the KDC knows diff --git a/src/site/markdown/preparing-the-host.md b/src/site/markdown/preparing-the-host.md index e0fdbe3..8ffbaf6 100644 --- a/src/site/markdown/preparing-the-host.md +++ b/src/site/markdown/preparing-the-host.md @@ -37,8 +37,9 @@ here: it refuses the unprotected SOAP — see [Authentication](authentication.html).) * **The service's `Basic` and `CredSSP` settings stay `False` unless you use HTTP Basic.** NTLM and Kerberos need neither. To use HTTP Basic, enable the `Basic` setting under the service's - `auth` section — `winrm set winrm/config/service/auth @{Basic=true}` — and connect over HTTPS - ([Authentication](authentication.html)). + `auth` section — `winrm set winrm/config/service/auth @{Basic=true}` — connect over HTTPS, and + authenticate with a **bare local account name**: Windows rejects Basic for domain accounts and + for any `DOMAIN\`-qualified name ([Authentication](authentication.html)). * **`TrustedHosts` is irrelevant.** That is a setting on the *Windows* WinRM **client**, consulted by the `winrs` command-line tool. A Java client never reads it, so you do not need to add anything to it on either machine.