Table of Contents

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