Tutorial: a server check tool
This tutorial builds a small console program that connects to a server, asks about an unknown host key the way ssh does, runs a
few checks, uploads a report and handles every failure cleanly. Each step adds one idea; the code is taken from a project that is
compiled with the library, so it matches the current API.
You need the .NET SDK (8 or 10) and an SSH server you can log in to with a key in your agent or a password.
1. Create the project
dotnet new console -n ServerCheck
cd ServerCheck
dotnet add package SshSharp
2. Connect
Replace Program.cs. The keys come from the SSH agent when one is running, and a password from the SSH_PASSWORD environment
variable when it is set; the client tries the keys first, as ssh does.
using SshSharp;
using SshSharp.Config;
// Program.cs: dotnet run -- <host> <user>
var (host, user) = (args[0], args[1]);
var agent = SshAgent.FromEnvironment();
var keys = agent is null ? [] : await agent.GetKeysAsync();
await using var client = await SshClient.ConnectAsync(host, 22, new SshClientOptions
{
UserName = user,
PrivateKeys = keys,
Password = Environment.GetEnvironmentVariable("SSH_PASSWORD"), // tried after the keys, if set
HostKeyVerifier = KnownHostsVerifier.ForCurrentUser(StrictHostKeyChecking.Yes),
});
Console.WriteLine($"Connected to {client.ServerIdentification}, host key {client.HostKey.Fingerprint}");
return 0;
Run it with dotnet run -- example.com user. With StrictHostKeyChecking.Yes, a server that is not in ~/.ssh/known_hosts is
refused with an SshHostKeyException: the program will not talk to a server it cannot identify.
3. Ask about unknown hosts
ssh asks whether to trust a host it has not seen. KnownHostsVerifier does the same with the Ask policy and an AskHostKey
callback; a key you accept is added to known_hosts, so the next run does not ask. Use this verifier in place of
KnownHostsVerifier.ForCurrentUser(...) from step 2:
var verifier = new KnownHostsVerifier([Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ssh", "known_hosts")])
{
Policy = StrictHostKeyChecking.Ask,
AskHostKey = (prompt, ct) =>
{
Console.WriteLine($"The authenticity of host '{prompt.Context.Host}' can't be established.");
Console.WriteLine($"{prompt.Context.HostKey.KeyType} key fingerprint is {prompt.Context.HostKey.Fingerprint}.");
Console.Write("Are you sure you want to continue connecting (yes/no)? ");
return ValueTask.FromResult(Console.ReadLine()?.Trim() == "yes"); // accepted keys are added to known_hosts
},
};
Warning
Compare the fingerprint with one you got another way (the server's console, its provisioning output) before you answer yes.
A changed key is never asked about: it is rejected, as ssh rejects it.
4. Run the checks
RunCommandAsync collects a command's output and exit status. Check the exit status: a command that fails on the server is not an
exception, it is a result.
var checks = new Dictionary<string, string>
{
["uptime"] = "uptime",
["disk"] = "df -h / | tail -1",
["memory"] = "free -m | sed -n 2p",
};
foreach (var (name, command) in checks)
{
var result = await client.RunCommandAsync(command);
var text = result.ExitStatus == 0 ? result.StandardOutputText.Trim() : $"failed ({result.ExitStatus}): {result.StandardErrorText.Trim()}";
Console.WriteLine($"{name,-8} {text}");
}
5. Upload a report
SFTP runs over the same connection. Overwrite = false makes an existing file an error instead of replacing it.
using SshSharp.Sftp;
await using var sftp = await client.OpenSftpAsync();
var report = $"report-{DateTime.UtcNow:yyyyMMdd-HHmmss}.txt";
await File.WriteAllTextAsync(report, "collected by the tutorial tool\n");
await sftp.UploadFileAsync(report, $"reports/{report}", new SftpTransferOptions { Overwrite = false });
Console.WriteLine($"Uploaded {report}");
The reports directory must exist on the server; sftp.CreateDirectoryAsync("reports") makes it.
6. Time limits and failures
Every network operation takes a CancellationToken. One token with a time limit bounds the whole run, and Ctrl-C cancels it.
Each kind of failure gets its own message:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; // Ctrl-C cancels cleanly
try
{
await using var client = await SshClient.ConnectAsync(host, 22, options, cts.Token);
var result = await client.RunCommandAsync("uptime", cts.Token);
Console.WriteLine(result.StandardOutputText);
return result.ExitStatus ?? 1;
}
catch (SshHostKeyException ex)
{
Console.Error.WriteLine($"Refusing to connect: {ex.Message}");
}
catch (SshAuthenticationException ex)
{
Console.Error.WriteLine($"Login failed. The server accepts: {string.Join(", ", ex.AllowedMethods)}");
}
catch (SshException ex)
{
Console.Error.WriteLine($"{ex.Reason}: {ex.Message}");
}
catch (OperationCanceledException)
{
Console.Error.WriteLine("Timed out or cancelled.");
}
return 2;
Next steps
- Connect by
ssh_configalias instead of host and user: Use ssh_config and known_hosts. - Check many servers at once: the fleet health check scenario.
- Stream output that does not fit in memory: Stream large output.