Skip to content

Releases: o-kolomoiets/Go-SFTP-Server

v0.4.0

Choose a tag to compare

@o-kolomoiets o-kolomoiets released this 17 Oct 15:39
ea43424

[0.4.0] - 2026-10-17

Multi-user operation: reload on SIGHUP, users in users.d, SSH
certificates, host key rotation, Prometheus metrics, hooks, PROXY protocol,
a systemd unit and Landlock on Linux. It also fixes a crash that an
authenticated client could cause (see Fixed). Read
docs/operations.md before upgrading; five
changes need attention: headers from load balancers not in
server.trusted_proxies are refused; serve refuses trusted files inside
mounts and two host keys of one type; on Linux Landlock is on by default,
so some changes need a restart instead of a reload; and cert-authority
and command="internal-sftp" lines in authorized_keys, which v0.3
skipped with a warning, now take effect.

When upgrading a server behind a load balancer that sends PROXY protocol
headers, add the load balancer to server.trusted_proxies together with
the new binary (v0.3 refuses the key), or turn PROXY protocol off on the
load balancer before the upgrade: headers from any other address are now
refused (see Changed).

Upgrading

  • A systemd unit with its own SystemCallFilter= must allow the three
    system calls of Landlock: landlock_create_ruleset, landlock_add_rule
    and landlock_restrict_self (@system-service has none of them, and
    @sandbox exists only from systemd 254). Without them gosftpd runs
    without Landlock and logs landlock: not available with the reason
    "blocked by a seccomp filter" (with SystemCallErrorNumber=) or "killed
    by a seccomp filter" (a filter that kills, such as the one of the
    interim unit of the v0.3 hardening guide); server.start records
    landlock = blocked in both cases. The shipped unit allows them; to
    move to it from the interim unit, see
    docs/install.md.
  • Check the authorized_keys files for cert-authority lines and lines
    with command="internal-sftp" (or a path to sftp-server). v0.3
    skipped them with a warning; v0.4 uses them, so a cert-authority line
    lets in the certificates its CA signs for the user (see Changed). Remove
    the lines that should not grant logins before upgrading.

Added

  • Reload on SIGHUP (systemctl reload): the audit log file is reopened
    first (for logrotate), then the configuration file is read and checked
    again and applied to new logins: users, keys, passwords, mounts,
    permissions, limits, bans settings and the log level. Open connections
    keep the configuration they logged in under; with the new
    reload.disconnect_removed_users they are closed when the new
    configuration would refuse their login. An invalid file keeps the running
    configuration. A key file that is missing or unsafe on reload revokes its
    keys instead of failing the reload, and a mount that is unavailable is
    left out until the next reload. A remounted mount directory is opened
    anew for new logins, with mount reopened in the log (under Landlock:
    after a restart, see Changed). See
    docs/configuration.md
    and ADR 0005.
  • Audit event server.reload; conn.close result revoked.
  • sd_notify: READY=1, RELOADING=1 with MONOTONIC_USEC, STOPPING=1
    (Type=notify-reload).
  • user add --write writes users.d/NAME.toml after checking the
    configuration with the new user, and prints the connection details for
    the user (host, port, host key fingerprint, sftp command). user disable, enable and remove change a user's file without touching the
    rest of it; the main configuration file is never rewritten. gosftpd init
    includes users.d/*.toml.
  • auth.failure has a reason when the login was refused although the key
    offered is one of the user's or the password is right: disabled,
    expired, address, key_expired or removed. The client is told
    nothing more, and the key of such an account is refused already when
    offered, as before. A key is checked again once the client has proved
    that it holds it (VerifiedPublicKeyCallback), against the configuration
    current then.
  • user add --write prints the warnings about the new user and refuses one
    that could not log in (for example a password while auth.methods has
    only publickey) unless --force.
  • OpenSSH user certificates (ADR 0007):
    CAs trusted for every user (auth.trusted_user_ca_keys,
    trusted_user_ca_keys_file), with users.NAME.principals to map
    certificate principals to users, and cert-authority lines with
    principals= in authorized_keys. Certificates without principals, with
    SHA-1 CA signatures or with critical options other than source-address
    and an SFTP-only force-command are refused. See
    docs/configuration.md.
  • Revoked keys and certificates (auth.revoked_keys, revoked_keys_file):
    a certificate revokes its key, as in sshd. The list fails closed: a line
    that is not a key, or a KRL, is an error at start and fails a reload.
  • auth.success names a certificate login (cert_key_id, cert_serial as
    a decimal string, cert_ca_fp; key_fp is the certified key's);
    auth.failure gets key_fp and these fields with a reason, and the
    reasons key_revoked, cert_principal, cert_expired,
    cert_not_yet_valid and cert_invalid.
  • user add takes --principal; with --write and trusted CAs a user
    needs no key, and the connection details show the ssh-keygen command to
    sign its certificate. user list shows the principals.
  • command="internal-sftp" (or a path to sftp-server, without arguments)
    is accepted in authorized_keys, since gosftpd serves only SFTP.
  • Host key rotation without breaking clients
    (ADR 0008): after login, gosftpd
    announces its host keys to OpenSSH clients, with a next key KEY.next
    and a previous key KEY.old (hostkeys-00@openssh.com), and proves that
    it holds them (hostkeys-prove-00@openssh.com), so that OpenSSH 8.5 and
    later add the next key to known_hosts before it is used and drop retired
    keys. gosftpd hostkey rotate creates the next key; --finish,
    --rollback, --retire and --abort take the further steps. New setting
    server.announce_host_keys (on by default); new audit event
    conn.hostkeys_proved. See
    docs/configuration.md.
  • Host certificates: with server.host_certificates = true, KEY-cert.pub
    is served with each host key while it is valid, after the checks OpenSSH
    applies; warnings before expiry. Go clients that pin the plain host key
    (rclone) then need host_key_algorithms, see
    Host certificates.
  • hostkey show --config shows every configured host key with its next and
    previous keys and certificates; the start banner and user add --write
    show the next key. During a rotation, or with a certificate, hostkey show prints more lines after those of the current key: a script that
    takes its last line should take the third.
  • Admin listener (ADR 0009):
    [metrics] listen serves /metrics (Prometheus text format, written by
    gosftpd itself, without a new dependency), /healthz and /readyz
    (starting, stopping, audit_unavailable). Metrics count connections
    by result (including health probes and scanners, which the audit log
    leaves out), refusals, logins, authentication attempts, SFTP operations,
    bytes, transfer times, conflicts, bans, reloads and audit write errors,
    with labels from fixed sets only. metrics.pprof serves /debug/pprof in
    binaries built with -tags pprof. See docs/metrics.md.
  • gosftpd healthcheck for container health checks: without --url it
    waits for the SSH banner of server.listen and leaves no trace in the
    audit log; with --url it checks the admin listener. It exits with 0 or
    1 only.
  • server.start has metrics_listen with an admin listener.
  • Hooks (ADR 0010, docs/hooks.md):
    [[hooks]] run a program (exec, argv without a shell, GOSFTPD_*
    variables, the event on stdin) or call a webhook (the audit line, signed
    with HMAC-SHA256 over the delivery ID, timestamp and body, with retries)
    when a file event succeeds, filtered by on, mounts, glob and
    exclude, optionally after a delay. Asynchronous, with a bounded queue
    per hook; a reload updates hooks in place, for open connections too. An
    exec hook runs only if its file is still the one of the event, and its
    program still belongs to root or gosftpd's user; its process group is
    killed when it ends or times out. New metrics
    gosftpd_hook_deliveries_total, gosftpd_hook_retries_total,
    gosftpd_hook_queue_length and gosftpd_hooks_not_updated.
  • Audit: every fs.* event on a path in a mount has mount; fs.rename
    has "uploaded":true for the first rename by its uploader, within an
    hour, of a file whose upload closed ok: the rename that completes a
    temp-file upload (WinSCP's .filepart, rclone's .partial), or a plain
    upload renamed later. fs.upload hooks fire for it.
  • PROXY protocol v1 and v2 behind a load balancer
    (ADR 0011): a connection from
    an address in the new server.trusted_proxies must begin with a PROXY
    protocol header, which names the client. Bans, max_connections_per_ip,
    allow_from, from=, certificate source-address and the audit log's
    remote_addr and local_addr then see the client, and the new context
    field proxy_addr names the load balancer. LOCAL and UNKNOWN headers
    (health checks, clients of a Unix socket front end) are the loa...
Read more

v0.3.0

Choose a tag to compare

@o-kolomoiets o-kolomoiets released this 09 Oct 21:22
2bd0b1f

Hardening for servers exposed to the internet. Read
docs/security/hardening.md before upgrading a
public server; two changes need attention: serve refuses to run as root
without --allow-root, and uploads stop below 1 GiB of free space unless
min_free_space says otherwise.

Added

  • Connection limits checked right after accept, before the SSH handshake:
    limits.max_connections (256), max_connections_per_ip (16, per IPv4
    address or IPv6 /64) and max_preauth_connections (64). Refused
    connections are audited as conn.reject, at most 10 per second.
  • Bans ([auth.ban]): a source with 10 failures within 10 minutes is
    refused at accept for 30 minutes, and its open connections get no further
    password checks. Every wrong password is a failure at once; rejected keys
    count once per connection, not per offered key; loopback is exempt by
    default; the table is bounded. New audit event auth.ban.
  • server.idle_timeout (15m) closes connections without SFTP traffic;
    server.keepalive_interval (30s) closes connections that leave 3
    keepalive@openssh.com requests unanswered. conn.close reports
    idle_timeout or keepalive_timeout.
  • Opt-in password login: auth.methods = ["publickey", "password"] and
    password_hash (argon2id; bcrypt accepted for imported accounts). Each
    attempt checks one hash; unknown users are checked against a stand-in of
    the costliest configured hash, and failures wait until it would have
    finished, so the response time does not reveal which users exist (fully
    with one kind of hash; config validate warns about mixed ones). Checks
    run at most one per available CPU. A method missing from auth.methods is
    refused, and its keys or passwords are not loaded. gosftpd user hash-password [--stdin] prints a hash, and
    user add --password-hash adds it to a user.
  • server.crypto_policy: modern (default, as before) or compat, which
    adds NIST curve and SHA-2 Diffie-Hellman key exchange and non-ETM MACs for
    old clients.
  • on_conflict = "version": an upload or posix-rename onto an existing file
    replaces it and moves the old file to
    .versions/<path>/<stem>.<UTC time><ext>, keeping versions.keep (10)
    versions per file for at most versions.max_age (30 days); only users who
    may delete or overwrite files make older versions drop out. The versions
    directory is not listed (sync tools would delete it); clients can open it
    by path and read it, but not change it. Replacing a file this way
    needs only the write permission. fs.upload and fs.rename get
    conflict = "versioned" and version_path.
  • atomic_uploads: uploads are written to a hidden temporary file
    (.gosftpd-*.part) and take their name only when closed, by the conflict
    policy at that moment; an aborted upload leaves nothing. Resume is not
    possible then. fsync flushes such uploads before they are published.
    Temporary files left by a crash are removed once 24 hours old (checked at
    start and every 6 hours) and when their directory is removed.
  • max_file_size (off by default), checked against the end of every write
    and on truncation, and min_free_space (1 GiB), checked when a file is
    opened for writing. Sizes accept units such as "10GiB".
  • Fuzzing: six targets (path resolution, confinement on a tree with
    symlinks, conflict copy names, the configuration, authorized_keys, and
    arbitrary SFTP packets against the real handlers) run 60 seconds each on
    every change and 10 minutes each night.
  • CI checks coverage floors of the security packages (unit and interop
    coverage merged), audits crypto_policy = "modern" with ssh-audit, and
    runs OpenSSF Scorecard weekly; all actions are pinned by commit SHA.
  • Documents: threat model,
    hardening guide, a manual WinSCP checklist,
    and ADR 0004 on
    limits@openssh.com.

Changed

  • BREAKING: gosftpd serve refuses to run as root unless --allow-root
    is given.
  • A connection keeps the user name of its first authentication request, as
    sshd does; attempts with another name are refused.
  • auth.success has key_fp only for public-key logins.
  • Uploads are refused while the mount's filesystem has less than 1 GiB free
    (min_free_space; "0" turns the check off).
  • fs.rename for a posix-rename that replaced its target under
    on_conflict = "overwrite" reports conflict = "overwritten" instead of
    "none".
  • Names starting with .gosftpd- (in any case) are reserved: hidden from
    listings and refused in paths.
  • BREAKING: rename_template must contain {n} exactly once and be
    valid UTF-8 of at most 64 bytes, so that copy names always fit 255 bytes;
    config validate names a template that does not.

Fixed

  • A client that disconnected right after logging in left no auth.success
    and conn.close events.
  • A client that sent requests with a guessed handle while an OPEN was in
    progress raced with the OPEN inside pkg/sftp, which could crash the
    server; gosftpd now passes an OPEN on only after the earlier requests are
    answered, and requests with a handle only after the OPEN is (found by
    fuzzing).
  • Copy names under on_conflict = "rename" could exceed 255 bytes for
    names with a very long extension; a shortened name could equal the
    original.
  • A configuration file whose first line ended inside an escape sequence
    (a = "C:\) made serve and config validate panic instead of
    reporting the error (found by fuzzing).

v0.2.0

Choose a tag to compare

@o-kolomoiets o-kolomoiets released this 09 Oct 05:22
ae4b125

Added

  • Configuration file (TOML, config_version = 1): found via --config,
    $GOSFTPD_CONFIG, ./gosftpd.toml, /etc/gosftpd/config.toml or the user
    configuration directory. Unknown keys are an error; every problem is
    reported at once with its key path. include merges [users.NAME] tables
    from other files.
  • Multiple users ([users.NAME]): inline authorized_keys and
    authorized_keys_file, allow_from, expires, disabled.
  • Per-mount permissions: presets read, upload, readwrite, full or
    flags (list, read, write, overwrite, delete, rename, mkdir,
    rmdir, setstat). Uploaders may rename and set times on files they
    created in the same session (WinSCP .filepart, rclone .partial).
  • Per-user home directories: a mount path ending in {user}. A home that is
    a symlink or not a directory makes the mount unavailable (audited as
    fs.denied with reason=home_not_dir).
  • Mount options: create, read_only, on_conflict, rename_template,
    max_rename_attempts, compound_extensions, setstat_mode, umask,
    require_mountpoint; [defaults] for all mounts and flatten.
  • Server options: several listen addresses, host_keys with
    host_key_auto_generate, handshake_timeout, shutdown_timeout;
    [limits] max_sessions_per_conn, max_open_handles, max_auth_tries.
  • Commands: init, config validate [--check-fs], config show,
    config example [--full], user add, user list, hostkey generate,
    completion.
  • Startup checks like sshd's StrictModes: configuration, authorized_keys
    and host key files must not be writable by group or others and must
    belong to root or to the user running gosftpd.
  • Resumable uploads (OpenSSH reput, paramiko mode a, Cyberduck) with an
    append-only guard: the bytes a file had when it was opened cannot be
    changed or truncated (resume = "append-only", the default, or "off").
    With on_conflict = "overwrite" and the overwrite permission a resume
    without APPEND is a plain write; with APPEND the old end is always
    kept (offsets are used as sent, so a client that appends at offset 0 gets
    "existing data is immutable"). fs.upload records start_offset; a
    refused write is audited as fs.denied and the upload as denied.
    After a renamed put, a reput of the same name continues the copy.
  • A file that an upload has open cannot be opened for writing in place by
    another upload (overwrite or resume) until it is closed: "file is busy".
  • stat_redirect (default on): after an upload or rename went to a new name
    under the rename policy, STAT, LSTAT and SETSTAT of the requested name by
    the same user answer for the new one for 60 seconds, in all of the user's
    connections, so paramiko put(confirm=True) and rclone's size check work
    and never touch the original.
  • Uploaders may rename and set times on files they created with only the
    write permission in any of their connections (rclone uploads on one
    connection and moves the file on another), for an hour after their last
    change and only while nobody else changed the file.
  • statvfs@openssh.com (df in sftp) on Linux, macOS and FreeBSD; mounts
    the user cannot change are reported read-only.
  • Listings show a virtual owner (uid/gid 1000, the user's own name in
    ls -l) instead of host accounts.
  • symlinks = "deny" refuses paths through existing symlinks and hides them
    from listings; the default "inside-only" follows links that stay inside
    the mount.
  • [audit] events selects categories (conn, auth, session,
    transfer, modify, denied, opt-in list and stat; server is
    always on) and on_error = "fail-open" keeps serving when the audit log
    cannot be written. The format is pinned by a schema test.
  • Release binaries for Linux and macOS (amd64, arm64) with checksums.txt,
    built by GoReleaser when a release is published.
  • Interop tests with OpenSSH 10.6p1, paramiko 5.0.0, rclone v1.75.0 and lftp
    in CI.
  • Documentation: docs/quickstart.md, configuration.md (every key),
    audit-log.md, security.md, interop.md, release-checklist.md.

Changed

  • New files are created with mode 0666 &^ umask and directories with
    0777 &^ umask; the default umask is 0027 (was a fixed 0644/0755).
  • serve without --dir now reads a configuration file instead of failing.
    --authorized-keys, --user and --state-dir only apply with --dir.

v0.1.0-alpha

v0.1.0-alpha Pre-release
Pre-release

Choose a tag to compare

@o-kolomoiets o-kolomoiets released this 08 Oct 11:43
533795c

First alpha: gosftpd serve works with OpenSSH sftp and scp.

  • public-key auth only; authorized_keys with from= / expiry-time=
  • every mount confined with os.Root (no escapes via .., absolute paths, symlinks)
  • uploads never silently overwrite: --on-conflict rename|reject|overwrite
  • JSON audit log, fail-closed (no uploads or new connections if it can't be written)
  • shell, exec and port forwarding refused

Install: go install github.com/o-kolomoiets/go-sftp-server/cmd/gosftpd@v0.1.0-alpha
Not yet: config file, multiple users, resumable uploads (planned for v0.2).