fix: SSHAuthenticator vide après installation + logs debug

Bug principal : l'SSHAuthenticator est créé au démarrage avec host=""
(DB vide avant installation). Après configure, il gardait host vide.
Le login lisait maintenant le ssh_host depuis la DB à chaque requête.

Logs ajoutés :
- ssh_auth.go : dial SSH, succès, échec avec détail d'erreur
- auth.go : host SSH utilisé, résultat auth à chaque login
- updates.go : credentials SSH, démarrage/fin de job
- terminal.go : ouverture/échec session SSH

Frontend :
- auth.store.ts : gère les réponses non-JSON sur erreur HTTP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
enzo 2026-03-20 23:39:52 +01:00
parent 15965082ce
commit 07af66ad81
5 changed files with 46 additions and 7 deletions

View file

@ -5,6 +5,7 @@ import (
"crypto/sha256" "crypto/sha256"
"database/sql" "database/sql"
"encoding/hex" "encoding/hex"
"log"
"net/http" "net/http"
"time" "time"
@ -60,13 +61,27 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
return return
} }
// Authentification PAM via SSH // Lire le host SSH depuis la DB à chaque login.
userInfo, err := h.sshAuth.Authenticate(body.Username, body.Password) // L'authenticator global est créé au démarrage avec host="" (avant installation)
// et ne se met pas à jour automatiquement après configuration.
sshHost, _, _ := h.db.GetSetting("ssh_host")
if sshHost == "" {
log.Printf("[auth/login] SSH non configuré (ssh_host vide en base) — user=%s ip=%s", body.Username, ip)
JSONError(w, "SSH non configuré, veuillez vérifier l'installation", http.StatusServiceUnavailable)
return
}
log.Printf("[auth/login] Tentative — user=%s ip=%s ssh_host=%s", body.Username, ip, sshHost)
authenticator := auth.NewSSHAuthenticator(sshHost)
userInfo, err := authenticator.Authenticate(body.Username, body.Password)
if err != nil { if err != nil {
log.Printf("[auth/login] Échec auth SSH — user=%s ssh_host=%s erreur=%v", body.Username, sshHost, err)
h.auditLogger.Log(nil, body.Username, "login_failed", "", map[string]string{"error": err.Error()}, ip) h.auditLogger.Log(nil, body.Username, "login_failed", "", map[string]string{"error": err.Error()}, ip)
JSONError(w, "Identifiants invalides", http.StatusUnauthorized) JSONError(w, "Identifiants invalides", http.StatusUnauthorized)
return return
} }
log.Printf("[auth/login] Succès — user=%s admin=%v ssh_host=%s", body.Username, userInfo.IsAdmin, sshHost)
// Créer ou mettre à jour le profil utilisateur en SQLite // Créer ou mettre à jour le profil utilisateur en SQLite
userID, err := h.upsertUser(userInfo) userID, err := h.upsertUser(userInfo)

View file

@ -5,6 +5,7 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"net/http" "net/http"
"time" "time"
@ -57,10 +58,12 @@ func (h *TerminalHandler) WebSocket(w http.ResponseWriter, r *http.Request) {
sshPass, _ := h.encryptor.Decrypt(encryptedPass) sshPass, _ := h.encryptor.Decrypt(encryptedPass)
if sshHost == "" { if sshHost == "" {
log.Printf("[terminal] SSH non configuré — user=%s", claims.Username)
conn.WriteMessage(gorillaws.TextMessage, []byte("\r\nErreur : SSH non configuré\r\n")) conn.WriteMessage(gorillaws.TextMessage, []byte("\r\nErreur : SSH non configuré\r\n"))
return return
} }
log.Printf("[terminal] Ouverture session — user=%s ssh_host=%s", claims.Username, sshHost)
h.auditLogger.Log(&claims.UserID, claims.Username, "terminal_open", sshHost, nil, clientIP(r)) h.auditLogger.Log(&claims.UserID, claims.Username, "terminal_open", sshHost, nil, clientIP(r))
// Établir la connexion SSH // Établir la connexion SSH
@ -75,9 +78,11 @@ func (h *TerminalHandler) WebSocket(w http.ResponseWriter, r *http.Request) {
sshClient, err := gossh.Dial("tcp", sshHost, sshConfig) sshClient, err := gossh.Dial("tcp", sshHost, sshConfig)
if err != nil { if err != nil {
log.Printf("[terminal] Échec connexion SSH %s@%s : %v", sshUser, sshHost, err)
conn.WriteMessage(gorillaws.TextMessage, []byte(fmt.Sprintf("\r\nErreur SSH : %v\r\n", err))) conn.WriteMessage(gorillaws.TextMessage, []byte(fmt.Sprintf("\r\nErreur SSH : %v\r\n", err)))
return return
} }
log.Printf("[terminal] Connecté — %s@%s", sshUser, sshHost)
defer sshClient.Close() defer sshClient.Close()
// Créer une session SSH avec pseudo-terminal // Créer une session SSH avec pseudo-terminal

View file

@ -5,6 +5,8 @@ package api
import ( import (
"fmt" "fmt"
"log"
"math/rand"
"net/http" "net/http"
"time" "time"
@ -14,7 +16,6 @@ import (
"git.geronzi.fr/proxmoxPanel/core/backend/internal/ssh" "git.geronzi.fr/proxmoxPanel/core/backend/internal/ssh"
"git.geronzi.fr/proxmoxPanel/core/backend/internal/websocket" "git.geronzi.fr/proxmoxPanel/core/backend/internal/websocket"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"math/rand"
) )
// UpdatesHandler contient les handlers de mises à jour. // UpdatesHandler contient les handlers de mises à jour.
@ -56,7 +57,9 @@ func (h *UpdatesHandler) RunUpdate(w http.ResponseWriter, r *http.Request) {
sshUser, _, _ := h.db.GetSetting("ssh_username") sshUser, _, _ := h.db.GetSetting("ssh_username")
encryptedPass, _, _ := h.db.GetSetting("ssh_password") encryptedPass, _, _ := h.db.GetSetting("ssh_password")
sshPass, _ := h.encryptor.Decrypt(encryptedPass) sshPass, _ := h.encryptor.Decrypt(encryptedPass)
log.Printf("[updates/run] Credentials SSH — host=%s user=%s password_len=%d", sshHost, sshUser, len(sshPass))
if sshHost == "" || sshUser == "" || sshPass == "" { if sshHost == "" || sshUser == "" || sshPass == "" {
log.Printf("[updates/run] SSH non configuré — host=%q user=%q password_empty=%v", sshHost, sshUser, sshPass == "")
JSONError(w, "SSH non configuré", http.StatusServiceUnavailable) JSONError(w, "SSH non configuré", http.StatusServiceUnavailable)
return return
} }
@ -71,7 +74,7 @@ func (h *UpdatesHandler) RunUpdate(w http.ResponseWriter, r *http.Request) {
h.auditLogger.Log(&claims.UserID, claims.Username, "update_start", body.Target, nil, clientIP(r)) h.auditLogger.Log(&claims.UserID, claims.Username, "update_start", body.Target, nil, clientIP(r))
// Lancer la mise à jour en arrière-plan log.Printf("[updates/run] Job %s démarré — target=%s user=%d", jobID, body.Target, claims.UserID)
go h.executeUpdate(jobID, body.Target, sshHost, sshUser, sshPass, claims.UserID) go h.executeUpdate(jobID, body.Target, sshHost, sshUser, sshPass, claims.UserID)
JSONResponse(w, http.StatusAccepted, map[string]string{ JSONResponse(w, http.StatusAccepted, map[string]string{
@ -164,8 +167,14 @@ func (h *UpdatesHandler) executeUpdate(jobID, target, sshHost, sshUser, sshPass
} }
// Lancer le streaming SSH // Lancer le streaming SSH
cmdPreview := command
if len(cmdPreview) > 80 {
cmdPreview = cmdPreview[:80] + "..."
}
log.Printf("[updates/execute] Job %s — SSH %s@%s commande: %s", jobID, sshUser, sshHost, cmdPreview)
err := h.sshPool.StreamCommand(sshHost, sshUser, sshPass, command, outputChan) err := h.sshPool.StreamCommand(sshHost, sshUser, sshPass, command, outputChan)
if err != nil { if err != nil {
log.Printf("[updates/execute] Job %s — Erreur SSH : %v", jobID, err)
h.db.Exec(`UPDATE update_history SET status='error', output=?, finished_at=CURRENT_TIMESTAMP WHERE job_id=?`, h.db.Exec(`UPDATE update_history SET status='error', output=?, finished_at=CURRENT_TIMESTAMP WHERE job_id=?`,
"Erreur SSH : "+err.Error(), jobID) "Erreur SSH : "+err.Error(), jobID)
h.hub.Publish("update:"+jobID, "update_error", map[string]string{"error": err.Error()}) h.hub.Publish("update:"+jobID, "update_error", map[string]string{"error": err.Error()})
@ -180,6 +189,7 @@ func (h *UpdatesHandler) executeUpdate(jobID, target, sshHost, sshUser, sshPass
} }
// Finaliser le job // Finaliser le job
log.Printf("[updates/execute] Job %s — terminé (%d octets de sortie)", jobID, len(fullOutput))
h.db.Exec(`UPDATE update_history SET status='success', output=?, finished_at=CURRENT_TIMESTAMP WHERE job_id=?`, h.db.Exec(`UPDATE update_history SET status='success', output=?, finished_at=CURRENT_TIMESTAMP WHERE job_id=?`,
fullOutput, jobID) fullOutput, jobID)
h.hub.Publish("update:"+jobID, "update_done", map[string]string{"job_id": jobID}) h.hub.Publish("update:"+jobID, "update_done", map[string]string{"job_id": jobID})

View file

@ -6,6 +6,7 @@ package auth
import ( import (
"fmt" "fmt"
"log"
"net" "net"
"strings" "strings"
"time" "time"
@ -45,8 +46,10 @@ func (a *SSHAuthenticator) Authenticate(username, password string) (*UserInfo, e
} }
// Tentative de connexion SSH // Tentative de connexion SSH
log.Printf("[ssh_auth] Dial %s@%s...", username, a.host)
client, err := ssh.Dial("tcp", a.host, config) client, err := ssh.Dial("tcp", a.host, config)
if err != nil { if err != nil {
log.Printf("[ssh_auth] Échec dial %s@%s : %v", username, a.host, err)
// Distinguer les erreurs d'authentification des erreurs réseau // Distinguer les erreurs d'authentification des erreurs réseau
if strings.Contains(err.Error(), "unable to authenticate") || if strings.Contains(err.Error(), "unable to authenticate") ||
strings.Contains(err.Error(), "ssh: handshake failed") || strings.Contains(err.Error(), "ssh: handshake failed") ||
@ -56,13 +59,15 @@ func (a *SSHAuthenticator) Authenticate(username, password string) (*UserInfo, e
return nil, fmt.Errorf("connexion SSH impossible : %w", err) return nil, fmt.Errorf("connexion SSH impossible : %w", err)
} }
defer client.Close() defer client.Close()
log.Printf("[ssh_auth] Connexion établie — %s@%s", username, a.host)
// Vérifier l'appartenance aux groupes sudo/wheel via la commande `id` // Vérifier l'appartenance aux groupes sudo/wheel via la commande `id`
isAdmin, err := checkSudoGroup(client) isAdmin, err := checkSudoGroup(client)
if err != nil { if err != nil {
// En cas d'erreur de vérification des groupes, l'utilisateur est authentifié mais pas admin log.Printf("[ssh_auth] Avertissement vérification sudo — user=%s : %v", username, err)
isAdmin = false isAdmin = false
} }
log.Printf("[ssh_auth] Authentifié — user=%s admin=%v", username, isAdmin)
return &UserInfo{ return &UserInfo{
Username: username, Username: username,

View file

@ -53,9 +53,13 @@ export const useAuthStore = defineStore('auth', () => {
}) })
if (!res.ok) { if (!res.ok) {
const contentType = res.headers.get('content-type') || ''
if (contentType.includes('application/json')) {
const err = await res.json() const err = await res.json()
throw new Error(err.error || 'Erreur d\'authentification') throw new Error(err.error || 'Erreur d\'authentification')
} }
throw new Error(`Erreur ${res.status} — réponse inattendue du serveur`)
}
const data = await res.json() const data = await res.json()
accessToken.value = data.access_token accessToken.value = data.access_token