OpenSSH 10.6 refuses a command-line username that a config file still accepts
OpenSSH 10.6p1 refused lab$user@<closed-host> before it opened a socket. The process exited 255 and printed remote username contains invalid characters. The same destination, given to portable OpenSSH 10.5p1 and to the already installed OpenSSH_10.0p2 Debian-7+deb13u4 client, did not produce that message. Both clients continued far enough to report that the closed port had refused the connection.
That split is easy to miss if a wrapper only records a non-zero exit. A down host, a bad port, and a username the new client will not accept all end as exit 255. The text on standard error is the part that says which of those happened. The other easy miss is the opposite one. The 6 October 2026 release notes say a User directive in a configuration file is not subject to the new restriction. A command that sets the same name with -o User= is still refused. Those are not two spellings of one input path.
The comparison used portable source builds of OpenSSH 10.6p1 and 10.5p1 on Linux amd64, linked against OpenSSL 3.5.7, plus the distribution client already on the host. Both tarballs were fetched from the OpenSSH portable mirror and checked against the base64 SHA-256 strings in the release notes before they were unpacked. ssh -V on the new build printed OpenSSH_10.6p1, OpenSSL 3.5.7 9 Jun 2026. The 10.5 build printed OpenSSH_10.5p1 with the same OpenSSL line. The distribution client printed OpenSSH_10.0p2 Debian-7+deb13u4.
The new characters are dollar and backslash
The release notes describe the command-line change as a refusal of $ and \ in a destination username. The motivation they give is injection into a shell context through ProxyCommand, Match exec, and similar expansions when a username from an untrusted source reaches the client. The notes also say the restriction is not absolute, and they repeat the existing advice not to expose ssh command lines to untrusted input. This check is a narrower client refusal. It is not a reason to start passing untrusted names to ssh.
The character test in the 10.6 source is not a new function. Both trees have ssh_valid_ruser(). In the 10.5 tree the rejected set already includes a single quote, a double quote, a backtick, a semicolon, an ampersand, angle brackets, a pipe, parentheses, and braces, plus control characters, a leading hyphen, and a hyphen after whitespace. The 10.6 tree adds $ and \ to that set. A command-line username containing a semicolon was already fatal on 10.5, with the same remote username contains invalid characters message. A single quote was already fatal too. The observable change, on the forms tested here, is the two characters the notes name.
A backslash in lab\user@<closed-host> followed the same split as the dollar sign. 10.6 exited with the invalid-character fatal. 10.5 and the distribution 10.0p2 client attempted the connection. A plain labuser destination was the control: 10.6 attempted that connection and reported the closed port, so the fatal is not a blanket refusal of every destination.
The release notes publish SHA-256 values as base64, and they say so. A sha256sum comparison against that string does not match, even when the file is the file the note describes. The base64 digest of each downloaded tarball matched the note. That encoding detail is independent of the username check, but it is the reason a checksum script can reject a good tarball before any client test runs.
Ask the client that will run
The refusal is in ssh, not in sshd. An older client pointed at a newer server still accepts the command-line name and will try to connect. A newer client pointed at an older server still refuses the name, and the server never sees it. authorized_keys, a Match User block, and PAM cannot explain an error that is printed before the socket opens. They also cannot be treated as a substitute for the client check when the client is old.
Read the version from the binary the job will execute, not from a package list on a different host.
ssh -V
On the three clients in this comparison the version line was enough to separate 10.6p1 from the two older clients, and the closed-port test agreed with that separation. A distribution can backport a check into a package whose ssh -V line still names an older release. This run did not include such a package. The distribution client that was present, 10.0p2, did not refuse the dollar username. If a later package claims a backport, repeat the closed-port test against that binary. A version string that does not contain 10.6 is not, by itself, evidence that the refusal is absent, and a string that does contain 10.6 is not evidence about a different binary on the path.
Use an empty configuration so a personal User, ProxyJump, or ProxyCommand does not change the result. -F /dev/null did that for the command-line forms below. Point the client at a closed port on a host you control, with BatchMode and a short connect timeout, so a client that accepts the name fails quickly and a client that refuses the name never needs the port to be open.
ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=2 -p <closed-port> \
'lab$user@<closed-host>' true
Quote the destination. If the shell expands $user before ssh starts, the client never sees the character the check is for, and the test answers a different question. The quotes in that command are part of the test, not decoration.
10.6p1 printed the invalid-character fatal and did not print a connection error. 10.5p1 and OpenSSH_10.0p2 printed a connection refusal and did not print the invalid-character fatal. Both outcomes are exit 255. A script that branches only on the exit code cannot tell them apart.
The same fatal appeared for -l and for a command-line option:
ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=2 -p <closed-port> \
-l 'lab$user' <closed-host> true
ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=2 -p <closed-port> \
-o 'User=lab$user' <closed-host> true
Both were fatal on 10.6 before a connection attempt. Both reached a connection refusal on 10.5 and on the distribution client. ssh -G -o User=lab$user <closed-host> also died with remote username contains invalid characters on 10.6, so the configuration dump never printed a resolved user. The release note’s exception is a directive in a configuration file. It is not every way of spelling User.
-J is the same kind of command-line input. On 10.6 this was fatal before any socket opened:
ssh -F /dev/null -o BatchMode=yes -o ConnectTimeout=2 \
-J 'lab$user@<jump-host>:<jump-port>' -p <closed-port> \
labuser@<closed-host> true
The messages were parse_jump: invalid username "lab$user" and Invalid -J argument. The jump port was not contacted. The same -J value on 10.5 contacted the jump port and reported connection refused. A jump username is checked on its own. A clean destination user does not exempt a dirty jump user, and a refusal of the jump user is not a refusal of the destination host.
A configuration file is the exception, until it expands
A file with a literal User line is a different path. This was the file used for the dollar case:
Host target
User lab$user
HostName <closed-host>
Port <closed-port>
ssh -F <that-file> -G target exited 0 on both 10.6 and 10.5 and printed user lab$user. When standard input is not a terminal, -G also prints Pseudo-terminal will not be allocated because stdin is not a terminal. That warning is not the username check. A following connection attempt on 10.6 reached the closed port. It did not print remote username contains invalid characters. A file whose User line was lab\user behaved the same way: -G printed the backslash, and the connection attempt reached the closed port.
The source comment in the 10.6 client says command-line names, and names expanded from the configuration, must be validated, while a literal configuration value is not. The 10.5 tree has the same split. What changed is which characters make a command-line name fail. The file exception is visible now because $ and \ are newly fatal on the command line and still accepted as a literal User value.
Expansion removes that exception. With this file:
Host target
User lab${JB_SUFFIX}
HostName <closed-host>
Port <closed-port>
three runs on 10.6 produced three different results.
When JB_SUFFIX was ok, -G exited 0 and printed user labok. The dollar sign was consumed by expansion, the result had none of the rejected characters, and the check passed.
When JB_SUFFIX was a value that still contained $, -G exited 255 and printed remote username contains invalid characters. Expansion ran, the result was not a literal configuration value, and the check applied to the expanded text.
When JB_SUFFIX was unset, both 10.6 and 10.5 exited 255 with vdollar_percent_expand: env var ${JB_SUFFIX} has no value and invalid environment variable expansion. That failure is older than the 10.6 character change. A missing variable is not evidence that the username check fired.
A configuration-file ProxyJump followed the file rule rather than the -J rule. This file kept the dollar sign in the jump user and used a different closed port for the jump and the destination, so a connection error named the port that was actually contacted:
Host target
HostName <closed-host>
Port <closed-port>
ProxyJump lab$user@<jump-host>:<jump-port>
On 10.6, ssh -G exited 0 and printed a proxyjump line that still contained lab$user. A connection attempt contacted the jump port and reported connection refused. It did not print parse_jump: invalid username. The same file on 10.5 also contacted the jump port. The command-line -J form is the one 10.6 rejects. A ProxyJump line written into a file is not.
That is the operational gap. An inventory script that puts a directory username on the ssh command line will see the new fatal, once the client is 10.6. The same script, writing the same username into User or ProxyJump and then invoking ssh -F, will not. The release notes already say the file form is exempt. The run above is the part they do not spell out: -o User= and -J are not the file form, and a ProxyJump line in the file is not the -J form.
Read the error, then decide what to change
Keep the closed-port test next to the version check. Run it with -F /dev/null for command-line names, and with the real file when the job uses a generated config. Record standard error, not only the exit code.
A fatal of remote username contains invalid characters, or parse_jump: invalid username, means this client rejected the name before a connection. The host was not proven down, and the server was not asked to authenticate. Fix the name, or move a legitimate unusual name into a literal User line if that name is actually required and the file is trusted. Do not silence the fatal by wrapping the client in a retry.
A connection refusal, timeout, or name-resolution failure means the client accepted the name far enough to use the network. On 10.6 that is the expected result for a plain username, for a literal User or ProxyJump value containing $ or \, and for an expanded User whose result no longer contains a rejected character. It is also the expected result for every one of those forms on the 10.5 and 10.0p2 clients tested here, except that those older clients also accept the command-line dollar and backslash forms.
If the job builds a configuration file from an external directory, validate the username before writing User or ProxyJump. The 10.6 client will not do that for a literal line. The same is true of a ProxyCommand or Match exec line that interpolates a username: the new check does not inspect those strings when they come from a file, and this article does not demonstrate what a shell would do with them. Keep those directives out of any file assembled from untrusted fields.
The compression change in the same release is a separate question. 10.6 disables the LZ77 dictionary coder because of a cross-channel plaintext-recovery result described in the release notes. That change does not alter the username fatal, and a successful username test says nothing about whether compression is still useful on a session. The scp -R deprecation warning is separate again. Neither belongs in a wrapper that is only trying to learn whether a destination username will be refused.
The result of this comparison is narrow. Portable OpenSSH 10.6p1 refuses $ and \ in a command-line destination user, in -l, in -o User=, and in -J, and it does so before opening a socket. A literal User or ProxyJump line in a file still carries those characters through to a connection attempt. Expansion of a User line is checked after expansion, and an unset variable fails for an older reason that 10.5 already prints. The distribution client tested here, OpenSSH_10.0p2, behaved like 10.5 on the command-line forms. A later package, or a backport, needs the same closed-port comparison before a wrapper treats the fatal as either universal or already present.