Repository navigation
v0.3.0
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) andmax_preauth_connections(64). Refused
connections are audited asconn.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 eventauth.ban. server.idle_timeout(15m) closes connections without SFTP traffic;
server.keepalive_interval(30s) closes connections that leave 3
keepalive@openssh.comrequests unanswered.conn.closereports
idle_timeoutorkeepalive_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 validatewarns about mixed ones). Checks
run at most one per available CPU. A method missing fromauth.methodsis
refused, and its keys or passwords are not loaded.gosftpd user hash-password [--stdin]prints a hash, and
user add --password-hashadds it to a user. server.crypto_policy:modern(default, as before) orcompat, 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>, keepingversions.keep(10)
versions per file for at mostversions.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 thewritepermission.fs.uploadandfs.renameget
conflict = "versioned"andversion_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.fsyncflushes 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, andmin_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), auditscrypto_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 serverefuses 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.successhaskey_fponly 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.renamefor a posix-rename that replaced its target under
on_conflict = "overwrite"reportsconflict = "overwritten"instead of
"none".- Names starting with
.gosftpd-(in any case) are reserved: hidden from
listings and refused in paths. - BREAKING:
rename_templatemust contain{n}exactly once and be
valid UTF-8 of at most 64 bytes, so that copy names always fit 255 bytes;
config validatenames a template that does not.
Fixed
- A client that disconnected right after logging in left no
auth.success
andconn.closeevents. - A client that sent requests with a guessed handle while an OPEN was in
progress raced with the OPEN insidepkg/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:\) madeserveandconfig validatepanic instead of
reporting the error (found by fuzzing).