Skip to content

feat(winrm)!: refuse to send a password over plain HTTP - #516

Open
james-nesbitt wants to merge 6 commits into
k0sproject:mainfrom
james-nesbitt:fix/winrm-insecure-password-exposure
Open

james-nesbitt wants to merge 6 commits into
k0sproject:mainfrom
james-nesbitt:fix/winrm-insecure-password-exposure

Conversation

@james-nesbitt

@james-nesbitt james-nesbitt commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

What

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

Written by AI: claude-opus-5

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The security guidance incorrectly claims NTLM encrypts SOAP bodies, so its recommended remedy can leave commands and output unencrypted.

4 open issues
What changed in this PR

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.

Review effort: Balanced

Comment thread protocol/winrm/config.go Resolved
Comment thread protocol/winrm/config.go Resolved
Comment thread protocol/winrm/config_test.go Resolved
Comment thread protocol/winrm/connection.go Resolved
@kke

kke commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

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
james-nesbitt force-pushed the fix/winrm-insecure-password-exposure branch from 70295b8 to 8ede149 Compare November 10, 2026 12:18
@james-nesbitt 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
@james-nesbitt

james-nesbitt commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor Author

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.

Written by AI: claude-opus-5

@james-nesbitt
james-nesbitt marked this pull request as ready for review November 10, 2026 12:22
@james-nesbitt

Copy link
Copy Markdown
Contributor Author

@kke let me know if you'd like me to drop that commit with the log reference injection.

@kke
kke requested a balanced review from Copilot November 11, 2026 11:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The connection path does not enforce validation, allowing passwords to be sent over plaintext HTTP without an explicit insecure opt-in.

2 open issues
4 resolved since last review

Review effort: Balanced

Comment thread protocol/winrm/config.go Resolved
Comment thread protocol/winrm/config.go Resolved
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>
@kke
kke requested a balanced review from Copilot November 11, 2026 11:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

High severity 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.

Medium severity 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.

2 resolved since last review

Review effort: Balanced

…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>
@kke
kke requested a balanced review from Copilot November 11, 2026 11:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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.

1 open issue

Review effort: Balanced

Comment thread protocol/winrm/connection.go Resolved
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>
@kke
kke requested a balanced review from Copilot November 11, 2026 12:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

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>
@kke
kke requested a balanced review from Copilot November 11, 2026 13:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants