8be6add7bc
OpenVPN now requires BOTH a valid client certificate AND a
username/password to establish a VPN connection.
Architecture:
Client .ovpn has 'auth-user-pass' → prompts for credentials
Server.conf has 'auth-user-pass-verify verify.sh via-env'
verify.sh (bash+curl) calls POST /api/vpn/verify on the manager
Manager verifies bcrypt hash via Go's golang.org/x/crypto/bcrypt
Endpoint is localhost-only (127.0.0.1) for security
Model changes:
Instance: new AuthMode field ('cert' | 'cert+password', default cert+password)
VPNUser: new PasswordHash (bcrypt) + Password (plaintext, transient)
Backend:
model: AuthMode type, VPNUser.PasswordHash, VPNUser.Password (transient)
store: ListUsers clears PasswordHash before returning
service: CreateUser hashes password with bcrypt, enforces min 4 chars
service: ResetVPNPassword for admin password reset
service: UpdateUser preserves PasswordHash from old record
service: CreateInstance defaults AuthMode=cert+password
api: POST /api/vpn/verify (no JWT, localhost-only, bcrypt verify)
api: POST /instances/:id/users/:uid/password (admin reset VPN pwd)
openvpn: WriteVerifyScript generates bash+curl verify script
openvpn: WriteServerConf adds script-security/auth-user-pass-verify
openvpn: GenerateClientOVPN adds auth-user-pass directive
Frontend:
Users.vue: password field on create form
Users.vue: '重置密码' button in table + dialog
Users.vue: '证书+密码' tag in auth column
api: Inst.resetVPNPassword() method
Security:
/api/vpn/verify rejects non-127.0.0.1 clients (403)
PasswordHash never exposed via any API response
verify.sh uses localhost curl (no external dependencies)
bcrypt cost=10 (same as admin passwords)
Verified: correct pwd → 200, wrong pwd → 401, missing user → 401,
non-localhost → 403, .ovpn has auth-user-pass, server.conf has
script-security 2 + auth-user-pass-verify + verify-client-cert require.
89 lines
1.9 KiB
Go
89 lines
1.9 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"openvpn-manager/internal/api"
|
|
"openvpn-manager/internal/config"
|
|
"openvpn-manager/internal/service"
|
|
"openvpn-manager/internal/store"
|
|
"openvpn-manager/pkg/openvpn"
|
|
)
|
|
|
|
func main() {
|
|
dataDir := flag.String("data", "", "data directory (default $OVPNMGR_DATA or ./data)")
|
|
distDir := flag.String("dist", "", "frontend dist directory (default $OVPNMGR_DIST or ./dist)")
|
|
flag.Parse()
|
|
|
|
if *dataDir == "" {
|
|
*dataDir = os.Getenv("OVPNMGR_DATA")
|
|
}
|
|
if *dataDir == "" {
|
|
*dataDir = "./data"
|
|
}
|
|
if *distDir == "" {
|
|
*distDir = os.Getenv("OVPNMGR_DIST")
|
|
}
|
|
if *distDir == "" {
|
|
*distDir = "./dist"
|
|
}
|
|
abs, _ := filepath.Abs(*dataDir)
|
|
cfg := config.Load(abs)
|
|
|
|
if err := os.MkdirAll(cfg.PKIDir(), 0o700); err != nil {
|
|
log.Fatalf("mkdir pki: %v", err)
|
|
}
|
|
if err := os.MkdirAll(cfg.InstancesDir(), 0o755); err != nil {
|
|
log.Fatalf("mkdir instances: %v", err)
|
|
}
|
|
if err := os.MkdirAll(cfg.ClientsDir(), 0o755); err != nil {
|
|
log.Fatalf("mkdir clients: %v", err)
|
|
}
|
|
if err := os.MkdirAll(cfg.BackupsDir(), 0o755); err != nil {
|
|
log.Fatalf("mkdir backups: %v", err)
|
|
}
|
|
|
|
st, err := store.Open(cfg.DBFile())
|
|
if err != nil {
|
|
log.Fatalf("open store: %v", err)
|
|
}
|
|
ovm := openvpn.NewManager(cfg.OpenVPNBin, cfg.DataDir)
|
|
ovm.SetPort(cfg.Port) // 让 verify.sh 知道回调哪个端口
|
|
if err := ovm.EnsureCA(); err != nil {
|
|
log.Printf("warn: ensure CA: %v", err)
|
|
}
|
|
svc := service.New(cfg, st, ovm)
|
|
|
|
addr := cfg.Host + ":" + itoa(cfg.Port)
|
|
log.Printf("openvpn-manager listening on %s, data=%s, dist=%s", addr, cfg.DataDir, *distDir)
|
|
srv := api.NewServer(cfg, svc)
|
|
if err := srv.Router(*distDir).Run(addr); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
neg := false
|
|
if n < 0 {
|
|
neg = true
|
|
n = -n
|
|
}
|
|
var buf [20]byte
|
|
i := len(buf)
|
|
for n > 0 {
|
|
i--
|
|
buf[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
if neg {
|
|
i--
|
|
buf[i] = '-'
|
|
}
|
|
return string(buf[i:])
|
|
} |