Pam Fix 2.0
This commit is contained in:
@@ -331,7 +331,6 @@ const fTotpPeriod = document.getElementById("fTotpPeriod");
|
|||||||
const fTotpWindow = document.getElementById("fTotpWindow");
|
const fTotpWindow = document.getElementById("fTotpWindow");
|
||||||
const fTotpDigits = document.getElementById("fTotpDigits");
|
const fTotpDigits = document.getElementById("fTotpDigits");
|
||||||
const fAllowStatic = document.getElementById("fAllowStatic");
|
const fAllowStatic = document.getElementById("fAllowStatic");
|
||||||
const fUsePam = document.getElementById("fUsePam");
|
|
||||||
const fMaxConn = document.getElementById("fMaxConn");
|
const fMaxConn = document.getElementById("fMaxConn");
|
||||||
const fExpires = document.getElementById("fExpires");
|
const fExpires = document.getElementById("fExpires");
|
||||||
const fUp = document.getElementById("fUp");
|
const fUp = document.getElementById("fUp");
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ function fillUserForm(u) {
|
|||||||
fTotpWindow.value = u.totp_window ?? 1;
|
fTotpWindow.value = u.totp_window ?? 1;
|
||||||
fTotpDigits.value = u.totp_digits || 6;
|
fTotpDigits.value = u.totp_digits || 6;
|
||||||
fAllowStatic.checked = !!u.allow_static_password;
|
fAllowStatic.checked = !!u.allow_static_password;
|
||||||
if (fUsePam) fUsePam.checked = !!u.use_pam;
|
|
||||||
fMaxConn.value = u.max_connections || "";
|
fMaxConn.value = u.max_connections || "";
|
||||||
fUp.value = u.limit_mbps_up || "";
|
fUp.value = u.limit_mbps_up || "";
|
||||||
fDown.value = u.limit_mbps_down || "";
|
fDown.value = u.limit_mbps_down || "";
|
||||||
@@ -145,7 +144,6 @@ userForm.addEventListener("submit", async e => {
|
|||||||
totp_window: parseInt(fTotpWindow.value||"1",10),
|
totp_window: parseInt(fTotpWindow.value||"1",10),
|
||||||
totp_digits: parseInt(fTotpDigits.value||"6",10),
|
totp_digits: parseInt(fTotpDigits.value||"6",10),
|
||||||
allow_static_password: !!fAllowStatic.checked,
|
allow_static_password: !!fAllowStatic.checked,
|
||||||
use_pam: !!(fUsePam && fUsePam.checked),
|
|
||||||
max_connections: parseInt(fMaxConn.value||"0",10),
|
max_connections: parseInt(fMaxConn.value||"0",10),
|
||||||
expires_at: isoFromLocal(fExpires.value),
|
expires_at: isoFromLocal(fExpires.value),
|
||||||
limit_mbps_up: parseInt(fUp.value||"0",10),
|
limit_mbps_up: parseInt(fUp.value||"0",10),
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ async function loadManagedServerConfig(id) {
|
|||||||
document.getElementById("managedCfgSSHIdleTimeout").value = c.ssh_idle_timeout || "0s";
|
document.getElementById("managedCfgSSHIdleTimeout").value = c.ssh_idle_timeout || "0s";
|
||||||
document.getElementById("managedCfgQuiet").checked = !!c.quiet;
|
document.getElementById("managedCfgQuiet").checked = !!c.quiet;
|
||||||
document.getElementById("managedCfgUserCount").checked = !!c.user_count;
|
document.getElementById("managedCfgUserCount").checked = !!c.user_count;
|
||||||
|
document.getElementById("managedCfgPamAuth").checked = !!c.pam_auth_enabled;
|
||||||
document.getElementById("managedCfgBanner").value = c.banner || "";
|
document.getElementById("managedCfgBanner").value = c.banner || "";
|
||||||
|
|
||||||
const hasDnstt = !!c.dnstt;
|
const hasDnstt = !!c.dnstt;
|
||||||
@@ -487,6 +488,7 @@ function managedConfigFromForm() {
|
|||||||
ssh_idle_timeout: document.getElementById("managedCfgSSHIdleTimeout").value.trim() || "0s",
|
ssh_idle_timeout: document.getElementById("managedCfgSSHIdleTimeout").value.trim() || "0s",
|
||||||
quiet: document.getElementById("managedCfgQuiet").checked,
|
quiet: document.getElementById("managedCfgQuiet").checked,
|
||||||
user_count: document.getElementById("managedCfgUserCount").checked,
|
user_count: document.getElementById("managedCfgUserCount").checked,
|
||||||
|
pam_auth_enabled: document.getElementById("managedCfgPamAuth").checked,
|
||||||
banner: document.getElementById("managedCfgBanner").value,
|
banner: document.getElementById("managedCfgBanner").value,
|
||||||
banner_file: "/opt/sshpanel/banner.txt",
|
banner_file: "/opt/sshpanel/banner.txt",
|
||||||
dnstt: document.getElementById("managedCfgDnsttEnabled").checked ? {
|
dnstt: document.getElementById("managedCfgDnsttEnabled").checked ? {
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ async function loadServerConfig() {
|
|||||||
document.getElementById("cfgSSHIdleTimeout").value = c.ssh_idle_timeout || "0s";
|
document.getElementById("cfgSSHIdleTimeout").value = c.ssh_idle_timeout || "0s";
|
||||||
document.getElementById("cfgQuiet").checked = !!c.quiet;
|
document.getElementById("cfgQuiet").checked = !!c.quiet;
|
||||||
document.getElementById("cfgUserCount").checked = !!c.user_count;
|
document.getElementById("cfgUserCount").checked = !!c.user_count;
|
||||||
|
document.getElementById("cfgPamAuth").checked = !!c.pam_auth_enabled;
|
||||||
|
|
||||||
// Banner
|
// Banner
|
||||||
document.getElementById("cfgBanner").value = c.banner || "";
|
document.getElementById("cfgBanner").value = c.banner || "";
|
||||||
@@ -182,6 +183,7 @@ async function saveServerConfig() {
|
|||||||
ssh_idle_timeout: document.getElementById("cfgSSHIdleTimeout").value.trim() || "0s",
|
ssh_idle_timeout: document.getElementById("cfgSSHIdleTimeout").value.trim() || "0s",
|
||||||
quiet: document.getElementById("cfgQuiet").checked,
|
quiet: document.getElementById("cfgQuiet").checked,
|
||||||
user_count: document.getElementById("cfgUserCount").checked,
|
user_count: document.getElementById("cfgUserCount").checked,
|
||||||
|
pam_auth_enabled: document.getElementById("cfgPamAuth").checked,
|
||||||
banner: document.getElementById("cfgBanner").value,
|
banner: document.getElementById("cfgBanner").value,
|
||||||
banner_file: "/opt/sshpanel/banner.txt",
|
banner_file: "/opt/sshpanel/banner.txt",
|
||||||
dnstt: document.getElementById("cfgDnsttEnabled").checked ? {
|
dnstt: document.getElementById("cfgDnsttEnabled").checked ? {
|
||||||
|
|||||||
+4
-1
@@ -306,7 +306,6 @@
|
|||||||
<div class="field"><label>TOTP Window</label><input id="fTotpWindow" type="number" min="0" placeholder="1"/></div>
|
<div class="field"><label>TOTP Window</label><input id="fTotpWindow" type="number" min="0" placeholder="1"/></div>
|
||||||
<div class="field"><label>TOTP Digits</label><input id="fTotpDigits" type="number" min="6" max="8" placeholder="6"/></div>
|
<div class="field"><label>TOTP Digits</label><input id="fTotpDigits" type="number" min="6" max="8" placeholder="6"/></div>
|
||||||
<div class="field"><label>Allow static password too</label><input id="fAllowStatic" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
<div class="field"><label>Allow static password too</label><input id="fAllowStatic" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
||||||
<div class="field"><label>Use Linux PAM auth (legacy)</label><input id="fUsePam" type="checkbox" style="width:16px;height:16px;margin-top:10px;"/></div>
|
|
||||||
<div class="field"><label>Max connections</label><input id="fMaxConn" type="number" min="0" placeholder="0 = unlimited"/></div>
|
<div class="field"><label>Max connections</label><input id="fMaxConn" type="number" min="0" placeholder="0 = unlimited"/></div>
|
||||||
<div class="field"><label>Expires at</label><input id="fExpires" type="datetime-local"/></div>
|
<div class="field"><label>Expires at</label><input id="fExpires" type="datetime-local"/></div>
|
||||||
<div class="field"><label>Max Upload (Mb/s)</label><input id="fUp" type="number" min="0" placeholder="0 = default"/></div>
|
<div class="field"><label>Max Upload (Mb/s)</label><input id="fUp" type="number" min="0" placeholder="0 = default"/></div>
|
||||||
@@ -808,6 +807,7 @@
|
|||||||
<div class="field"><label>SSH Idle Timeout <span class="hint">0s/off = disabled</span></label><input type="text" id="managedCfgSSHIdleTimeout" placeholder="0s" title="Keep disabled for VPN/XHTTP connections."/></div>
|
<div class="field"><label>SSH Idle Timeout <span class="hint">0s/off = disabled</span></label><input type="text" id="managedCfgSSHIdleTimeout" placeholder="0s" title="Keep disabled for VPN/XHTTP connections."/></div>
|
||||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgQuiet"/> Quiet Logs</label>
|
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgQuiet"/> Quiet Logs</label>
|
||||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgUserCount"/> User Count Display</label>
|
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgUserCount"/> User Count Display</label>
|
||||||
|
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;" title="Verify SSH logins against the Linux system password (/etc/shadow); regular accounts (UID ≥ 1000) are auto-imported."><input type="checkbox" id="managedCfgPamAuth"/> Linux PAM Login (auto-import)</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1269,6 +1269,9 @@
|
|||||||
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;">
|
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;">
|
||||||
<input type="checkbox" id="cfgUserCount"/> User Count Display
|
<input type="checkbox" id="cfgUserCount"/> User Count Display
|
||||||
</label>
|
</label>
|
||||||
|
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;" title="Verify SSH logins against the Linux system password (/etc/shadow). Regular accounts (UID ≥ 1000) that log in successfully are auto-imported into the panel.">
|
||||||
|
<input type="checkbox" id="cfgPamAuth"/> Linux PAM Login (auto-import)
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ func applyFullConfigReload(newCfg *Config) ConfigReloadReport {
|
|||||||
setDefaultLimits(newCfg.DefaultLimitMbpsUp, newCfg.DefaultLimitMbpsDown)
|
setDefaultLimits(newCfg.DefaultLimitMbpsUp, newCfg.DefaultLimitMbpsDown)
|
||||||
setSSHIdleTimeoutFromConfig(newCfg.SSHIdleTimeout)
|
setSSHIdleTimeoutFromConfig(newCfg.SSHIdleTimeout)
|
||||||
setMaxTotalConnsFromConfig(newCfg.MaxTotalConnections)
|
setMaxTotalConnsFromConfig(newCfg.MaxTotalConnections)
|
||||||
|
setPAMAuthEnabled(newCfg.PAMAuthEnabled)
|
||||||
|
|
||||||
// Quiet logging / user count display
|
// Quiet logging / user count display
|
||||||
if newCfg.Quiet {
|
if newCfg.Quiet {
|
||||||
|
|||||||
@@ -97,6 +97,13 @@ type Config struct {
|
|||||||
|
|
||||||
UserCount bool `json:"user_count"`
|
UserCount bool `json:"user_count"`
|
||||||
|
|
||||||
|
// PAMAuthEnabled turns on Linux system-password login for this server. When
|
||||||
|
// true, an SSH login with a username not present in the panel is verified
|
||||||
|
// against /etc/shadow; on success the account (regular users, UID >= 1000)
|
||||||
|
// is auto-imported into the panel. When false, only panel-managed accounts
|
||||||
|
// can log in and previously-imported PAM accounts are refused.
|
||||||
|
PAMAuthEnabled bool `json:"pam_auth_enabled"`
|
||||||
|
|
||||||
// SSHIdleTimeout controls how long an authenticated SSH connection may
|
// SSHIdleTimeout controls how long an authenticated SSH connection may
|
||||||
// remain with no bytes moving in either direction before it is closed and
|
// remain with no bytes moving in either direction before it is closed and
|
||||||
// released from the active user count. Empty, "0", or "0s" disables it.
|
// released from the active user count. Empty, "0", or "0s" disables it.
|
||||||
@@ -390,6 +397,19 @@ func (m *UserManager) Get(username string) (*UserState, bool) {
|
|||||||
return u, ok
|
return u, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddIfAbsent inserts u only if no user with the same username exists yet, and
|
||||||
|
// reports whether it was added. Used by PAM auto-import to register a freshly
|
||||||
|
// authenticated system account without clobbering an existing runtime state.
|
||||||
|
func (m *UserManager) AddIfAbsent(u *UserState) bool {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if _, exists := m.users[u.Cfg.Username]; exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
m.users[u.Cfg.Username] = u
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (m *UserManager) List() []*UserState {
|
func (m *UserManager) List() []*UserState {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
@@ -2160,8 +2180,16 @@ func matchTOTPPassword(u *UserState, supplied string, now time.Time) bool {
|
|||||||
// ---------- Auth callbacks ----------
|
// ---------- Auth callbacks ----------
|
||||||
|
|
||||||
func passwordCallback(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
func passwordCallback(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) {
|
||||||
|
supplied := string(pass)
|
||||||
u, ok := userMgr.Get(meta.User())
|
u, ok := userMgr.Get(meta.User())
|
||||||
if !ok {
|
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)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("authentication failed")
|
return nil, fmt.Errorf("authentication failed")
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -2172,13 +2200,11 @@ func passwordCallback(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, err
|
|||||||
if err := ownerIsActive(u.Cfg.OwnerUsername); err != nil {
|
if err := ownerIsActive(u.Cfg.OwnerUsername); err != nil {
|
||||||
return nil, fmt.Errorf("authentication failed: %w", err)
|
return nil, fmt.Errorf("authentication failed: %w", err)
|
||||||
}
|
}
|
||||||
supplied := string(pass)
|
|
||||||
|
|
||||||
// Legacy PAM mode: authenticate against the Linux system account via PAM's
|
// PAM-imported user: verify against the Linux system password each time.
|
||||||
// auth phase only. This bypasses the panel-managed password/TOTP entirely.
|
|
||||||
if u.Cfg.UsePAM {
|
if u.Cfg.UsePAM {
|
||||||
if !pamAuthAvailable {
|
if !isPAMAuthEnabled() {
|
||||||
log.Printf("user %s is configured for PAM auth but this build has no PAM support", meta.User())
|
// System login was disabled server-wide; refuse PAM accounts.
|
||||||
return nil, fmt.Errorf("authentication failed")
|
return nil, fmt.Errorf("authentication failed")
|
||||||
}
|
}
|
||||||
if err := authenticatePAM(meta.User(), supplied); err != nil {
|
if err := authenticatePAM(meta.User(), supplied); err != nil {
|
||||||
@@ -3237,6 +3263,7 @@ func main() {
|
|||||||
setDefaultLimits(cfg.DefaultLimitMbpsUp, cfg.DefaultLimitMbpsDown)
|
setDefaultLimits(cfg.DefaultLimitMbpsUp, cfg.DefaultLimitMbpsDown)
|
||||||
setSSHIdleTimeoutFromConfig(cfg.SSHIdleTimeout)
|
setSSHIdleTimeoutFromConfig(cfg.SSHIdleTimeout)
|
||||||
setMaxTotalConnsFromConfig(cfg.MaxTotalConnections)
|
setMaxTotalConnsFromConfig(cfg.MaxTotalConnections)
|
||||||
|
setPAMAuthEnabled(cfg.PAMAuthEnabled)
|
||||||
|
|
||||||
// Initialise listener pools (used for initial startup and hot-reload alike).
|
// Initialise listener pools (used for initial startup and hot-reload alike).
|
||||||
publicPool = newListenerPool(serveHTTP80)
|
publicPool = newListenerPool(serveHTTP80)
|
||||||
|
|||||||
+81
-1
@@ -1,11 +1,15 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/GehirnInc/crypt"
|
"github.com/GehirnInc/crypt"
|
||||||
_ "github.com/GehirnInc/crypt/apr1_crypt"
|
_ "github.com/GehirnInc/crypt/apr1_crypt"
|
||||||
@@ -14,6 +18,7 @@ import (
|
|||||||
_ "github.com/GehirnInc/crypt/sha512_crypt"
|
_ "github.com/GehirnInc/crypt/sha512_crypt"
|
||||||
"github.com/openwall/yescrypt-go"
|
"github.com/openwall/yescrypt-go"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pamAuthAvailable reports that system-password ("PAM") auth is compiled in.
|
// 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.
|
// on non-Linux builds /etc/shadow simply does not exist and auth fails closed.
|
||||||
const pamAuthAvailable = true
|
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")
|
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
|
// authenticatePAM verifies password against the Linux system account matching
|
||||||
// username. It reads the account's hash from /etc/shadow (the panel runs as
|
// 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"
|
// root) and recomputes it with the same algorithm — this is the "just the auth"
|
||||||
|
|||||||
Reference in New Issue
Block a user