Use ssh_config and known_hosts
SshSharp reads OpenSSH's ssh_config and known_hosts with the same semantics as ssh. Connecting by alias applies everything
the configuration says; the SshSharp.Config types also read the files directly, for tools that only want the values.
When to use
- Your users already have working
~/.ssh/configentries, and your tool should behave likesshwith them. - Operations teams manage host names, users, keys, bastions and host key policy in configuration, not in code.
- You need to inspect or update
known_hostsyourself.
Connect by alias
// Everything comes from ~/.ssh/config: HostName, User, IdentityFile, ProxyJump, known_hosts, ...
await using var client = await SshClient.ConnectAsync("prod-db", cancellationToken: cancellationToken);
// The other destination forms ssh accepts.
await using var other = await SshClient.ConnectAsync("deploy@build.example.com", cancellationToken: cancellationToken);
await using var third = await SshClient.ConnectAsync("ssh://deploy@build.example.com:2222", cancellationToken: cancellationToken);
What the configuration decides: HostName, Port, User, IdentityFile, CertificateFile, the agent (IdentityAgent,
IdentitiesOnly), known_hosts files and StrictHostKeyChecking, algorithms, ProxyJump and ProxyCommand, keep-alive,
ConnectTimeout, and more; the ssh_config reference lists every directive and how it is used.
Supply what a configuration file cannot
Passphrases, answers to prompts, and policy for things a file should not decide alone go in SshConfigConnectOptions:
await using var client = await SshClient.ConnectAsync("prod-db", new SshConfigConnectOptions
{
// Encrypted IdentityFile keys: the passphrase from a secret store or the environment, never from source code.
PassphraseProvider = (path, ct) =>
ValueTask.FromResult(Environment.GetEnvironmentVariable("SSH_KEY_PASSPHRASE")),
// StrictHostKeyChecking ask: decide about an unknown host key.
AskHostKey = (prompt, ct) =>
ValueTask.FromResult(Confirm($"Trust {prompt.Context.HostKey.KeyType} {prompt.Context.HostKey.Fingerprint}?")),
// A configuration that names a ProxyCommand runs a local program: refuse it unless the file is trusted.
AllowProxyCommand = false,
}, cancellationToken);
Ship a configuration with your application
SshConfig.Parse (or SshConfig.Load for a file) gives the application its own configuration, independent of the user's:
// A configuration that ships with the application, instead of the user's ~/.ssh/config.
var config = SshConfig.Parse("""
Host bastion
HostName bastion.example.com
User jump
IdentityFile ~/.ssh/fleet_ed25519
Host app-*
ProxyJump bastion
User deploy
IdentityFile ~/.ssh/fleet_ed25519
UserKnownHostsFile /etc/myapp/known_hosts
StrictHostKeyChecking yes
ServerAliveInterval 30
""");
await using var client = await SshClient.ConnectAsync("app-3", new SshConfigConnectOptions { Config = config }, cancellationToken);
Read the configuration without connecting
Resolve returns what ssh -G would print for a host: Host and Match blocks, Include, tokens and environment variables
applied.
var config = SshConfig.LoadDefault(); // ~/.ssh/config, then the system-wide file
SshHostConfig host = config.Resolve("prod-db"); // what ssh -G prod-db prints
Console.WriteLine($"{host.User}@{host.HostName}:{host.Port}");
Console.WriteLine($"via {string.Join(" -> ", host.ProxyJump)}");
Console.WriteLine($"keys {string.Join(", ", host.IdentityFiles)}");
Console.WriteLine($"StrictHostKeyChecking {host.StrictHostKeyChecking}");
The verifier ssh would use for that host comes from the same resolution:
// The verifier ssh would use for this host: its known_hosts files, StrictHostKeyChecking, HashKnownHosts, RevokedHostKeys ...
var host = SshConfig.LoadDefault().Resolve("prod-db");
var verifier = KnownHostsVerifier.FromConfig(host);
Read and update known_hosts
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh", "known_hosts");
var knownHosts = await KnownHostsFile.LoadAsync(path, cancellationToken);
switch (knownHosts.Check("example.com", 22, client.HostKey.KeyType, client.HostKey.PublicKey.Span))
{
case KnownHostsCheckResult.Match:
Console.WriteLine("known");
break;
case KnownHostsCheckResult.Unknown:
await KnownHostsFile.AppendAsync(path, "example.com", 22, client.HostKey.KeyType, client.HostKey.PublicKey, hash: true,
cancellationToken: cancellationToken);
break;
case KnownHostsCheckResult.Changed or KnownHostsCheckResult.Revoked:
throw new InvalidOperationException("The host key does not match known_hosts.");
}
Hashed host names (|1|salt|hash), [host]:port, wildcards and negation, @cert-authority and @revoked are understood.
AppendAsync only appends, so comments and the order of existing lines are kept.
Notes and pitfalls
- Commands in configuration files.
ProxyCommandruns a local program (allowed by default, as inssh; turn it off withAllowProxyCommand = false).KnownHostsCommanddecides which host keys are trusted, so it runs only withAllowKnownHostsCommand = true.Match execruns only whenSshConfigResolveOptions.ExecMatcheris set. Only enable these for configuration files you trust. Match execand canonicalization are asynchronous. A configuration that needs them must be resolved withResolveAsync;ResolvethrowsNotSupportedException. Connecting by alias always resolves asynchronously.BatchMode yesturns off every prompt:AskHostKey,ConfirmHostKeyUpdate,PassphraseProviderand the keyboard-interactive handler are not used.- Session settings are yours to apply.
SendEnv,SetEnvandRequestTTYare resolved (SshHostConfig.SendEnv,SetEnv,RequestTty) but not applied by connecting, since sessions are started by your code: pass them inSshSessionOptions. - Which file.
SshConfig.LoadDefault()reads~/.ssh/config, then the system file;SshConfigConnectOptions.Configreplaces both.
See also
- Configuration files (in-depth:
Matchcriteria,KnownHostsCommand, tokens) - Connecting: by ssh_config alias
- ssh_config directives
- Verify host keys