Configuration files
The SshSharp.Config namespace reads OpenSSH's ssh_config and known_hosts with the same semantics as ssh, for
applications that want the values without connecting.
ssh_config
using SshSharp.Config;
var config = SshConfig.LoadDefault(); // ~/.ssh/config, then the system file
SshHostConfig host = config.Resolve("prod-db");
Console.WriteLine($"{host.User}@{host.HostName}:{host.Port}");
Console.WriteLine(string.Join(", ", host.IdentityFiles));
Console.WriteLine(string.Join(" -> ", host.ProxyJump));
Resolution follows ssh -G: first value wins, Host and Match (host, originalhost, user, localuser,
canonical, final, all, exec, tagged, localnetwork, sessiontype, command), Include with globs, % tokens, ~
and environment variables, and algorithm lists with +, - and ^. SshConfig.Parse(text) reads a configuration from a string,
and SshConfigResolveOptions supplies the home directory, environment, local user and host name (for tests, or another user).
Match localnetwork 10.0.0.0/8,fd00::/8compares the addresses of the network interfaces that are up with the networks (CIDR, IPv4 or IPv6; a bare address is one host).SshConfigResolveOptions.LocalAddressesreplaces the interface list. As ssh_config(5) warns, a network address is not a trustworthy criterion in many situations.Match sessiontype(shell,exec,subsystem,none) andMatch commanddescribe the session about to start, whichsshknows from its command line and a library does not: setSshConfigResolveOptions.SessionTypeandCommand(the command, or the subsystem name such assftp). WithoutSessionType, blocks with these criteria never apply, negated or not, rather than guess; the same holds forMatch version, which concerns another OpenSSH release. ProxyJump hops are resolved with session typenoneand no command, as ssh resolves thessh -Wit runs for each hop.RefuseConnection yes(OpenSSH 10.0) makes connecting by that alias fail at once, before any network activity.
Match exec runs a local command, so it is evaluated only when SshConfigResolveOptions.ExecMatcher is set, for configuration files
you trust (ExecMatcher = (command, ct) => new(SshExecMatcher.RunShellAsync(command, ct)) runs it in the system shell); without
it, Match exec does not match. ProxyCommand
and Match exec inputs from the caller (host and user names) are validated so they cannot inject shell syntax.
ExecMatcher and HostCanonicalizer are asynchronous (they run a process, or look a name up), so a config that needs either
resolves with ResolveAsync; Resolve throws NotSupportedException for it. Connecting by alias always resolves
asynchronously.
Within one resolution, each distinct Match exec command runs once and its answer is reused, as is each canonicalization. That
includes the second pass that Match final or canonicalization causes, where OpenSSH runs the command again. A command whose
tokens expand differently in that pass (%h after canonicalization, say) is a different command, and runs.
KnownHostsCommand asks a program for host keys, in addition to the known_hosts files, as OpenSSH 8.5+ does: its output is read
as known_hosts lines. As in OpenSSH:
- The command is split into arguments; no shell is involved. The program must be given by absolute path and is taken as written
(on Windows it cannot be a
.bator.cmdfile, which Windows would run throughcmd.exe). - Each argument after it has its environment variables (
${VAR}) and tokens expanded, in one pass, so a variable's value is not expanded again. Besides the usual tokens there are%H, the name looked up ([host]:portfor another port);%I,HOSTNAME, orORDERwhen it runs before the key is known; and the presented key's%K(base64),%f(fingerprint) and%t(type),NONEbefore it is known. - While a host certificate is checked,
%Kand%tare the certificate's; if the check falls back to the plain key, the command runs again with that. - A non-zero exit status fails the connection. Unlike OpenSSH, the run is limited to 30 seconds and 1 MiB of output.
Because its output decides which host keys are trusted, connecting by alias runs it only with
SshConfigConnectOptions.AllowKnownHostsCommand = true(off by default); otherwise a configuration that names one fails the connection, rather than quietly checking fewer keys than it says, unless the application supplies its ownHostKeyVerifier, which then decides alone.KnownHostsVerifier.FromConfignever runs it.
known_hosts
var knownHosts = await KnownHostsFile.LoadAsync(Path.Combine(home, ".ssh", "known_hosts"));
foreach (var entry in knownHosts.Find("example.com", 22))
{
Console.WriteLine($"{entry.Marker} {entry.KeyType}");
}
KnownHostsCheckResult check = knownHosts.Check("example.com", 22, client.HostKey.KeyType, client.HostKey.PublicKey.Span);
// Match, Unknown, Changed or Revoked
await KnownHostsFile.AppendAsync(path, "new.example.com", 22, client.HostKey.KeyType, client.HostKey.PublicKey, hash: true);
Hashed host names (|1|salt|hash), [host]:port, wildcards and negation, @cert-authority and @revoked are supported.
SshHostConfig.RevokedHostKeys is the resolved RevokedHostKeys file (~ and tokens expanded); KnownHostsVerifier.FromConfig
checks host keys against it (see Host keys).
AppendAsync only appends, so comments and the order of existing lines are kept.
See also
- Use ssh_config and known_hosts
- ssh_config directives: every directive and how a connection applies it
- Unit-test code that uses SshSharp