In this article, we will learn what Secure File Transfer Protocol is and how to use SFTP file upload to securely send files to a remote server in .NET and C#.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

What Is SFTP for Secure File Upload and How It Works?

SFTP stands for Secure File Transfer Protocol. SFTP is a protocol designed as an extension of Secure Shell (SSH) that enables users and programs to upload files to a server using a secure channel over the network.

We can use this protocol to securely transfer files containing sensitive data such as personal information or financial data between servers. It provides a secure and encrypted connection between the client and the SFTP server using SSH keys, ensuring that the data transferred is protected against interception and tampering by third parties.

Typically, clients must authenticate to an SFTP server using either a username and password or a public/private key pair. Once authenticated, they can use FTP-like commands to perform operations on the remote file system.

Using SSH.NET to Perform a Secure File Upload to an SFTP Server

To use SSH.NET in our project we will have to install a NuGet package first:

dotnet add package SSH.NET

Once installed, let’s instantiate the SftpClient object and establish a connection to the SFTP server using a username and password. Then, let’s use that instance to perform various operations against the server:

using SftpClient client = new("sftp.test-server.com", 22, "tester", "password");

try
{
    client.Connect();
    if (client.IsConnected)
    {
        client.CreateDirectory("/images");
        client.UploadFile(File.OpenRead("./files/portrait.jpg"), "/images/portrait.jpg");

        Console.WriteLine("Directory listing:");
        
        foreach(var sftpFile in client.ListDirectory("/images"))
        {
            Console.WriteLine($"\t{sftpFile.FullName}");
        }

        client.Disconnect();
    }
}
catch (Exception e) when (e is SshConnectionException || e is SocketException || e is ProxyException)
{
    Console.WriteLine($"Error connecting to server: {e.Message}");
}
catch (SshAuthenticationException e)
{
    Console.WriteLine($"Failed to authenticate: {e.Message}");
}
catch (SftpPermissionDeniedException e)
{
    Console.WriteLine($"Operation denied by the server: {e.Message}");
}
catch (SshException e)
{
    Console.WriteLine($"Sftp Error: {e.Message}");
}

First, let’s create a new directory on the server named images by calling the CreateDirectory() method. Then, let’s upload a file to the newly created folder, and finally, let’s use the ListDirectory() method to see how our file now is saved in the server at the location we choose:

Directory listing:
  /images/.
  /images/..
  /images/portrait.jpg

We can see how the UploadFile() method requires a FileStream to read the file data and, as its second parameter, the remote destination path and file name. In turn, the ListDirectory() method will return a list of SftpFile instances that we can iterate across.

Finally, we can end the SFTP session by calling the Disconnect() method.

Exception Handling With SSH.NET

When handling errors and exceptions, let’s consider different kinds of events starting with connection failures. Connection errors can happen due to violations of the SSH protocol or other network-related issues. We can handle all these by catching SshConnectionException, SocketException, and ProxyException exceptions.

Regardless of the authentication method we use, username and password, or cryptographic keys, the server can reject an authentication attempt. We can catch this by handling the SshAuthenticationException.

Even when properly authenticated, the server may deny some actions due to a lack of privileges that will depend on the specific server configuration. For example, our user may not have permission to write to a particular folder on the server. This category is handled by the SftpPermissionDeniedException.

Finally, an SFTP server can throw other, more general, exceptions that we can catch through the SshException type.

Using a Private Key to Access an SFTP Server

Alternatively, we can authenticate against the SFTP server using a private ssh key whose corresponding public key has been previously authorized by the server:

var privateKey = new PrivateKeyFile("./ssh-keys/userkey");
using SftpClient client = new("sftp.test-server.com", 22, "tester", privateKey);
client.Connect();

In this snippet, we have loaded a private key file located at ./ssh-keys/userkey into a PrivateKeyFile instance. After that, we use it to initialize the SftpClient instance passing the private key instead of the password. 

Let’s be aware that there are multiple private key formats and SSH.NET doesn’t support all of them. If we created our key pair using ssh-keygen, chances are that we will have to export our private key to PEM format before attempting to use it with SSH.NET.

Checking the Server’s Public Key Fingerprint

So far, we have authenticated our requests to the server either by providing our username and password or, even better, by using our ssh key. On top of this, the SFTP host also holds a public-private key pair that lets us verify the server’s identity. Verifying the server key will protect our data even further by preventing man-in-the-middle and spoofing attacks.

A host key fingerprint is a unique identifier that represents the public key of the host. We can use the key fingerprint to verify the authenticity of a remote server when connecting to it via SFTP.

When connecting to an SFTP server for the first time, the client can check the host key fingerprint provided by the server against a locally stored one. If they match, then the connection proceeds as usual. However, if the fingerprints do not match, then the SFTP client will throw a SshConnectionException.

With SSH.NET, we have to implement the check explicitly by subscribing an event handler delegate to the HostKeyReceived event:

var expectedFingerprint = Convert.FromHexString("D29B9B75F725D9164EA0D4935AFCACD3");

using SftpClient client = new("sftp.test-server.com", 22, "tester", privateKey);
client.HostKeyReceived += delegate(object? sender, HostKeyEventArgs e)
{
    e.CanTrust = e.FingerPrint.SequenceEqual(expectedFingerprint);
};

Here we can see how we receive the host fingerprint as a byte array inside the event handler. Once received, we need to compare it to a locally stored copy of the expected fingerprint.

Using WinSCP to Perform a Secure File Upload to an SFTP Server

Another common, although less popular way to upload files to an SFTP server using C# is the WinSCP package:

dotnet add package WinSCP

This library is a .NET wrapper around the winscp.exe executable, so WinSCP is not a cross-platform library and will only run on Windows.

Let’s open a session with an SFTP server by creating a SessionOptions object containing the server’s hostname or address and port along with our user credentials. Unlike SSH.NET, WinSCP will require a server key fingerprint by default:

SessionOptions sessionOptions = new()
{
    Protocol = Protocol.Sftp,
    HostName = "sftp.test-server.com",
    PortNumber = 22,
    UserName = "tester",
    Password = "password",
    SshHostKeyFingerprint = "ssh-rsa 2048 uLaKVzqzc2htyHFWOLvXyweUzOLvvBL0LTsmITcabJw",
};

After that, let’s create a new instance of the Session object and call the Open() method with our SessionOptions instance. Then, let’s use the CreateDirectory() method to create a new directory on the server. After that, let’s call the PutFile() method that takes a FileStream and a destination path in the server to upload our file. Finally, we can list the contents of the new directory using the ListDirectory() method:

try
{
    using Session session = new();
    session.Open(sessionOptions);

    session.CreateDirectory("/images");
    session.PutFile(File.OpenRead("./files/portrait.jpg"), "/images/portrait.jpg");

    var directoryInfo = session.ListDirectory("/images");

    Console.WriteLine("Directory listing:");
    foreach (RemoteFileInfo file in directoryInfo.Files)
    {
        Console.WriteLine($"\t{file.FullName}");
    }

    if (session.Opened)
        session.Close();
}
catch (SessionRemoteException e)
{
    Console.WriteLine(e.Message);
}

Alternatively, we can authenticate using a private key instead of a password. We can do that by passing the path to our private key file to the SshPrivateKeyPath property of the SessionOptions object:

SessionOptions sessionOptions = new()
{
    Protocol = Protocol.Sftp,
    HostName = "sftp.test-server.com",
    PortNumber = 22,
    UserName = "tester",
    SshPrivateKeyPath = @".\ssh-keys\userkey.ppk",
    SshHostKeyFingerprint = "ssh-rsa 2048 uLaKVzqzc2htyHFWOLvXyweUzOLvvBL0LTsmITcabJw"
};

To use it with WinSCP, our private key must be in PPK format. The easiest way to create a PPK key or convert an existing one into PPK format is using PuTTYgen.

Exception Handling With WinSCP

With WinSCP, error handling becomes simpler although arguably less flexible since WinSCP will raise any errors that happen during SFTP operations as a SessionRemoteException with a multi-line exception message describing the problem in detail:

Cannot overwrite remote file '/images/portrait.jpg'.$$
 
Press 'Delete' to delete the file and create new one instead of overwriting it.$$
The file path does not exist or is invalid.
Error code: 10
Error message from server: Path not found.

These error messages come directly from the WinSCP executable execution output and can be tricky to work with if we want to integrate SFTP operations seamlessly into our software.

Best Practices for Secure File Upload to an SFTP Server

Despite SFTP being a very safe way of transmitting data between servers and applications, loosely implemented integrations can make it insecure. Next, we will review a few recommendations to avoid the risk of exposing sensitive data.

Authenticate Using Private Keys

We should favor private key authentication over username and password authentication and always encrypt your keys with strong passphrases.

We should also use the most up-to-date key types and encryption algorithms that both your client library and server support. For example, if your client and server support it, use AES-256 encryption instead of AES-128.

Always Check the Host Key Fingerprint

In one way or another, both client libraries allow host key fingerprint verification. Verifying the server’s identity upon connection will add a layer of protection against man-in-the-middle attacks.

Dealing With Sensitive Data

For the sake of simplicity, code examples in this article contain hardcoded sensitive data like passwords or private keys. We must understand that hardcoding passwords and secret keys in our code is a bad practice and must be avoided.

It is much safer to secure sensitive data separated from the source code and configuration files. We can even consider using some kind of secrets manager like Azure Key Vault or AWS Secrets Manager.

Other SFTP Clients for .NET

Besides the popular free options for SFTP interactions, there’re several commercial packages available:

Rebex SFTP Client is a commercial cross-platform client that supports various authentication methods. It uses a modernized SSH client library with support for updated cipher algorithms.

IPWorks SFTP is another paid cross-platform option that supports strong SSH 2.0 encryption and updated cryptographic algorithms.

Xceed SFTP for .NET yet another licensed client cross-platform implementation of the SFTP protocol. Claims to be high-performant and efficient while RFC compliant.

Conclusion

In this article, we have learned what SFTP is and how it works. We have seen examples of how to upload files to an SFTP server using two of the most popular libraries: SSH.NET and WinSCP.

We have also learned how to authenticate to an SFTP host using a private ssh key and how to verify the server identity by validating the host key fingerprint. Also, we have reviewed the best practices to follow when working with SFTP secure connections. Finally, we have reviewed some third-party libraries that can work with SFTP services.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!