Support of PAM

This commit is contained in:
2026-07-13 23:17:05 -03:00
parent dab8b09f0c
commit 2ff7976768
9 changed files with 133 additions and 14 deletions
+1
View File
@@ -331,6 +331,7 @@ 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");
+3 -1
View File
@@ -73,7 +73,7 @@ function renderUsers(users) {
const cells = [ const cells = [
u.username, u.username,
on ? `<span class="badge-on">${t("online")}</span>` : `<span class="badge-off">${t("idle")}</span>`, on ? `<span class="badge-on">${t("online")}</span>` : `<span class="badge-off">${t("idle")}</span>`,
u.totp_enabled ? (u.allow_static_password ? "TOTP+pw" : "TOTP") : "Password", u.use_pam ? "PAM" : (u.totp_enabled ? (u.allow_static_password ? "TOTP+pw" : "TOTP") : "Password"),
u.active_conns ?? 0, u.active_conns ?? 0,
u.max_connections || 0, u.max_connections || 0,
u.limit_mbps_up || 0, u.limit_mbps_up || 0,
@@ -121,6 +121,7 @@ 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 || "";
@@ -144,6 +145,7 @@ 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),
+1
View File
@@ -306,6 +306,7 @@
<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>
+1
View File
@@ -4,6 +4,7 @@ go 1.25.4
require ( require (
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/msteinert/pam/v2 v2.1.0
github.com/xtaci/kcp-go/v5 v5.6.61 github.com/xtaci/kcp-go/v5 v5.6.61
github.com/xtaci/smux v1.5.50 github.com/xtaci/smux v1.5.50
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.45.0
+2
View File
@@ -36,6 +36,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/msteinert/pam/v2 v2.1.0 h1:er5F9TKV5nGFuTt12ubtqPHEUdeBwReP7vd3wovidGY=
github.com/msteinert/pam/v2 v2.1.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+6 -5
View File
@@ -61,23 +61,24 @@ detect_pkg_manager() {
set_package_deps() { set_package_deps() {
case "$PKG_MANAGER" in case "$PKG_MANAGER" in
apt) apt)
PKG_DEPS=(curl wget git rsync build-essential postgresql ca-certificates unzip openssh-client openssl python3 tar gzip) # libpam0g-dev: PAM headers for the optional PAM auth build (cgo).
PKG_DEPS=(curl wget git rsync build-essential libpam0g-dev postgresql ca-certificates unzip openssh-client openssl python3 tar gzip)
PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables)
;; ;;
dnf|yum) dnf|yum)
PKG_DEPS=(curl wget git rsync gcc make postgresql-server ca-certificates unzip openssh-clients openssl python3 tar gzip) PKG_DEPS=(curl wget git rsync gcc make pam-devel postgresql-server ca-certificates unzip openssh-clients openssl python3 tar gzip)
PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables)
;; ;;
zypper) zypper)
PKG_DEPS=(curl wget git rsync gcc make postgresql-server ca-certificates unzip openssh openssl python3 tar gzip) PKG_DEPS=(curl wget git rsync gcc make pam-devel postgresql-server ca-certificates unzip openssh openssl python3 tar gzip)
PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables)
;; ;;
pacman) pacman)
PKG_DEPS=(curl wget git rsync base-devel postgresql ca-certificates unzip openssh openssl python tar gzip) PKG_DEPS=(curl wget git rsync base-devel pam postgresql ca-certificates unzip openssh openssl python tar gzip)
PKG_OPTIONAL_DEPS=(iptables-nft nftables) PKG_OPTIONAL_DEPS=(iptables-nft nftables)
;; ;;
apk) apk)
PKG_DEPS=(curl wget git rsync build-base postgresql ca-certificates unzip openssh-client openssl python3 tar gzip) PKG_DEPS=(curl wget git rsync build-base linux-pam-dev postgresql ca-certificates unzip openssh-client openssl python3 tar gzip)
PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables)
;; ;;
esac esac
+40 -8
View File
@@ -351,6 +351,12 @@ type UserConfig struct {
// When false and totp_secret is set, only the TOTP code is accepted. // When false and totp_secret is set, only the TOTP code is accepted.
AllowStaticPassword bool `json:"allow_static_password"` AllowStaticPassword bool `json:"allow_static_password"`
// UsePAM is a legacy opt-in: when true, the supplied SSH password is
// verified against the Linux PAM auth stack (auth phase only) for the
// system account matching this username, instead of the panel-managed
// Password/TOTP. New users leave this false and keep the script's own auth.
UsePAM bool `json:"use_pam"`
MaxConnections int `json:"max_connections"` MaxConnections int `json:"max_connections"`
ExpiresAt string `json:"expires_at"` // RFC3339 or empty ExpiresAt string `json:"expires_at"` // RFC3339 or empty
@@ -1351,13 +1357,15 @@ func (s *Store) EnsureUsersSchema(ctx context.Context) error {
totp_period INT NOT NULL DEFAULT 60, totp_period INT NOT NULL DEFAULT 60,
totp_window INT NOT NULL DEFAULT 1, totp_window INT NOT NULL DEFAULT 1,
totp_digits INT NOT NULL DEFAULT 6, totp_digits INT NOT NULL DEFAULT 6,
allow_static_password BOOLEAN NOT NULL DEFAULT FALSE allow_static_password BOOLEAN NOT NULL DEFAULT FALSE,
use_pam BOOLEAN NOT NULL DEFAULT FALSE
)`, )`,
`ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS totp_secret TEXT NOT NULL DEFAULT ''`, `ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS totp_secret TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS totp_period INT NOT NULL DEFAULT 60`, `ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS totp_period INT NOT NULL DEFAULT 60`,
`ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS totp_window INT NOT NULL DEFAULT 1`, `ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS totp_window INT NOT NULL DEFAULT 1`,
`ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS totp_digits INT NOT NULL DEFAULT 6`, `ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS totp_digits INT NOT NULL DEFAULT 6`,
`ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS allow_static_password BOOLEAN NOT NULL DEFAULT FALSE`, `ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS allow_static_password BOOLEAN NOT NULL DEFAULT FALSE`,
`ALTER TABLE ssh_users ADD COLUMN IF NOT EXISTS use_pam BOOLEAN NOT NULL DEFAULT FALSE`,
`ALTER TABLE ssh_users ALTER COLUMN password SET DEFAULT ''`, `ALTER TABLE ssh_users ALTER COLUMN password SET DEFAULT ''`,
} }
for _, stmt := range stmts { for _, stmt := range stmts {
@@ -1407,7 +1415,7 @@ func (s *Store) LoadUsers(ctx context.Context) (map[string]*UserState, error) {
SELECT username, password, max_connections, expires_at, limit_mbps_up, limit_mbps_down, SELECT username, password, max_connections, expires_at, limit_mbps_up, limit_mbps_down,
COALESCE(totp_secret, ''), COALESCE(totp_period, 60), COALESCE(totp_window, 1), COALESCE(totp_secret, ''), COALESCE(totp_period, 60), COALESCE(totp_window, 1),
COALESCE(totp_digits, 6), COALESCE(allow_static_password, FALSE), COALESCE(totp_digits, 6), COALESCE(allow_static_password, FALSE),
COALESCE(owner_username, '') COALESCE(use_pam, FALSE), COALESCE(owner_username, '')
FROM ssh_users`) FROM ssh_users`)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1428,10 +1436,11 @@ func (s *Store) LoadUsers(ctx context.Context) (map[string]*UserState, error) {
totpWindow int totpWindow int
totpDigits int totpDigits int
allowStaticPassword bool allowStaticPassword bool
usePAM bool
ownerUsername string ownerUsername string
) )
if err := rows.Scan(&username, &password, &maxConnections, &expiresAt, &limitUp, &limitDown, if err := rows.Scan(&username, &password, &maxConnections, &expiresAt, &limitUp, &limitDown,
&totpSecret, &totpPeriod, &totpWindow, &totpDigits, &allowStaticPassword, &ownerUsername); err != nil { &totpSecret, &totpPeriod, &totpWindow, &totpDigits, &allowStaticPassword, &usePAM, &ownerUsername); err != nil {
return nil, err return nil, err
} }
password, err = openSSHPassword(password) password, err = openSSHPassword(password)
@@ -1450,6 +1459,7 @@ func (s *Store) LoadUsers(ctx context.Context) (map[string]*UserState, error) {
TOTPWindow: totpWindow, TOTPWindow: totpWindow,
TOTPDigits: totpDigits, TOTPDigits: totpDigits,
AllowStaticPassword: allowStaticPassword, AllowStaticPassword: allowStaticPassword,
UsePAM: usePAM,
OwnerUsername: ownerUsername, OwnerUsername: ownerUsername,
} }
@@ -1480,9 +1490,9 @@ func (s *Store) UpsertUser(ctx context.Context, u UserConfig) error {
_, err = s.db.ExecContext(ctx, ` _, err = s.db.ExecContext(ctx, `
INSERT INTO ssh_users ( INSERT INTO ssh_users (
username, password, max_connections, expires_at, limit_mbps_up, limit_mbps_down, username, password, max_connections, expires_at, limit_mbps_up, limit_mbps_down,
totp_secret, totp_period, totp_window, totp_digits, allow_static_password, owner_username totp_secret, totp_period, totp_window, totp_digits, allow_static_password, use_pam, owner_username
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
ON CONFLICT (username) DO UPDATE ON CONFLICT (username) DO UPDATE
SET password = EXCLUDED.password, SET password = EXCLUDED.password,
max_connections = EXCLUDED.max_connections, max_connections = EXCLUDED.max_connections,
@@ -1493,10 +1503,11 @@ func (s *Store) UpsertUser(ctx context.Context, u UserConfig) error {
totp_period = EXCLUDED.totp_period, totp_period = EXCLUDED.totp_period,
totp_window = EXCLUDED.totp_window, totp_window = EXCLUDED.totp_window,
totp_digits = EXCLUDED.totp_digits, totp_digits = EXCLUDED.totp_digits,
allow_static_password = EXCLUDED.allow_static_password`, allow_static_password = EXCLUDED.allow_static_password,
use_pam = EXCLUDED.use_pam`,
// owner_username is intentionally excluded from UPDATE — ownership is set at creation only. // owner_username is intentionally excluded from UPDATE — ownership is set at creation only.
u.Username, storedPassword, u.MaxConnections, u.ExpiresAt, u.LimitMbpsUp, u.LimitMbpsDown, u.Username, storedPassword, u.MaxConnections, u.ExpiresAt, u.LimitMbpsUp, u.LimitMbpsDown,
u.TOTPSecret, u.TOTPPeriod, u.TOTPWindow, u.TOTPDigits, u.AllowStaticPassword, u.OwnerUsername) u.TOTPSecret, u.TOTPPeriod, u.TOTPWindow, u.TOTPDigits, u.AllowStaticPassword, u.UsePAM, u.OwnerUsername)
return err return err
} }
@@ -1713,6 +1724,7 @@ type UserDTO struct {
TOTPWindow int `json:"totp_window"` TOTPWindow int `json:"totp_window"`
TOTPDigits int `json:"totp_digits"` TOTPDigits int `json:"totp_digits"`
AllowStaticPassword bool `json:"allow_static_password"` AllowStaticPassword bool `json:"allow_static_password"`
UsePAM bool `json:"use_pam"`
TOTPEnabled bool `json:"totp_enabled"` TOTPEnabled bool `json:"totp_enabled"`
OwnerUsername string `json:"owner_username,omitempty"` OwnerUsername string `json:"owner_username,omitempty"`
ServerID string `json:"server_id,omitempty"` ServerID string `json:"server_id,omitempty"`
@@ -1759,6 +1771,7 @@ func handleListUsers(w http.ResponseWriter, r *http.Request) {
TOTPWindow: cfg.TOTPWindow, TOTPWindow: cfg.TOTPWindow,
TOTPDigits: cfg.TOTPDigits, TOTPDigits: cfg.TOTPDigits,
AllowStaticPassword: cfg.AllowStaticPassword, AllowStaticPassword: cfg.AllowStaticPassword,
UsePAM: cfg.UsePAM,
TOTPEnabled: strings.TrimSpace(cfg.TOTPSecret) != "", TOTPEnabled: strings.TrimSpace(cfg.TOTPSecret) != "",
OwnerUsername: cfg.OwnerUsername, OwnerUsername: cfg.OwnerUsername,
}) })
@@ -1781,6 +1794,7 @@ type UserPayload struct {
TOTPWindow int `json:"totp_window"` TOTPWindow int `json:"totp_window"`
TOTPDigits int `json:"totp_digits"` TOTPDigits int `json:"totp_digits"`
AllowStaticPassword bool `json:"allow_static_password"` AllowStaticPassword bool `json:"allow_static_password"`
UsePAM bool `json:"use_pam"`
OwnerUsername string `json:"owner_username,omitempty"` OwnerUsername string `json:"owner_username,omitempty"`
ServerID string `json:"server_id,omitempty"` ServerID string `json:"server_id,omitempty"`
} }
@@ -1882,7 +1896,9 @@ func handleCreateUser(store *Store) http.HandlerFunc {
).Scan(&existing) ).Scan(&existing)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
if strings.TrimSpace(p.TOTPSecret) == "" { // PAM users authenticate against the system account, so they
// need neither a panel password nor a TOTP secret.
if strings.TrimSpace(p.TOTPSecret) == "" && !p.UsePAM {
http.Error(w, "password or totp_secret required for new user", http.StatusBadRequest) http.Error(w, "password or totp_secret required for new user", http.StatusBadRequest)
return return
} }
@@ -1933,6 +1949,7 @@ func handleCreateUser(store *Store) http.HandlerFunc {
TOTPWindow: p.TOTPWindow, TOTPWindow: p.TOTPWindow,
TOTPDigits: p.TOTPDigits, TOTPDigits: p.TOTPDigits,
AllowStaticPassword: p.AllowStaticPassword, AllowStaticPassword: p.AllowStaticPassword,
UsePAM: p.UsePAM,
OwnerUsername: ownerUsername, OwnerUsername: ownerUsername,
} }
@@ -2156,6 +2173,21 @@ func passwordCallback(meta ssh.ConnMetadata, pass []byte) (*ssh.Permissions, err
return nil, fmt.Errorf("authentication failed: %w", err) return nil, fmt.Errorf("authentication failed: %w", err)
} }
supplied := string(pass) supplied := string(pass)
// Legacy PAM mode: authenticate against the Linux system account via PAM's
// auth phase only. This bypasses the panel-managed password/TOTP entirely.
if u.Cfg.UsePAM {
if !pamAuthAvailable {
log.Printf("user %s is configured for PAM auth but this build has no PAM support", meta.User())
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
}
if strings.TrimSpace(u.Cfg.TOTPSecret) != "" { if strings.TrimSpace(u.Cfg.TOTPSecret) != "" {
if matchTOTPPassword(u, supplied, now) { if matchTOTPPassword(u, supplied, now) {
return nil, nil return nil, nil
+63
View File
@@ -0,0 +1,63 @@
//go:build linux && cgo
package main
import (
"errors"
"fmt"
"os"
"strings"
"github.com/msteinert/pam/v2"
)
// pamAuthAvailable reports that PAM authentication is compiled into this build.
const pamAuthAvailable = true
// pamServiceName returns the PAM service to use for the auth-only check. This
// is the name of a file under /etc/pam.d. Override with SSHPANEL_PAM_SERVICE;
// it defaults to "login", which exists on every mainstream distro and runs the
// standard unix auth stack (common-auth / system-auth).
func pamServiceName() string {
if s := strings.TrimSpace(os.Getenv("SSHPANEL_PAM_SERVICE")); s != "" {
return s
}
return "login"
}
// authenticatePAM verifies password against the Linux PAM stack for username.
//
// It runs ONLY the auth phase (pam_authenticate) — no account management
// (pam_acct_mgmt), no session, no credential setup, and nothing to do with the
// system SSH daemon. This is the "just the auth" behaviour: the supplied
// password is checked against the system account exactly as PAM's auth modules
// would, and nothing else. Returns nil on success, an error on failure.
//
// The panel runs as root, so pam_unix can read /etc/shadow to verify the hash
// (including yescrypt/sha512crypt) for any local account.
func authenticatePAM(username, password string) error {
if username == "" {
return errors.New("pam: empty username")
}
t, err := pam.StartFunc(pamServiceName(), username, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff, pam.PromptEchoOn:
return password, nil
case pam.ErrorMsg, pam.TextInfo:
// Informational messages from modules; nothing to return.
return "", nil
default:
return "", fmt.Errorf("pam: unsupported conversation style %v", s)
}
})
if err != nil {
return fmt.Errorf("pam start: %w", err)
}
defer func() { _ = t.End() }()
// Silent keeps modules from writing to stdout/syslog noise; auth phase only.
if err := t.Authenticate(pam.Silent); err != nil {
return fmt.Errorf("pam auth: %w", err)
}
return nil
}
+16
View File
@@ -0,0 +1,16 @@
//go:build !linux || !cgo
package main
import "errors"
// pamAuthAvailable is false when the binary is built without cgo/PAM support
// (for example the Windows development build, or any CGO_ENABLED=0 build).
// In that case a user flagged for PAM auth can never authenticate.
const pamAuthAvailable = false
func pamServiceName() string { return "" }
func authenticatePAM(username, password string) error {
return errors.New("pam authentication is not supported in this build")
}