mirror of
https://github.com/MengMengCode/CLICD.git
synced 2026-08-08 14:34:49 +08:00
添加新建虚拟机/重装系统 预设SSH密码以及KEY Auth功能
This commit is contained in:
@@ -241,6 +241,9 @@ type ContainerConfig struct {
|
||||
AssignIPv6 bool `json:"assign_ipv6"`
|
||||
IPv6Count int `json:"ipv6_count,omitempty"`
|
||||
IPv6Addresses []string `json:"ipv6_addresses,omitempty"`
|
||||
SSHAuthMode string `json:"ssh_auth_mode,omitempty"`
|
||||
SSHPassword string `json:"ssh_password,omitempty"`
|
||||
SSHPublicKey string `json:"ssh_public_key,omitempty"`
|
||||
ExpiresAt string `json:"expires_at"`
|
||||
}
|
||||
|
||||
@@ -270,6 +273,10 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
if config.FindContainerByName(cfg.Name) != nil {
|
||||
return fmt.Errorf("container name already exists: %s", cfg.Name)
|
||||
}
|
||||
sshAccess, err := ResolveCreateSSHAccess(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Allocate ID and build LXC name
|
||||
id := config.AllocateContainerID()
|
||||
@@ -329,7 +336,7 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
sshPassword := generateRandomString(16)
|
||||
sshPassword := sshAccess.Password
|
||||
|
||||
sshPort := 0
|
||||
portMappings := []config.PortMapping{}
|
||||
@@ -420,6 +427,13 @@ func (m *Manager) CreateContainer(cfg ContainerConfig) error {
|
||||
if err := m.preconfigureSSH(rootfsPath, cfg.TemplateID); err != nil {
|
||||
fmt.Printf("Warning: failed to pre-configure SSH in %s: %v\n", lxcName, err)
|
||||
}
|
||||
if sshAccess.PublicKey != "" {
|
||||
if err := m.installRootAuthorizedKey(rootfsPath, sshAccess.PublicKey); err != nil {
|
||||
_ = m.cleanupContainerStorage(lxcName)
|
||||
config.RemoveContainer(id)
|
||||
return fmt.Errorf("failed to install SSH public key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.shiftRootfsForUnprivileged(lxcName); err != nil {
|
||||
_ = m.cleanupContainerStorage(lxcName)
|
||||
@@ -1931,6 +1945,7 @@ ssh-keygen -A >/dev/null 2>&1 || true
|
||||
|
||||
cat >/etc/ssh/sshd_config.d/99-clicd.conf <<'EOF'
|
||||
PermitRootLogin yes
|
||||
PubkeyAuthentication yes
|
||||
PasswordAuthentication yes
|
||||
KbdInteractiveAuthentication no
|
||||
ChallengeResponseAuthentication no
|
||||
@@ -1938,6 +1953,7 @@ UsePAM no
|
||||
EOF
|
||||
|
||||
set_sshd_option PermitRootLogin yes
|
||||
set_sshd_option PubkeyAuthentication yes
|
||||
set_sshd_option PasswordAuthentication yes
|
||||
set_sshd_option KbdInteractiveAuthentication no
|
||||
set_sshd_option ChallengeResponseAuthentication no
|
||||
@@ -2102,6 +2118,45 @@ func (m *Manager) setRootfsPassword(rootfsPath, password string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) installRootAuthorizedKey(rootfsPath, publicKey string) error {
|
||||
key, err := NormalizeSSHPublicKey(publicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
sshDir := filepath.Join(rootfsPath, "root", ".ssh")
|
||||
if err := os.MkdirAll(sshDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
authPath := filepath.Join(sshDir, "authorized_keys")
|
||||
existing, _ := os.ReadFile(authPath)
|
||||
lines := strings.Split(string(existing), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == key {
|
||||
_ = os.Chmod(sshDir, 0700)
|
||||
_ = os.Chmod(authPath, 0600)
|
||||
_ = os.Chown(sshDir, 0, 0)
|
||||
_ = os.Chown(authPath, 0, 0)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
content := strings.TrimRight(string(existing), "\r\n")
|
||||
if content != "" {
|
||||
content += "\n"
|
||||
}
|
||||
content += key + "\n"
|
||||
if err := os.WriteFile(authPath, []byte(content), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Chmod(sshDir, 0700)
|
||||
_ = os.Chmod(authPath, 0600)
|
||||
_ = os.Chown(sshDir, 0, 0)
|
||||
_ = os.Chown(authPath, 0, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func safeRootfsCommandArgs(args []string) ([]string, error) {
|
||||
if len(args) == 0 {
|
||||
return nil, fmt.Errorf("empty rootfs command")
|
||||
@@ -2499,7 +2554,7 @@ func copyRootfsContents(src, dst string) error {
|
||||
}
|
||||
|
||||
// ReinstallContainer reinstalls the container OS
|
||||
func (m *Manager) ReinstallContainer(id int, templateID string) error {
|
||||
func (m *Manager) ReinstallContainer(id int, templateID string, authConfig ...ContainerConfig) error {
|
||||
c := config.FindContainer(id)
|
||||
if c == nil {
|
||||
return fmt.Errorf("container not found: %d", id)
|
||||
@@ -2509,6 +2564,14 @@ func (m *Manager) ReinstallContainer(id int, templateID string) error {
|
||||
if tmpl == nil {
|
||||
return fmt.Errorf("template not found: %s", templateID)
|
||||
}
|
||||
authCfg := ContainerConfig{SSHAuthMode: SSHAuthKeep}
|
||||
if len(authConfig) > 0 {
|
||||
authCfg = authConfig[0]
|
||||
}
|
||||
sshAccess, err := ResolveReinstallSSHAccess(c.SSHPassword, authCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lxcName := c.LxcName()
|
||||
|
||||
@@ -2562,12 +2625,15 @@ func (m *Manager) ReinstallContainer(id int, templateID string) error {
|
||||
fmt.Printf("Warning: failed to install IPv6 init in %s after reinstall: %v\n", lxcName, err)
|
||||
}
|
||||
}
|
||||
if c.SSHPassword == "" {
|
||||
c.SSHPassword = generateRandomString(16)
|
||||
}
|
||||
c.SSHPassword = sshAccess.Password
|
||||
if err := m.preconfigureSSH(rootfsPath, templateID); err != nil {
|
||||
fmt.Printf("Warning: failed to pre-configure SSH in %s after reinstall: %v\n", lxcName, err)
|
||||
}
|
||||
if sshAccess.PublicKey != "" {
|
||||
if err := m.installRootAuthorizedKey(rootfsPath, sshAccess.PublicKey); err != nil {
|
||||
return fmt.Errorf("failed to install SSH public key: %v", err)
|
||||
}
|
||||
}
|
||||
if err := m.shiftRootfsForUnprivileged(lxcName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
package lxc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
SSHAuthAutoPassword = "auto_password"
|
||||
SSHAuthPassword = "password"
|
||||
SSHAuthKey = "key"
|
||||
SSHAuthKeep = "keep"
|
||||
)
|
||||
|
||||
type SSHAccess struct {
|
||||
Mode string
|
||||
Password string
|
||||
PublicKey string
|
||||
}
|
||||
|
||||
func HasSSHAuthOptions(cfg ContainerConfig) bool {
|
||||
return strings.TrimSpace(cfg.SSHAuthMode) != "" ||
|
||||
strings.TrimSpace(cfg.SSHPassword) != "" ||
|
||||
strings.TrimSpace(cfg.SSHPublicKey) != ""
|
||||
}
|
||||
|
||||
func ResolveCreateSSHAccess(cfg ContainerConfig) (SSHAccess, error) {
|
||||
mode, err := resolveSSHAuthMode(cfg.SSHAuthMode, cfg.SSHPassword, cfg.SSHPublicKey, SSHAuthAutoPassword)
|
||||
if err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
if mode == SSHAuthKeep {
|
||||
mode = SSHAuthAutoPassword
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case SSHAuthAutoPassword:
|
||||
return SSHAccess{Mode: mode, Password: generateRandomString(16)}, nil
|
||||
case SSHAuthPassword:
|
||||
password := strings.TrimSpace(cfg.SSHPassword)
|
||||
if password == "" {
|
||||
return SSHAccess{}, fmt.Errorf("请填写自定义 SSH 密码")
|
||||
}
|
||||
if err := ValidateCustomSSHPassword(password); err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
return SSHAccess{Mode: mode, Password: password}, nil
|
||||
case SSHAuthKey:
|
||||
publicKey, err := NormalizeSSHPublicKey(cfg.SSHPublicKey)
|
||||
if err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
if publicKey == "" {
|
||||
return SSHAccess{}, fmt.Errorf("请填写 SSH 公钥")
|
||||
}
|
||||
password := strings.TrimSpace(cfg.SSHPassword)
|
||||
if password == "" {
|
||||
password = generateRandomString(16)
|
||||
} else if err := ValidateCustomSSHPassword(password); err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
return SSHAccess{Mode: mode, Password: password, PublicKey: publicKey}, nil
|
||||
default:
|
||||
return SSHAccess{}, fmt.Errorf("不支持的 SSH 登录方式: %s", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveReinstallSSHAccess(currentPassword string, cfg ContainerConfig) (SSHAccess, error) {
|
||||
mode, err := resolveSSHAuthMode(cfg.SSHAuthMode, cfg.SSHPassword, cfg.SSHPublicKey, SSHAuthKeep)
|
||||
if err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case SSHAuthKeep:
|
||||
password := strings.TrimSpace(currentPassword)
|
||||
if password == "" {
|
||||
password = generateRandomString(16)
|
||||
}
|
||||
if err := validateRootPassword(password); err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
return SSHAccess{Mode: mode, Password: password}, nil
|
||||
case SSHAuthAutoPassword:
|
||||
return SSHAccess{Mode: mode, Password: generateRandomString(16)}, nil
|
||||
case SSHAuthPassword:
|
||||
password := strings.TrimSpace(cfg.SSHPassword)
|
||||
if password == "" {
|
||||
return SSHAccess{}, fmt.Errorf("请填写自定义 SSH 密码")
|
||||
}
|
||||
if err := ValidateCustomSSHPassword(password); err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
return SSHAccess{Mode: mode, Password: password}, nil
|
||||
case SSHAuthKey:
|
||||
publicKey, err := NormalizeSSHPublicKey(cfg.SSHPublicKey)
|
||||
if err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
if publicKey == "" {
|
||||
return SSHAccess{}, fmt.Errorf("请填写 SSH 公钥")
|
||||
}
|
||||
password := strings.TrimSpace(cfg.SSHPassword)
|
||||
if password != "" {
|
||||
if err := ValidateCustomSSHPassword(password); err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
} else {
|
||||
password = strings.TrimSpace(currentPassword)
|
||||
if password == "" {
|
||||
password = generateRandomString(16)
|
||||
}
|
||||
}
|
||||
if err := validateRootPassword(password); err != nil {
|
||||
return SSHAccess{}, err
|
||||
}
|
||||
return SSHAccess{Mode: mode, Password: password, PublicKey: publicKey}, nil
|
||||
default:
|
||||
return SSHAccess{}, fmt.Errorf("不支持的 SSH 登录方式: %s", mode)
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateCustomSSHPassword(password string) error {
|
||||
if len(password) < 8 || len(password) > 64 {
|
||||
return fmt.Errorf("密码长度必须为 8-64 位")
|
||||
}
|
||||
hasLetter := false
|
||||
hasDigit := false
|
||||
for _, r := range password {
|
||||
if unicode.IsSpace(r) {
|
||||
return fmt.Errorf("密码不能包含空白字符")
|
||||
}
|
||||
if unicode.IsLetter(r) {
|
||||
hasLetter = true
|
||||
}
|
||||
if unicode.IsDigit(r) {
|
||||
hasDigit = true
|
||||
}
|
||||
}
|
||||
if !hasLetter || !hasDigit {
|
||||
return fmt.Errorf("密码至少需要包含字母和数字")
|
||||
}
|
||||
return validateRootPassword(password)
|
||||
}
|
||||
|
||||
func NormalizeSSHPublicKey(publicKey string) (string, error) {
|
||||
key := strings.TrimSpace(publicKey)
|
||||
if key == "" {
|
||||
return "", nil
|
||||
}
|
||||
if len(key) > 8192 {
|
||||
return "", fmt.Errorf("SSH 公钥长度不能超过 8192 字符")
|
||||
}
|
||||
if strings.ContainsAny(key, "\r\n") || strings.ContainsRune(key, '\x00') {
|
||||
return "", fmt.Errorf("SSH 公钥只能填写一行")
|
||||
}
|
||||
|
||||
fields := strings.Fields(key)
|
||||
if len(fields) < 2 {
|
||||
return "", fmt.Errorf("SSH 公钥格式不正确")
|
||||
}
|
||||
if !isSupportedSSHKeyType(fields[0]) {
|
||||
return "", fmt.Errorf("不支持的 SSH 公钥类型: %s", fields[0])
|
||||
}
|
||||
parsed, _, _, rest, err := ssh.ParseAuthorizedKey([]byte(key))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("SSH 公钥格式不正确")
|
||||
}
|
||||
if strings.TrimSpace(string(rest)) != "" {
|
||||
return "", fmt.Errorf("一次只能填写一个 SSH 公钥")
|
||||
}
|
||||
if !isSupportedSSHKeyType(parsed.Type()) {
|
||||
return "", fmt.Errorf("不支持的 SSH 公钥类型: %s", parsed.Type())
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func resolveSSHAuthMode(rawMode, password, publicKey, defaultMode string) (string, error) {
|
||||
mode := strings.ToLower(strings.TrimSpace(rawMode))
|
||||
mode = strings.ReplaceAll(mode, "-", "_")
|
||||
if mode == "" {
|
||||
if strings.TrimSpace(publicKey) != "" {
|
||||
return SSHAuthKey, nil
|
||||
}
|
||||
if strings.TrimSpace(password) != "" {
|
||||
return SSHAuthPassword, nil
|
||||
}
|
||||
return defaultMode, nil
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "auto", "auto_password", "generated", "generate":
|
||||
return SSHAuthAutoPassword, nil
|
||||
case "password", "custom_password":
|
||||
return SSHAuthPassword, nil
|
||||
case "key", "ssh_key", "public_key":
|
||||
return SSHAuthKey, nil
|
||||
case "keep", "retain", "keep_password":
|
||||
return SSHAuthKeep, nil
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的 SSH 登录方式: %s", rawMode)
|
||||
}
|
||||
}
|
||||
|
||||
func isSupportedSSHKeyType(keyType string) bool {
|
||||
switch keyType {
|
||||
case "ssh-ed25519",
|
||||
"ssh-rsa",
|
||||
"ecdsa-sha2-nistp256",
|
||||
"ecdsa-sha2-nistp384",
|
||||
"ecdsa-sha2-nistp521",
|
||||
"sk-ssh-ed25519@openssh.com",
|
||||
"sk-ecdsa-sha2-nistp256@openssh.com":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user