Connections
How to connect to each kind of server: authentication, trust, proxies, session pools and retries, runtime connection changes, and checking a connection before and after you save it.
See the overview for the configuration sections and the mount model.
SFTP: authentication, host keys, and jump hosts
{
"Host": "sftp.internal",
"Username": "deploy",
"AuthenticationMode": "Auto",
"Password": "...",
"PrivateKeyPath": "/secrets/id_ed25519",
"PrivateKeyContent": null,
"AdditionalPrivateKeyPaths": [],
"PrivateKeyPassphrase": "...",
"KnownHostsPath": "/home/app/.ssh/known_hosts",
"HostKeyFingerprints": [],
"Ciphers": ["aes256-gcm@openssh.com", "aes256-ctr"],
"Encoding": "utf-8",
"BufferSize": 262144,
"JumpHost": {
"Host": "bastion.example.com",
"Username": "jump",
"PrivateKeyPath": "/secrets/bastion_ed25519",
"KnownHostsPath": "/home/app/.ssh/known_hosts"
}
}
AuthenticationMode:Password,PrivateKey,KeyboardInteractive(answers the password prompt), orAuto, which offers keys, then password, then keyboard-interactive, and also satisfies servers that demand several methods. Keys can be files or inline text (PrivateKeyContent) from a secret store. SSH agents are not supported by the underlying SSH library.- Host keys are trusted through
HostKeyFingerprints(SHA256:...), an OpenSSHKnownHostsPath(plain, hashed, wildcard, and[host]:portentries), orAutoAcceptHostKeyfor development. A key marked@revokedinknown_hostsis refused even with auto-accept. A rejected key reportsstorage.host_key_rejectedwith the presented fingerprint inDetails. - Algorithms:
KeyExchangeAlgorithms,Ciphers,MacAlgorithms, andHostKeyAlgorithmsrestrict and order what is offered, for hardening or for old servers. Unknown names fail validation and list what is supported. JumpHosttunnels through an SSH bastion. The target's key is still verified against the target's settings, and a configuredProxyapplies to the bastion connection.
FTP and FTPS
{
"Host": "ftp.partner.example",
"EncryptionMode": "Explicit",
"TrustedPublicKeySha256": ["SHA256:..."],
"TlsProtocols": ["Tls12", "Tls13"],
"EncryptDataChannel": true,
"DataConnectionMode": "AutoPassive",
"ActivePortMin": 50000,
"ActivePortMax": 50100,
"ActiveExternalIp": "203.0.113.7",
"Encoding": "windows-1252",
"TransferType": "Binary",
"ListingParser": "Auto",
"ServerTimeZone": "Europe/Copenhagen",
"ReadTimeoutSeconds": 60,
"SocketKeepAlive": true,
"LoginCommands": ["SITE UMASK 022"]
}
- Pins:
TrustedCertificateSha256pins the whole certificate;TrustedPublicKeySha256pins only its public key, so it keeps working across renewals that keep the key. Pinned self-signed certificates are accepted unlessRequireValidCertificateChainis set. Without pins, normal validation applies. TLS problems reportstorage.tls_failure. - Active mode behind NAT:
ActivePortMin/ActivePortMaxandActiveExternalIp. - Encodings such as
windows-1252,iso-8859-1,ibm437, andshift_jisare supported for file names on older servers. - Listings:
ListingParserforces a format (Unix,Windows,Machine, …) andServerTimeZoneconverts times from servers that report local time. - Timeouts:
ConnectTimeoutSeconds,ReadTimeoutSeconds, andDataConnectionTimeoutSecondsfall back toTimeoutSeconds. LoginCommandsrun after every login; a command the server rejects fails the connection so misconfiguration surfaces immediately.
WebDAV
AuthenticationMode accepts None, Basic (sent up front, saving a challenge round trip),
BearerToken, Digest, Ntlm, Negotiate, and Windows (current user). HTTPS endpoints support
TrustedCertificateSha256 and TrustedPublicKeySha256 pins, RequireValidCertificateChain, and a
PFX ClientCertificatePath for mutual TLS. MaxConnectionsPerServer caps concurrent connections.
Cloud emulators and compatible services
- S3-compatible (MinIO, Ceph, …): set
ServiceUrland usuallyForcePathStyle. - Azure Blob: a connection string works with Azurite (
UseDevelopmentStorage=true). - Google Cloud Storage:
ServiceUrlplusAuthenticationMode = Anonymoustargets an emulator such as fake-gcs-server. - Swift: Keystone, or
AuthenticationMode = TempAuthV1for SAIO-style servers (AuthenticationUrlending in/auth/v1.0).
Plain-HTTP endpoints need AllowInsecureHttp = true.
Proxies
Every remote provider can tunnel through an HTTP (CONNECT), SOCKS5, or SOCKS4 proxy:
"Proxy": { "Type": "Socks5", "Host": "proxy.corp.local", "Port": 1080, "Username": "me", "Password": "..." }
SOCKS5 and HTTP proxies resolve the destination host name on the proxy side. SOCKS4 cannot, so the host must resolve from the client, and SOCKS4 carries no password. FTP data connections are tunnelled as well, so use passive mode, and the server's passive address must be reachable from the proxy.
Sessions, retries, and keep-alive
FTP and SFTP keep authenticated sessions in a per-connection pool, and FTP, SFTP, and WebDAV retry transient failures automatically. Both are tuned per connection:
{
"Session": {
"MaxSessions": 4,
"MaxIdleSessions": 2,
"IdleLifetimeSeconds": 120,
"AcquireTimeoutSeconds": 30,
"ValidateAfterIdleSeconds": 15,
"KeepAliveSeconds": 60
},
"Retry": {
"RetryCount": 3,
"BaseDelayMs": 100,
"MaxDelayMs": 30000,
"RetryNonIdempotent": false
}
}
MaxSessionscaps open sessions, busy or idle, so the library stays under a server's per-user connection limit. Callers beyond it wait up toAcquireTimeoutSeconds, then receivestorage.server_busy.- A pooled session idle longer than
ValidateAfterIdleSecondsis probed (FTPNOOP, SFTPstat) before reuse. Sessions that time out, drop, or fail TLS mid-operation are closed instead of reused. KeepAliveSecondssends FTPNOOPor SSH keep-alive packets while a session is open.- Reads, listings, info, and directory creation retry on timeouts, refused or dropped connections,
and busy servers, with exponential backoff and jitter; a server
Retry-Afteris honored. Uploads retry only from a seekable stream, which is replayed from its starting position; staged uploads never leave a partial file. Deletes and moves retry only withRetryNonIdempotent, because the first attempt may already have succeeded. - Set
RetryCountto0for single-attempt behavior.
Runtime connections and native clients
await storage.AddOrUpdateConnectionAsync("backup", new SftpConnectionConfig
{
Host = "sftp.example.com",
Username = "backup",
AuthenticationMode = SftpAuthenticationMode.PrivateKey,
PrivateKeyPath = @"C:\keys\backup_ed25519",
HostKeyFingerprints = ["SHA256:..."]
});
Result<HealthStatus> health = await storage.CheckConnectionHealthAsync("backup");
Runtime changes are persisted to the provider's JSON section, or installed for the process only with
persist: false. A replacement is health-checked before it goes live. RegisterBackend installs a
custom IStorageBackend.
Reusable native SDK clients and scoped session clients remain available as an escape hatch for provider administration (bucket/container creation, IAM/lifecycle policies) and non-portable options:
IAmazonS3 s3 = storage.GetNativeClient<IAmazonS3>("media");
var opened = await storage.OpenNativeConnectionAsync<AsyncFtpClient>("legacy-ftp");
if (opened.IsSuccess)
{
await using var lease = opened.Value!;
AsyncFtpClient ftp = lease.Client;
}
Do not dispose reusable clients returned by GetNativeClient; dispose session leases.
Testing settings before saving them
TestConnectionAsync tries settings without saving or registering them, even before the library is
initialized. It reports each step it ran — validate, connect, list, details — and stops at the
first failure:
var settings = new SftpConnectionConfig { Host = "sftp.example.com", Username = "deploy", Password = "..." };
var report = await storage.TestConnectionAsync(settings);
foreach (var step in report.Steps)
Console.WriteLine($"{step.Name,-8} {(step.Succeeded ? "ok" : step.Error!.Code)} {step.Duration.TotalMilliseconds:0} ms");
When the server's certificate or host key is rejected, ServerIdentity still says what was presented,
which is what a setup screen needs for "the server presented this fingerprint — trust it?":
if (!report.Succeeded && report.ServerIdentity is { Kind: "ssh-host-key" } key)
{
// Show key.Fingerprint and key.Algorithm, and on confirmation:
settings.HostKeyFingerprints = [key.Fingerprint];
}
if (!report.Succeeded && report.ServerIdentity is { Kind: "tls-certificate" } certificate)
{
// certificate.Subject, certificate.Issuer, certificate.NotAfter describe it.
// Pin the public key so the pin survives a renewal that keeps the key:
ftpSettings.TrustedPublicKeySha256 = [certificate.PublicKeyFingerprint!];
}
Diagnosing a live connection
var diagnostics = (await storage.GetConnectionDiagnosticsAsync("partner")).Value!;
Console.WriteLine($"{diagnostics.Host}:{diagnostics.Port} over {diagnostics.Security}");
Console.WriteLine($"{diagnostics.ServerSoftware} ({diagnostics.ServerSystem})");
| Property | FTP/FTPS | SFTP | WebDAV |
|---|---|---|---|
Security |
None or Tls |
Ssh |
None or Tls |
ServerSystem |
SYST reply |
SSH version string | Server header |
ServerSoftware |
detected server type | e.g. OpenSSH_9.6p1 |
first Server token |
ServerFeatures |
FEAT capabilities |
— | DAV classes and Allow methods |
Negotiated |
tls, cipher |
kex, hostKey, cipher, mac, sftp |
http version |
ServerIdentity |
certificate | host key | certificate |
Pool |
session counters | session counters | — |
Other providers report host, port, security, and LastHealth. GetConnections() also carries
Host, Port, Security, and LastHealth for every configured connection. Diagnostics never contain
credentials.