Pam Fix 2.0
This commit is contained in:
+81
-1
@@ -1,11 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/GehirnInc/crypt"
|
||||
_ "github.com/GehirnInc/crypt/apr1_crypt"
|
||||
@@ -14,6 +18,7 @@ import (
|
||||
_ "github.com/GehirnInc/crypt/sha512_crypt"
|
||||
"github.com/openwall/yescrypt-go"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// pamAuthAvailable reports that system-password ("PAM") auth is compiled in.
|
||||
@@ -21,10 +26,85 @@ import (
|
||||
// on non-Linux builds /etc/shadow simply does not exist and auth fails closed.
|
||||
const pamAuthAvailable = true
|
||||
|
||||
const shadowFile = "/etc/shadow"
|
||||
const (
|
||||
shadowFile = "/etc/shadow"
|
||||
passwdFile = "/etc/passwd"
|
||||
|
||||
// minLoginUID / nobodyUID bound the accounts eligible for auto-import.
|
||||
// Regular human login accounts start at UID 1000 on Debian/Ubuntu; system
|
||||
// and service accounts (and "nobody") are excluded.
|
||||
minLoginUID = 1000
|
||||
nobodyUID = 65534
|
||||
)
|
||||
|
||||
var errNoSystemPassword = errors.New("account has no usable password")
|
||||
|
||||
// pamAuthEnabled mirrors Config.PAMAuthEnabled and is toggled live on config
|
||||
// reload. Guarded atomically so passwordCallback can read it lock-free.
|
||||
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
|
||||
// second concurrent/subsequent login for the same user is a no-op.
|
||||
func importPAMUser(username string) {
|
||||
cfg := UserConfig{Username: username, UsePAM: true}
|
||||
st := &UserState{Cfg: cfg}
|
||||
if !userMgr.AddIfAbsent(st) {
|
||||
return // already present in memory
|
||||
}
|
||||
log.Printf("PAM: auto-imported system user %s into the panel", username)
|
||||
if statsStore != nil {
|
||||
if err := statsStore.UpsertUser(context.Background(), cfg); err != nil {
|
||||
log.Printf("PAM: failed to persist auto-imported user %s: %v", username, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isRegularLoginUser reports whether username is a regular human login account
|
||||
// (UID >= 1000 and not "nobody"), by parsing /etc/passwd. System/service
|
||||
// accounts and root are excluded from auto-import.
|
||||
func isRegularLoginUser(username string) bool {
|
||||
data, err := os.ReadFile(passwdFile)
|
||||
if err != nil {
|
||||
log.Printf("PAM: cannot read %s: %v", passwdFile, err)
|
||||
return false
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
fields := strings.Split(line, ":")
|
||||
if len(fields) < 3 || fields[0] != username {
|
||||
continue
|
||||
}
|
||||
uid, err := strconv.Atoi(fields[2])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return uid >= minLoginUID && uid != nobodyUID
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// authenticatePAM verifies password against the Linux system account matching
|
||||
// username. It reads the account's hash from /etc/shadow (the panel runs as
|
||||
// root) and recomputes it with the same algorithm — this is the "just the auth"
|
||||
|
||||
Reference in New Issue
Block a user