You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(winrm)!: refuse to send a password over plain HTTP - #516
winrm now refuses a password on a transport that does not protect it, and insecure is widened from "accept invalid TLS certificates" to "do not refuse an unprotected transport".
Config
Before
After
password, plain HTTP
connects
ErrValidationFailed
password + useNTLM, plain HTTP
connects
ErrValidationFailed
password, plain HTTP, insecure
connects
connects
password, useHTTPS (or port 5986)
connects
connects
password, useHTTPS + insecure
connects
connects
certPath/keyPath, useHTTPS
connects
connects
certPath/keyPath, plain HTTP
cert silently ignored
ErrValidationFailed
insecure carries the decision because both readings describe the same caller: one who has accepted that the transport need not protect what it carries. On plain HTTP it previously did nothing, so nobody who set it for TLS gets the new meaning by surprise. Its schema description now states both effects.
Where it is enforced
Nothing in rig calls a protocol config's Validate() on the connection path, so the rule runs in three places:
Config.Validate() (and through it CompositeConfig.Validate())
NewConnection, after SetDefaults, so rig.NewClient(WithConnectionFactory(...)) fails before any request
Connection.Connect, because Connection.Config is exported and mutable and a Connection can be built without NewConnection
On the connection path the error wraps both ErrValidationFailed and ErrNonRetryable.
Port 5986 counts as HTTPS everywhere (validation and the endpoint Connect builds), matching what SetDefaults does, so a config validated or connected before SetDefaults is judged by the transport it will actually use.
NTLM is not an exemption
NTLM never sends the password itself, but the WinRM client does not seal the session: commands, output and any secret inside them stay readable, and the captured challenge response can be cracked offline or relayed. useNTLM therefore does not change the outcome, and gets its own error message rather than the basic-auth one about the password being sent.
Breaking changes
A config with a password, no useHTTPS (or port 5986) and no insecure now fails instead of connecting. Setting insecure: true keeps the previous behaviour.
certPath/keyPath without HTTPS now fails validation. The client certificate is only presented in a TLS handshake, so over plain HTTP it was silently ignored and the connection fell back to basic auth. The only config that worked before and fails now is certificate + password + insecure over plain HTTP, which authenticated with the password and never used the certificate.
Drive-by
fix(log): inject the logger into a Config pointer, not a copy: InjectLoggerTo was handed a Config value, whose method set excludes the pointer-receiver SetLogger, so the injection silently did nothing in both winrm and ssh. No symptom today because no Config method logs.
Also: Config.Connection() no longer returns a typed-nil *Connection inside a non-nil interface on error.
Testing
TestValidateRejectsExposedPassword: the matrix above, including port-5986 cases validated without SetDefaults, the NTLM message, and that no error repeats the password
TestConnectionRejectsExposedPassword: Config.Connection() refuses with both sentinels
TestConnectRefusesPasswordOverPlainHTTP: password set after NewConnection, and a directly constructed Connection; the stub server receives no requests
TestNewEndpointScheme: endpoint scheme for 5985, 5986 without useHTTPS, and custom ports
TestClientWithConnectionFactoryRefusesWinRMPasswordOverHTTP: through rig.NewClient, no request reaches the stub server
TestNewConnectionInjectsLoggerIntoConfig in protocol/ssh and protocol/winrm
This PR adds advisory warnings for potentially exposed WinRM passwords without rejecting existing configurations. It also fixes configuration logger injection in both WinRM and SSH.
Changes:
Warn once per connection for password authentication over plain HTTP without NTLM or TLS with certificate verification disabled.
Inject loggers into configuration pointers rather than values.
Add tests for warning behavior, password redaction, and logger wiring.
File
Description
protocol/winrm/connection.go
Emits connection-time warnings once and fixes logger injection.
protocol/winrm/config.go
Classifies password-exposure configurations and supplies warning messages.
protocol/winrm/config_test.go
Tests exposure classification, warning behavior, and logger injection.
protocol/ssh/logger_test.go
Tests logger injection into SSH configuration and connection objects.
protocol/ssh/connection.go
Passes the configuration pointer to logger injection.
Maybe go a step further and make it an error? insecure defaults to false, so how about expanding it from "verify the certificate" to "don't send passwords over unprotected transports". Plain HTTP with basic auth would then be rejected unless insecure: true is set.
NewConnection passes the caller's logger down to the protocol Config with
options.InjectLoggerTo(cfg, log.KeyProtocol, "winrm-config")
where cfg is a Config value. log.InjectLogger asserts its argument to the
unexported injectable interface, whose SetLogger has a pointer receiver, so
a value's method set does not satisfy it. The assertion fails, the function
takes its else branch, and the injection silently does nothing. The same
line exists in both winrm and ssh.
Evidence it is a mistake rather than a deliberate no-op:
- The "winrm-config" and "ssh-config" attributes those two calls pass are
unreachable today. Nothing can ever log under them.
- log.InjectLogger's fallback emits "logger is not injectable" to the
trace logger. That is a diagnostic for a caller error, not a supported
path.
- ssh/connection.go performs the same injection on bastionCfg, which is a
*Config, and works. The broken and working sites differ in exactly one
thing: pointer versus value.
- No test asserted the current behaviour.
There is no observable symptom at present, because no Config method logs
anything. It is fixed because the next one would inherit the silence: a
Config that is handed a logger and quietly drops it is a trap for whoever
adds the first config-level log line.
Injecting before the Config is copied into the Connection means the
connection's embedded Config carries the logger too.
Tested by asserting the wiring directly, since no Config method logs. Both
tests were run against the unfixed line and fail there:
logger_test.go:26: the connection's Config has no logger, want the one
passed to WithLogger
Written by AI: claude-opus-5
Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
Widens insecure from "accept invalid TLS certificates" to "do not refuse
to expose the credential", and rejects a password on a transport that does
not protect it. Both senses describe the same caller: one who has decided
the transport need not protect what it carries.
password + plain HTTP -> ErrValidationFailed
password + plain HTTP + insecure -> allowed
password + HTTPS -> allowed
password + HTTPS + insecure -> allowed
certPath/keyPath, no password -> allowed
NTLM gets no exemption. An earlier revision of this branch excused it on
the grounds that it encrypts at the message level; the WinRM client
authenticates with NTLM but sends the SOAP envelopes unencrypted, so the
commands, their output, and any credential inside them stay readable. A
remedy that leaves the session in the clear is not a remedy.
Certificate authentication is unaffected. It is only selected over HTTPS
(connection.go only attaches the client certificate when UseHTTPS is set),
which this never rejects, so a cert-authenticated config cannot trip the
rule even when it also carries a password.
This replaces the advisory warning the branch previously carried. A
warning on a connection the caller is about to make anyway communicates
the problem at the moment it is too late to act on it, and costs a
sync.Once, a Connect-time hook and a function returning a message string
to say so. The check belongs with the other auth-shape rules in
validateAuth, which Validate already calls and CompositeConfig reaches
for every protocol config.
Breaking: an existing config with a password, no useHTTPS and no insecure
now fails validation instead of connecting. The message names insecure as
the one-line way to keep the old behaviour.
Written by AI: claude-opus-5
Signed-off-by: James Nesbitt <jnesbitt@mirantis.com>
james-nesbitt
changed the title
feat(winrm): warn when a password is sent over an unprotected transport
feat(winrm)!: refuse to send a password over plain HTTP
Nov 10, 2026
Done — made it an error, and dropped the warning machinery entirely. insecure now reads as "do not refuse to expose the credential" rather than "accept invalid TLS certificates", so plain HTTP with a password is rejected unless insecure: true is set. It is three lines in validateAuth alongside the other auth-shape rules; passwordExposure, the sync.Once and the Connect-time hook are gone.
The only reason it was a warning in the first place was to avoid a breaking behaviour change — that was the whole of the rationale, and it is not a good enough one. A warning arrives at the moment the connection is about to be made anyway, which is too late for the caller to act on it, and the cost of saying so was a function returning a message string plus the plumbing to decide when to emit it.
While reworking it I also dropped NTLM's exemption. The previous revision excused NTLM over plain HTTP on the grounds that it encrypts at the message level; it does not — the client authenticates with NTLM and still sends the SOAP envelopes unencrypted, so commands and their output stay readable. useNTLM no longer changes the outcome.
One thing worth flagging, since it is now arguably out of scope: the branch still carries the InjectLoggerTo(&cfg, ...) fix as its first commit, and the password check no longer depends on it. That fix existed because the warning needed a logger on the Config. Nothing on the branch calls Log() any more, so it is now an unrelated drive-by: InjectLoggerTo is handed a Config value whose method set excludes the pointer-receiver SetLogger, the type assertion in log.InjectLogger fails, and the injection silently does nothing in both winrm and ssh. There is no symptom today because no Config method logs anything — it is only a trap for whoever adds the first one.
I have kept it as a separate commit so it is trivial to drop if you would rather it went in on its own, or not at all. Happy either way.
Validate is optional for callers and nothing in the connection path calls
it, so the rule only reached consumers who validated first.
NewConnection now applies the same check after SetDefaults, so
rig.NewClient(WithConnectionFactory(...)) and direct NewConnection callers
are refused before any request is sent. The error also wraps
ErrNonRetryable, since no retry can change the configuration.
Config.Connection returned a typed-nil *Connection inside the interface on
error; it now returns a plain nil.
A client certificate is only presented over TLS, so certPath/keyPath
without useHTTPS is now a validation error instead of being silently
ignored.
The insecure field's schema description now states both effects, and the
schemas are regenerated.
Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Mutable connection configuration can bypass the password-transport guard and send credentials over HTTP despite Insecure being false, while validation also incorrectly rejects port-inferred HTTPS configurations.
No open issues 🎉
2 previously missed issues
In code that hasn't changed since last review
Revalidate password transport security before connecting
protocol/winrm/connection.go:58
Constructing an HTTP connection without a password, assigning conn.Password, and calling Connect bypasses this check and sends the password over HTTP with Insecure false. The exported, mutable Connection.Config also allows direct construction that skips NewConnection. Recheck validatePasswordTransport at the start of Connect, wrapping both validation and non-retryable sentinels, so the protection applies to the configuration used for transmission.
Validate authentication using normalized HTTPS settings
protocol/winrm/config.go:90
With Port: 5986, a user/password and UseHTTPS left false, Config.Validate() and CompositeConfig.Validate() reject a configuration that NewConnection accepts as HTTPS after applying defaults. Validate authentication against the same normalized configuration so both this guard and the certificate guard at line 69 honor port-inferred HTTPS. Add a regression case that calls validation without first applying defaults.
…red HTTPS
Connection.Config is exported and mutable, and a Connection can be built
without NewConnection, so a password added after NewConnection reached
the plain-HTTP transport unchecked. Connect now applies the same rule
before any request.
SetDefaults turns UseHTTPS on for port 5986, but Validate is often called
before SetDefaults, so a password or client certificate on 5986 was
rejected there while NewConnection accepted it. usesHTTPS treats port 5986
as HTTPS in the validation rules and in Connect's endpoint, so every path
judges the transport the connection will actually use.
The endpoint construction moves into newEndpoint to keep Connect within
the cyclomatic complexity limit.
Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Missing endpoint-construction coverage for directly constructed connections could let a regression send passwords over HTTP on port 5986 without failing tests.
Validation treats port 5986 as HTTPS before SetDefaults, so a directly
constructed Connection on 5986 must build an HTTPS endpoint; otherwise a
password accepted on that basis would travel over plain HTTP. Covers
5985, 5986 without useHTTPS, and custom ports with and without it.
Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed to evaluate the additional HTTPS requirement for certificate configurations and the NTLM security rationale’s inaccurate claim of direct password disclosure.
NTLM never sends the password itself, so the basic-auth message about the
password being sent over plain HTTP was wrong for useNTLM. NTLM over plain
HTTP is still refused: the WinRM client does not seal the session, so
commands, output and any secret inside them are readable, and the
captured challenge response can be cracked offline or relayed. It now gets
its own message saying so.
Signed-off-by: Kimmo Lehto <klehto@mirantis.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Human assessment is needed because the normal connection path can bypass certificate/HTTPS validation and silently fall back to password authentication.
No open issues 🎉
Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
winrmnow refuses a password on a transport that does not protect it, andinsecureis widened from "accept invalid TLS certificates" to "do not refuse an unprotected transport".ErrValidationFaileduseNTLM, plain HTTPErrValidationFailedinsecureuseHTTPS(or port 5986)useHTTPS+insecurecertPath/keyPath,useHTTPScertPath/keyPath, plain HTTPErrValidationFailedinsecurecarries the decision because both readings describe the same caller: one who has accepted that the transport need not protect what it carries. On plain HTTP it previously did nothing, so nobody who set it for TLS gets the new meaning by surprise. Its schema description now states both effects.Where it is enforced
Nothing in rig calls a protocol config's
Validate()on the connection path, so the rule runs in three places:Config.Validate()(and through itCompositeConfig.Validate())NewConnection, afterSetDefaults, sorig.NewClient(WithConnectionFactory(...))fails before any requestConnection.Connect, becauseConnection.Configis exported and mutable and aConnectioncan be built withoutNewConnectionOn the connection path the error wraps both
ErrValidationFailedandErrNonRetryable.Port 5986 counts as HTTPS everywhere (validation and the endpoint
Connectbuilds), matching whatSetDefaultsdoes, so a config validated or connected beforeSetDefaultsis judged by the transport it will actually use.NTLM is not an exemption
NTLM never sends the password itself, but the WinRM client does not seal the session: commands, output and any secret inside them stay readable, and the captured challenge response can be cracked offline or relayed.
useNTLMtherefore does not change the outcome, and gets its own error message rather than the basic-auth one about the password being sent.Breaking changes
useHTTPS(or port 5986) and noinsecurenow fails instead of connecting. Settinginsecure: truekeeps the previous behaviour.certPath/keyPathwithout HTTPS now fails validation. The client certificate is only presented in a TLS handshake, so over plain HTTP it was silently ignored and the connection fell back to basic auth. The only config that worked before and fails now is certificate + password +insecureover plain HTTP, which authenticated with the password and never used the certificate.Drive-by
fix(log): inject the logger into a Config pointer, not a copy:InjectLoggerTowas handed aConfigvalue, whose method set excludes the pointer-receiverSetLogger, so the injection silently did nothing in both winrm and ssh. No symptom today because noConfigmethod logs.Also:
Config.Connection()no longer returns a typed-nil*Connectioninside a non-nil interface on error.Testing
TestValidateRejectsExposedPassword: the matrix above, including port-5986 cases validated withoutSetDefaults, the NTLM message, and that no error repeats the passwordTestConnectionRejectsExposedPassword:Config.Connection()refuses with both sentinelsTestConnectRefusesPasswordOverPlainHTTP: password set afterNewConnection, and a directly constructedConnection; the stub server receives no requestsTestNewEndpointScheme: endpoint scheme for 5985, 5986 withoutuseHTTPS, and custom portsTestClientWithConnectionFactoryRefusesWinRMPasswordOverHTTP: throughrig.NewClient, no request reaches the stub serverTestNewConnectionInjectsLoggerIntoConfiginprotocol/sshandprotocol/winrmWritten by AI: claude-opus-5