C# SFTP: SSH.NET and WinSCP Examples, and a Way That Skips Both
.NET has no SFTP client in the framework, so every C# SFTP job depends on something. Here is working code for SSH.NET, the library most of them use, the WinSCP .NET assembly for teams already standardized on it, and a third way where Files.com connects to the partner's server and your C# talks to Files.com over HTTPS, plus the host key check SSH.NET leaves to you and failing loudly on a schedule.
C# SFTP code rarely lives in the glamorous part of a .NET estate. It lives in the Windows service that sends a nightly extract to the bank, the Azure Function that collects files from a supplier, and the console application on a scheduled task that finance depends on even though nobody remembers who wrote it.
.NET has no SFTP client in the framework, so every one of those depends on something. There are three sensible choices: SSH.NET, the library behind most of the SFTP code written in C#; the WinSCP .NET assembly, for teams already standardized on WinSCP; and moving SFTP out of the application by letting Files.com connect to the remote server.
Here is complete code for each, along with the details that short samples leave out: host key verification, credential handling, deployment, and making a scheduled transfer fail loudly.
SSH.NET, the library most C# code uses
SSH.NET is a managed SSH implementation with an SftpClient for uploading, downloading, and managing remote files, and it runs anywhere modern .NET runs. Install it from NuGet:
dotnet add package SSH.NET
An unattended job wants a private key rather than a password:
using Renci.SshNet;
var key = new PrivateKeyFile("/etc/sftp/keys/brightway_ed25519");
using var client = new SftpClient("sftp.example.com", 22, "alice", key);
client.HostKeyReceived += (_, e) =>
{
var expected = Environment.GetEnvironmentVariable("BRIGHTWAY_HOST_KEY_SHA256");
e.CanTrust = e.FingerPrintSHA256 == expected;
};
client.Connect();
using (var local = File.OpenRead("/data/exports/orders.csv"))
{
client.UploadFile(local, "/inbound/orders.csv");
}
client.Disconnect();
PrivateKeyFile loads the key; if it is encrypted, pass the passphrase as a second argument, read from a secret store rather than the source. The SftpClient constructor takes the host, port, username, and either the key or a password string. Connect() opens the session, UploadFile(stream, remotePath) streams the local file up, and because the client is wrapped in using, the connection is disposed even when the upload throws.
Downloading and listing are just as direct:
foreach (var entry in client.ListDirectory("/outbound"))
{
Console.WriteLine($"{entry.Name} {entry.Length} bytes");
}
using (var local = File.Create("/data/imports/report.csv"))
{
client.DownloadFile("/outbound/report.csv", local);
}
The host key check SSH.NET leaves to you
The most important part of the upload example is not UploadFile(). It is the HostKeyReceived handler. A server's SSH host key proves you reached the server you meant rather than another machine answering at the same hostname, and OpenSSH checks it against known_hosts on every connection.
SSH.NET does not read known_hosts, and if you leave the handler out it trusts whatever answers at sftp.example.com. That is the gap a DNS mistake, a proxy misconfiguration, or a compromised network walks through with a sensitive file.
The pattern is simple. Ask the server's administrator for the SHA-256 host key fingerprint, store it in configuration or your secrets system, compare it inside HostKeyReceived, and set CanTrust to false on a mismatch. The connection fails, the job fails, and someone investigates. A changed host key stops the job; it is never a warning the transfer gets to ignore.
The WinSCP .NET assembly
If the job runs on Windows and the team already uses WinSCP, its .NET assembly drives the winscp.exe executable from C#:
using WinSCP;
var options = new SessionOptions
{
Protocol = Protocol.Sftp,
HostName = "sftp.example.com",
UserName = "alice",
SshPrivateKeyPath = @"C:\sftp\keys\brightway.ppk",
SshHostKeyFingerprint = "ssh-ed25519 255 SHA256:...",
};
using var session = new Session();
session.Open(options);
session.PutFiles(@"C:\data\exports\orders.csv", "/inbound/").Check();
session.GetFiles("/outbound/report.csv", @"C:\data\imports\").Check();
Two things recommend it for scheduled transfers. SshHostKeyFingerprint is part of the normal configuration and the session refuses to open if the server presents a different key, so the check from the previous section cannot be forgotten. And PutFiles() and GetFiles() return a result whose Check() throws on any failure, so a partial transfer can never be reported as a successful run.
The cost is deployment: the application depends on winscp.exe as well as the assembly, which means Windows only, and the key is in PuTTY's .ppk format. If you have an OpenSSH key to convert, the guide to importing SSH keys into WinSCP walks through it.
For Linux services and containers, SSH.NET is the simpler choice.
Let Files.com make the SFTP connection
With either library, SFTP is your application's responsibility, and that is more than a call to UploadFile(). It is a package or executable to keep current, a private key per connection, a pinned host key per partner, outbound access on port 22, retry and recovery behavior you write yourself, transfer logs and alerts, and a key rotation procedure.
For one or two connections that is reasonable. It gets hard as partners accumulate.
The third option is to let Files.com connect to the partner's SFTP server on the cloud side while your .NET code talks to Files.com over HTTPS.
Mount the partner's server as a folder
A Remote Server Mount presents an external SFTP server as a folder on your Files.com site. Mount a partner's server at /partners/brightway and paths like /partners/brightway/inbound/orders.csv become ordinary Files.com paths to your application. Every operation on the mounted folder passes through to the partner's server as it happens: an upload writes the file there, and listing, downloading, renaming, and deleting work through the same mount. Files.com keeps no copy.
Files.com manages the SSH connection
The mount holds the partner's hostname, port, credentials, and host key. Files.com authenticates with a password, a private key, or both, and it can generate the key pair itself (RSA, 4096-bit): you send the public half to the partner's administrator and the private half never leaves Files.com. An existing key can be supplied in OpenSSH, PuTTY, or SSH2 format, and every credential is stored encrypted.
Host key verification is handled at the mount, the way the SSH.NET section said to handle it, without the handler. Files.com detects and stores the partner's host key on the first connection, or an administrator enters the fingerprint the partner supplied, and if the server ever presents a different key the connection is disabled until an administrator approves the change.
The partner allowlists the Files.com IP addresses once instead of maintaining an outbound port-22 rule for every application environment.
The C# application uses HTTPS
With the mount in place, the .NET code uses the Files.com .NET SDK over HTTPS with an API key:
dotnet add package FilesCom
using FilesCom;
using FilesCom.Models;
var config = new FilesConfiguration
{
ApiKey = Environment.GetEnvironmentVariable("FILES_API_KEY"),
};
var files = new FilesClient(config);
await RemoteFile.UploadFile("/data/exports/orders.csv",
"/partners/brightway/inbound/orders.csv");
await RemoteFile.DownloadFile("/partners/brightway/outbound/report.csv",
"/data/imports/report.csv");
For uploads the local path comes first and the destination second; downloads take the remote path first and the local path to write second, and both have stream overloads for content generated in memory.
From the application's point of view the transfer now uses HTTPS on port 443. It needs no SSH.NET, no winscp.exe, no partner key, no fingerprint, and no port 22.
The SDK retries and resumes interrupted transfers, and every operation appears in the Files.com audit log with its user, file, action, and timestamp. If your site runs on a custom domain, set BaseUrl on the configuration.
You may not need C# at all
Once the partner's server is a folder on Files.com, some scheduled transfer jobs get removed rather than rewritten. A Sync pulls files from the partner or pushes files to them on a schedule, and an Automation renames, routes, copies, or processes files when they arrive.
The documented pattern is a supplier that uploads invoices to its own SFTP server: Files.com pulls them every ten minutes, and a Move Files automation routes each one to its processing folder.
The .NET code keeps the business rules and stops being a scheduler, transfer client, retry engine, and audit system as well.
Existing SSH.NET code keeps working too
Moving partner connections into Files.com does not force an immediate rewrite. Files.com also accepts inbound SFTP on port 22 with SSH key authentication.
Point the SSH.NET or WinSCP code above at yourcompany.files.com, upload into /partners/brightway/inbound, and Files.com passes the transfer through to the partner's server. The application gets one stable endpoint, credential, and host key, while partner-specific credentials and fingerprints stay attached to their mounts: ten partners are ten mounts and one connection path in the code.
A partner whose server sits inside a private network is reachable through the Files.com Agent, which connects outbound with no inbound firewall rule.
Deploying SFTP code safely
Most production failures happen around the transfer code, not inside UploadFile(). Never hardcode a password, passphrase, or API key; read them from environment variables, Azure Key Vault, AWS Secrets Manager, or whatever secrets system your platform already uses, as the GetEnvironmentVariable lines above do.
Keep the private key readable only by the account running the service, and test under that account rather than your own login, because a transfer that works interactively can fail in production when the Windows service account or container user cannot read the key.
If the receiving system processes files as they appear, upload to a temporary name such as orders.csv.uploading and rename it only after the transfer finishes.
And fail loudly: return a nonzero exit code or a failed state whenever the job cannot complete, log the start and end times, paths, size, result, and error, and connect the scheduler to an alert.
Catching the exception, logging it at Debug, and exiting successfully is how a missing nightly file goes unnoticed for a week.
Which one to use
SSH.NET when SFTP belongs inside your application logic, the code has to run on Windows, Linux, or in containers, and the HostKeyReceived handler is in place.
The WinSCP .NET assembly when the job runs on Windows and the organization already trusts WinSCP's session configuration and result checking.
A Files.com Remote Server Mount when you want the application on HTTPS instead of SFTP, when several partners mean several keys and fingerprints to manage, when transfers need central auditing, or when some of them can become a Sync with no code.
If C# is not the language for the job, the same three ways are covered for PowerShell, Python, Java, and Node.js, and the SFTP automation guide covers bash and cron.
When code stops being the right answer
Two stable SFTP jobs inside a .NET service do not justify a platform change. Dozens of partner connections, each with a separate key, owner, schedule, and failure mode, are a different problem, and that is the point where file transfer stops being a small piece of application code and becomes infrastructure.
Files.com is the cloud-native File Orchestration Platform: one platform that replaces the stack of legacy tools IT teams run to move files, including the SFTP servers, the services that feed them, and the schedulers holding it all together. It speaks every protocol, connects 50+ cloud and on-prem systems, automates every transfer, and keeps a complete audit trail.
For the job in this post, that means the scheduled upload becomes a Files.com automation with retries and a delivery record, and C# stays for the logic that is genuinely yours, calling the SDK. If the SFTP server has to stay in your own data center, the ExaVault appliance is a free SFTP server for up to 50 users that every example above can target.
Start a free Files.com trial and point the SDK at it. No credit card required, and an SFTP endpoint with SSH key support is live in minutes.
Frequently asked questions
Does .NET have a built-in SFTP client?
No. Neither the .NET Framework nor the modern .NET runtime includes an SFTP client. Add SSH.NET for a managed client that runs anywhere .NET does, use the WinSCP .NET assembly on Windows, or use the Files.com SDK over HTTPS and let Files.com make the SFTP connection.
How do I upload a file over SFTP in C#?
With SSH.NET, construct SftpClient with the host, username, and a PrivateKeyFile, verify the fingerprint in HostKeyReceived, call Connect(), then UploadFile(stream, remotePath) with the local file opened as a stream. With Files.com, call await RemoteFile.UploadFile(localPath, remotePath) over HTTPS, and a Remote Server Mount carries the file on to the partner's SFTP server.
Does SSH.NET verify host keys automatically?
No. It does not read known_hosts. Subscribe to HostKeyReceived, compare the presented fingerprint to the one the server's administrator gave you, and set CanTrust to false on a mismatch so the connection fails.
How do I use an SSH key with SSH.NET?
Load it with new PrivateKeyFile(path), or new PrivateKeyFile(path, passphrase) if it is encrypted, and pass it to the SftpClient constructor in place of a password. The passphrase comes from a secrets manager, not from the code.
Can Files.com connect to my partner's SFTP server for me?
Yes. A Remote Server Mount connects a folder on your Files.com site to any SFTP server in real time. Files.com holds the credentials, can generate the key pair, pins the partner's host key, and passes every operation through. Your C# uploads to Files.com over HTTPS with the SDK, or a scheduled Sync moves the files with no code at all.