core/backend/internal/auth/ssh_auth.go
enzo 5dbcb1df07 feat: initialisation complète du CORE ProxmoxPanel
Backend Go 1.23+ :
- API REST + WebSocket (chi, gorilla/websocket)
- Authentification PAM via SSH + JWT RS256
- Chiffrement AES-256-GCM pour secrets SQLite
- Pool SSH, client Proxmox REST, hub WebSocket pub/sub
- Système de modules compilés à initialisation conditionnelle
- Audit log, migrations SQLite versionnées

Frontend Vue 3 + Vite + TypeScript :
- Thème Neumorphism sombre/clair (CSS custom properties)
- Wizard d'installation, Dashboard drag-drop, Terminal xterm.js
- Toutes les vues CORE + stubs modules optionnels
- i18n EN/FR (vue-i18n v11)

Infrastructure :
- Docker multi-stage (Go → alpine, Node → nginx)
- docker-compose.yml, .gitattributes, LICENSE MIT, README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 21:08:53 +01:00

129 lines
3.9 KiB
Go

// Package auth — Authentification PAM via SSH.
// Au lieu de monter les fichiers système du host (/etc/shadow), on tente une connexion
// SSH avec les credentials de l'utilisateur. Si elle réussit, les credentials sont valides.
// L'appartenance au groupe sudo/wheel détermine le niveau admin.
package auth
import (
"fmt"
"net"
"strings"
"time"
"golang.org/x/crypto/ssh"
)
// UserInfo contient les informations d'un utilisateur authentifié.
type UserInfo struct {
Username string
IsAdmin bool
}
// SSHAuthenticator gère l'authentification des utilisateurs via SSH vers le host Proxmox.
type SSHAuthenticator struct {
host string // ex: "10.0.0.1:2244"
}
// NewSSHAuthenticator crée un authentificateur SSH pour le host donné.
func NewSSHAuthenticator(host string) *SSHAuthenticator {
return &SSHAuthenticator{host: host}
}
// Authenticate tente une connexion SSH avec les credentials fournis.
// Si la connexion réussit, retourne les informations de l'utilisateur.
// Vérifie l'appartenance au groupe sudo ou wheel pour déterminer le niveau admin.
func (a *SSHAuthenticator) Authenticate(username, password string) (*UserInfo, error) {
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
// Timeout court pour l'authentification
Timeout: 10 * time.Second,
// Accepter n'importe quelle clé host (le host est sur le réseau interne de confiance)
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
// Tentative de connexion SSH
client, err := ssh.Dial("tcp", a.host, config)
if err != nil {
// Distinguer les erreurs d'authentification des erreurs réseau
if strings.Contains(err.Error(), "unable to authenticate") ||
strings.Contains(err.Error(), "ssh: handshake failed") ||
strings.Contains(err.Error(), "no supported methods remain") {
return nil, fmt.Errorf("identifiants invalides")
}
return nil, fmt.Errorf("connexion SSH impossible : %w", err)
}
defer client.Close()
// Vérifier l'appartenance aux groupes sudo/wheel via la commande `id`
isAdmin, err := checkSudoGroup(client)
if err != nil {
// En cas d'erreur de vérification des groupes, l'utilisateur est authentifié mais pas admin
isAdmin = false
}
return &UserInfo{
Username: username,
IsAdmin: isAdmin,
}, nil
}
// TestConnectivity teste la connexion SSH sans authentification complète.
// Utilisé pendant l'installation pour valider les paramètres de connexion.
func TestConnectivity(host string, timeout time.Duration) error {
conn, err := net.DialTimeout("tcp", host, timeout)
if err != nil {
return fmt.Errorf("impossible de joindre %s : %w", host, err)
}
conn.Close()
return nil
}
// TestSSHAuth teste une connexion SSH complète avec credentials.
// Retourne nil si la connexion réussit, une erreur explicite sinon.
func TestSSHAuth(host, username, password string) error {
config := &ssh.ClientConfig{
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
Timeout: 10 * time.Second,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, err := ssh.Dial("tcp", host, config)
if err != nil {
if strings.Contains(err.Error(), "unable to authenticate") {
return fmt.Errorf("identifiants SSH invalides")
}
return fmt.Errorf("connexion SSH échouée : %w", err)
}
client.Close()
return nil
}
// checkSudoGroup exécute `id -nG` sur la session SSH et vérifie la présence
// des groupes "sudo" ou "wheel" dans la liste des groupes de l'utilisateur.
func checkSudoGroup(client *ssh.Client) (bool, error) {
session, err := client.NewSession()
if err != nil {
return false, fmt.Errorf("ouverture session SSH : %w", err)
}
defer session.Close()
output, err := session.Output("id -nG")
if err != nil {
return false, fmt.Errorf("exécution `id -nG` : %w", err)
}
groups := strings.Fields(strings.TrimSpace(string(output)))
for _, g := range groups {
if g == "sudo" || g == "wheel" {
return true, nil
}
}
return false, nil
}