mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-27 02:23:23 +00:00
* sftpd: support SSH user certificates signed by a trusted CA Adds a new "certificate" auth method to weed sftp. When enabled, the server loads trusted CA public keys from -trustedUserCAKeysFile (OpenSSH authorized_keys format, one or more keys) and accepts only ssh.Certificate blobs of type UserCert on the public-key channel. Validation uses ssh.CertChecker: CA signature, ValidAfter/ValidBefore, non-empty ValidPrincipals and SSH login user must appear in ValidPrincipals. The authenticated user must exist in the user store; home dir and permissions resolve as before. Behaviour mirrors MinIO's --sftp=trusted-user-ca-key and OpenSSH's TrustedUserCAKeys: when certificate auth is active, plain (non-cert) public keys are rejected even if "publickey" is also listed. Default authMethods remain "password,publickey", so existing deployments are unaffected. * Update weed/sftpd/auth/certificate.go Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * sftpd: address review feedback on certificate auth - Pre-marshal trusted CA public keys in IsUserAuthority instead of re-marshaling on every authentication attempt (gemini-code-assist). - Differentiate user-not-found from underlying store errors via errors.As(*user.UserNotFoundError) so backend/read failures are no longer reported as bad credentials (coderabbitai). - Fix the corresponding sanity check in the missing-file test to use errors.As instead of errors.Is (UserNotFoundError has no Is method, so the previous check never matched) (coderabbitai). * sftpd: register trustedUserCAKeysFile flag in filer and server commands The new field on SftpOptions is dereferenced unconditionally in resolvePaths(), but only the standalone `weed sftp` command was wiring its flag. `weed filer` and `weed server` both embed an SftpOptions value and call resolvePaths() on it, so they hit a nil pointer dereference at startup. Register `-sftp.trustedUserCAKeysFile` in both commands and update the -sftp.authMethods help text to mention the new "certificate" method. Fixes the SFTP Integration Tests CI failure on this PR. * helm: expose SFTP certificate auth in the SeaweedFS chart Adds Helm-chart support for the new SSH user-certificate auth method: - values.yaml (sftp:) gains `trustedUserCAKeys` (inline OpenSSH authorized_keys-format CA public keys) and `existingCAKeysSecret` (reference an externally managed Secret). Same pair added under allInOne.sftp with a null default that falls back to the top-level sftp.* setting. - New template templates/sftp/sftp-ca-secret.yaml renders a chart-managed Secret <release>-sftp-ca-secret with `ca_user.pub`, but only when SFTP is enabled, "certificate" is in authMethods, inline keys are provided, and no existingCAKeysSecret is set. - templates/sftp/sftp-deployment.yaml and the all-in-one deployment template add `-trustedUserCAKeysFile=/etc/sw/sftp_ca/ca_user.pub` to the weed sftp command, mount the CA secret at /etc/sw/sftp_ca and add the corresponding volume. All cert-auth bits are guarded by `contains "certificate" authMethods` so existing users see no change. - authMethods help text updated to mention "certificate". Verified end-to-end on a local k3d cluster: cert login succeeds, plain-pubkey login is rejected with "public key without certificate not allowed". * helm: fail render when SFTP certificate auth lacks CA keys When certificate is in authMethods but neither trustedUserCAKeys nor existingCAKeysSecret is set, the deployment mounted a secret that the chart never renders, leaving the pod stuck on a missing volume. Fail at template time with a clear message instead. * sftpd: fix stale auth-method list in SFTPServiceOptions comment keyboard-interactive was never implemented; certificate is the new supported method. Match the CLI help text. * sftpd: test Manager wiring of certificate vs public-key channel Cover the channel takeover at the Manager level: certificate auth displaces plain public-key auth when both are enabled, public-key auth stays put otherwise, and enabling certificate without a CA file errors. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
80 lines
2.7 KiB
Go
80 lines
2.7 KiB
Go
package auth
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// When both "publickey" and "certificate" are enabled, certificate auth takes
|
|
// over the public-key channel: plain keys are rejected, certs are accepted, and
|
|
// password auth stays independently wired.
|
|
func TestManager_CertificateTakesOverPublicKeyChannel(t *testing.T) {
|
|
env := newTestEnv(t)
|
|
store := newStubStore("alice")
|
|
|
|
mgr, err := NewManager(store, []string{"password", "publickey", "certificate"}, env.caKeyFile)
|
|
if err != nil {
|
|
t.Fatalf("NewManager: %v", err)
|
|
}
|
|
|
|
cfg := mgr.GetSSHServerConfig()
|
|
if cfg.PasswordCallback == nil {
|
|
t.Fatal("expected password callback to stay wired")
|
|
}
|
|
if cfg.PublicKeyCallback == nil {
|
|
t.Fatal("expected public-key callback")
|
|
}
|
|
|
|
// Plain public key is rejected by the certificate authenticator.
|
|
if _, err := cfg.PublicKeyCallback(&fakeConnMetadata{user: "alice"}, env.userSigner.PublicKey()); err == nil {
|
|
t.Fatal("expected plain public key to be rejected")
|
|
} else if !strings.Contains(err.Error(), "public key without certificate") {
|
|
t.Fatalf("expected certificate-channel rejection, got %v", err)
|
|
}
|
|
|
|
// A valid cert is accepted.
|
|
cert := env.signCert(t, ssh.UserCert, []string{"alice"},
|
|
time.Now().Add(-time.Minute), time.Now().Add(time.Hour))
|
|
perms, err := cfg.PublicKeyCallback(&fakeConnMetadata{user: "alice"}, cert)
|
|
if err != nil {
|
|
t.Fatalf("expected cert to be accepted: %v", err)
|
|
}
|
|
if perms.Extensions["username"] != "alice" {
|
|
t.Fatalf("expected username extension alice, got %q", perms.Extensions["username"])
|
|
}
|
|
}
|
|
|
|
// Without "certificate", the public-key channel keeps plain public-key auth.
|
|
func TestManager_PublicKeyOnlyChannel(t *testing.T) {
|
|
mgr, err := NewManager(newStubStore("alice"), []string{"publickey"}, "")
|
|
if err != nil {
|
|
t.Fatalf("NewManager: %v", err)
|
|
}
|
|
|
|
cfg := mgr.GetSSHServerConfig()
|
|
if cfg.PublicKeyCallback == nil {
|
|
t.Fatal("expected public-key callback")
|
|
}
|
|
|
|
env := newTestEnv(t)
|
|
// Routes to plain public-key auth (stub store has no registered key), which
|
|
// fails with a different error than the certificate channel would.
|
|
_, err = cfg.PublicKeyCallback(&fakeConnMetadata{user: "alice"}, env.userSigner.PublicKey())
|
|
if err == nil {
|
|
t.Fatal("expected authentication failure")
|
|
}
|
|
if strings.Contains(err.Error(), "without certificate") {
|
|
t.Fatalf("certificate auth should not be wired, got %v", err)
|
|
}
|
|
}
|
|
|
|
// Enabling certificate auth without a CA keys file is a hard configuration error.
|
|
func TestManager_CertificateRequiresCAFile(t *testing.T) {
|
|
if _, err := NewManager(newStubStore(), []string{"certificate"}, ""); err == nil {
|
|
t.Fatal("expected error when certificate auth enabled without CA keys file")
|
|
}
|
|
}
|