Skip to content

SFTP Storage Provider

Prerequisites: Storage Providers Overview

The NPipeline.StorageProviders.Sftp package implements IStorageProvider for SFTP servers. Supports connection pooling, password and private key authentication (including encrypted keys), keep-alive, server fingerprint validation, and health checks on pool acquire.

Installation

bash
dotnet add package NPipeline.StorageProviders.Sftp

Dependencies: SSH.NET 2025.x

Quick Start

csharp
var options = new SftpStorageProviderOptions
{
    DefaultHost = "sftp.example.com",
    DefaultUsername = "etl-user",
    DefaultKeyPath = "/path/to/private_key"
};
var factory = new SftpClientFactory(options);
var provider = new SftpStorageProvider(factory, options);

var stream = await provider.OpenReadAsync(
    StorageUri.Parse("sftp://sftp.example.com/data/orders.csv"));

URI Format

Credentials in the URI override defaults from configuration:

sftp://[user[:password]@]host[:port]/path/to/file
ComponentDescription
userOptional - override DefaultUsername
passwordOptional - override DefaultPassword
hostSFTP hostname
portOptional - override DefaultPort (default: 22)
path/to/fileFile path on the server

Authentication

Password

csharp
var options = new SftpStorageProviderOptions
{
    DefaultHost = "sftp.example.com",
    DefaultUsername = "etl-user",
    DefaultPassword = "secret"
};

Private Key

csharp
var options = new SftpStorageProviderOptions
{
    DefaultHost = "sftp.example.com",
    DefaultUsername = "etl-user",
    DefaultKeyPath = "/path/to/id_rsa"
};

Encrypted Private Key

csharp
var options = new SftpStorageProviderOptions
{
    DefaultHost = "sftp.example.com",
    DefaultUsername = "etl-user",
    DefaultKeyPath = "/path/to/id_rsa",
    DefaultKeyPassphrase = "key-passphrase"
};

Configuration

Connection

PropertyTypeDefaultDescription
DefaultHoststring?nullSFTP hostname
DefaultPortint22SSH port
DefaultUsernamestring?nullSSH username
DefaultPasswordstring?nullPassword auth
DefaultKeyPathstring?nullPath to SSH private key
DefaultKeyPassphrasestring?nullEncrypted key passphrase

Connection Pool

PropertyTypeDefaultDescription
MaxPoolSizeint10Maximum pooled connections
ConnectionIdleTimeoutTimeSpan5 minEvict idle connections after this duration
KeepAliveIntervalTimeSpan30sSSH keepalive interval
ConnectionTimeoutTimeSpan30sConnection timeout
ValidateOnAcquirebooltrueHealth-check connections when borrowed from pool

Security

PropertyTypeDefaultDescription
ValidateServerFingerprintbooltrueVerify the server host key
ExpectedFingerprintstring?nullExpected fingerprint (auto-accepts on first connect if null)

Dependency Injection

csharp
services.AddSftpStorageProvider();

services.AddSftpStorageProvider(options =>
{
    options.DefaultHost = "sftp.example.com";
    options.DefaultUsername = "etl-user";
    options.DefaultKeyPath = "/path/to/id_rsa";
    options.MaxPoolSize = 20;
});

Registers: IStorageProvider, IStorageProviderMetadataProvider

Features

  • Connection pooling - reuse SSH connections across operations (default pool size: 10)
  • Keep-alive - prevents server-side idle timeouts (30s default)
  • Health checks - validates connections on acquire; dead connections are replaced automatically
  • Fingerprint validation - prevents MITM attacks; auto-accepts on first connect when ExpectedFingerprint is null
  • Idempotent delete - treats 404 as success

Examples

Reading

csharp
var uri = StorageUri.Parse("sftp://sftp.example.com/data/orders.csv");

using var stream = await provider.OpenReadAsync(uri);
using var reader = new StreamReader(stream);
var content = await reader.ReadToEndAsync();

Writing

csharp
var uri = StorageUri.Parse("sftp://sftp.example.com/output/results.csv");

using var stream = await provider.OpenWriteAsync(uri);
using var writer = new StreamWriter(stream);
await writer.WriteLineAsync("id,name,value");

Listing

csharp
var prefix = StorageUri.Parse("sftp://sftp.example.com/data/");

await foreach (var item in provider.ListAsync(prefix, recursive: true))
{
    var type = item.IsDirectory ? "[DIR]" : "[FILE]";
    Console.WriteLine($"{type} {item.Uri} - {item.Size} bytes");
}

Multiple SFTP Servers

The URI determines which host is used; authentication from configuration applies to all:

csharp
var uri1 = StorageUri.Parse("sftp://server1.example.com/path/file1.csv");
var uri2 = StorageUri.Parse("sftp://server2.example.com/path/file2.csv");

Error Handling

The provider translates common SFTP exceptions into SftpStorageException:

ScenarioException
File not foundFileNotFoundException
Permission deniedUnauthorizedAccessException
Connection refusedIOException
Authentication failedUnauthorizedAccessException
TimeoutIOException (timeout context preserved)

Connection Pool Tuning

ScenarioRecommended Settings
Low-volume, single serverMaxPoolSize = 3, KeepAliveInterval = 60s
High-volume, single serverMaxPoolSize = 20, KeepAliveInterval = 15s
Flaky networkValidateOnAcquire = true, ConnectionTimeout = 60s

Best Practices

  1. Use key-based auth in production - avoid passwords
  2. Set ExpectedFingerprint in production to prevent MITM
  3. Enable ValidateOnAcquire (default) - catches dead connections before use
  4. Tune MaxPoolSize to match concurrency needs - too many connections may overwhelm the SFTP server
  5. Use KeepAliveInterval to prevent server idle disconnects

Next Steps

Released under the MIT License.