diff --git a/main.go b/main.go index 3e2ecaa..893bfac 100644 --- a/main.go +++ b/main.go @@ -2182,38 +2182,46 @@ func matchTOTPPassword(u *UserState, supplied string, now time.Time) bool { func passwordCallback(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { supplied := string(pass) u, ok := userMgr.Get(meta.User()) - if !ok { - // Unknown username. If system (PAM) login is enabled for this server, - // verify the password against the Linux account and auto-import the - // user on success so it appears in the panel and future logins are - // tracked normally. - if isPAMAuthEnabled() { - return pamLoginAndImport(meta.User(), supplied) + now := time.Now() + + // Enforce panel policy (expiry / reseller owner) for known users up front, + // so neither PAM nor the static password can bypass it. + if ok { + if u.ExpiresAt != nil && now.After(*u.ExpiresAt) { + log.Printf("user %s tried to connect but account is expired", meta.User()) + return nil, fmt.Errorf("account expired") } + if err := ownerIsActive(u.Cfg.OwnerUsername); err != nil { + return nil, fmt.Errorf("authentication failed: %w", err) + } + } + + // System (PAM) login. When enabled server-wide, the Linux system password + // (/etc/shadow) is accepted for any regular account (UID >= 1000) — whether + // or not it is already a panel user. Unknown accounts are auto-imported on + // success. Falls through to panel credentials if PAM does not accept. + if isPAMAuthEnabled() { + if isRegularLoginUser(meta.User()) { + if err := authenticatePAM(meta.User(), supplied); err == nil { + if !ok { + importPAMUser(meta.User()) + } + log.Printf("PAM: %q authenticated against /etc/shadow", meta.User()) + return nil, nil + } else { + log.Printf("PAM: %q rejected by /etc/shadow: %v", meta.User(), err) + } + } else if !ok { + log.Printf("PAM: %q is not a regular login account (needs an /etc/passwd entry with UID >= %d)", meta.User(), minLoginUID) + } + } + + if !ok { + log.Printf("auth: user %q rejected (no panel account and PAM did not accept it)", meta.User()) return nil, fmt.Errorf("authentication failed") } - now := time.Now() - if u.ExpiresAt != nil && now.After(*u.ExpiresAt) { - log.Printf("user %s tried to connect but account is expired", meta.User()) - return nil, fmt.Errorf("account expired") - } - if err := ownerIsActive(u.Cfg.OwnerUsername); err != nil { - return nil, fmt.Errorf("authentication failed: %w", err) - } - - // PAM-imported user: verify against the Linux system password each time. - if u.Cfg.UsePAM { - if !isPAMAuthEnabled() { - // System login was disabled server-wide; refuse PAM accounts. - return nil, fmt.Errorf("authentication failed") - } - if err := authenticatePAM(meta.User(), supplied); err != nil { - log.Printf("PAM auth failed for user %s: %v", meta.User(), err) - return nil, fmt.Errorf("authentication failed") - } - return nil, nil - } + // Fall back to panel-managed credentials (TOTP and/or static password). if strings.TrimSpace(u.Cfg.TOTPSecret) != "" { if matchTOTPPassword(u, supplied, now) { return nil, nil diff --git a/pam_auth.go b/pam_auth.go index 7524c9e..20cb041 100644 --- a/pam_auth.go +++ b/pam_auth.go @@ -18,7 +18,6 @@ import ( _ "github.com/GehirnInc/crypt/sha512_crypt" "github.com/openwall/yescrypt-go" "golang.org/x/crypto/bcrypt" - "golang.org/x/crypto/ssh" ) const ( @@ -41,23 +40,6 @@ var pamAuthEnabled atomic.Bool func setPAMAuthEnabled(v bool) { pamAuthEnabled.Store(v) } func isPAMAuthEnabled() bool { return pamAuthEnabled.Load() } -// pamLoginAndImport authenticates an unknown SSH username against the Linux -// system password and, on success, auto-imports it as a panel user. It is only -// called when server-wide PAM login is enabled. Returns nil permissions on -// success (matching the panel's other auth callbacks). -func pamLoginAndImport(username, password string) (*ssh.Permissions, error) { - if !isRegularLoginUser(username) { - // Not a regular human account (system/service account, root, or absent). - return nil, fmt.Errorf("authentication failed") - } - if err := authenticatePAM(username, password); err != nil { - log.Printf("PAM login failed for %s: %v", username, err) - return nil, fmt.Errorf("authentication failed") - } - importPAMUser(username) - return nil, nil -} - // importPAMUser registers a freshly PAM-authenticated account in the running // user manager and persists it (marked use_pam) so it shows up in the panel and // later logins are re-verified against the system password. Idempotent: a