继续实现按服务器管理SSH密钥
补充服务器SSH密钥状态模型、密钥生成及公钥部署接口,为自动部署流程继续开发做检查点。
This commit is contained in:
parent
5edf0fb092
commit
38c3e2a389
@ -9,7 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed migrations/sqlite/0001_initial.sql migrations/sqlite/0002_domain_options.sql migrations/sqlite/0003_deployment.sql migrations/sqlite/0004_task_attempts.sql migrations/sqlite/0005_users.sql migrations/sqlite/0006_server_validation.sql migrations/sqlite/0007_server_password_auth.sql migrations/mysql/0001_initial.sql migrations/mysql/0002_domain_options.sql migrations/mysql/0003_deployment.sql migrations/mysql/0004_task_attempts.sql migrations/mysql/0005_users.sql migrations/mysql/0006_server_validation.sql migrations/mysql/0007_server_password_auth.sql migrations/postgres/0001_initial.sql migrations/postgres/0002_domain_options.sql migrations/postgres/0003_deployment.sql migrations/postgres/0004_task_attempts.sql migrations/postgres/0005_users.sql migrations/postgres/0006_server_validation.sql migrations/postgres/0007_server_password_auth.sql
|
//go:embed migrations/sqlite/0001_initial.sql migrations/sqlite/0002_domain_options.sql migrations/sqlite/0003_deployment.sql migrations/sqlite/0004_task_attempts.sql migrations/sqlite/0005_users.sql migrations/sqlite/0006_server_validation.sql migrations/sqlite/0007_server_password_auth.sql migrations/sqlite/0009_server_ssh_key_status.sql migrations/mysql/0001_initial.sql migrations/mysql/0002_domain_options.sql migrations/mysql/0003_deployment.sql migrations/mysql/0004_task_attempts.sql migrations/mysql/0005_users.sql migrations/mysql/0006_server_validation.sql migrations/mysql/0007_server_password_auth.sql migrations/mysql/0009_server_ssh_key_status.sql migrations/postgres/0001_initial.sql migrations/postgres/0002_domain_options.sql migrations/postgres/0003_deployment.sql migrations/postgres/0004_task_attempts.sql migrations/postgres/0005_users.sql migrations/postgres/0006_server_validation.sql migrations/postgres/0007_server_password_auth.sql migrations/postgres/0009_server_ssh_key_status.sql
|
||||||
//go:embed migrations/sqlite/0008_user_groups.sql migrations/mysql/0008_user_groups.sql migrations/postgres/0008_user_groups.sql
|
//go:embed migrations/sqlite/0008_user_groups.sql migrations/mysql/0008_user_groups.sql migrations/postgres/0008_user_groups.sql
|
||||||
var migrationFiles embed.FS
|
var migrationFiles embed.FS
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ func Migrate(ctx context.Context, db *sql.DB, driver string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestMigrationVersion = 8
|
const latestMigrationVersion = 9
|
||||||
|
|
||||||
func applyMigration(ctx context.Context, db *sql.DB, driver string, version int) error {
|
func applyMigration(ctx context.Context, db *sql.DB, driver string, version int) error {
|
||||||
path := fmt.Sprintf("migrations/%s/%04d_%s.sql", driver, version, migrationName(version))
|
path := fmt.Sprintf("migrations/%s/%04d_%s.sql", driver, version, migrationName(version))
|
||||||
@ -119,7 +119,7 @@ func shouldSkipMySQLStatement(ctx context.Context, db *sql.DB, statement string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func migrationName(version int) string {
|
func migrationName(version int) string {
|
||||||
switch version {
|
switch version {`n`tcase 9:`n`t return "server_ssh_key_status"
|
||||||
case 1:
|
case 1:
|
||||||
return "initial"
|
return "initial"
|
||||||
case 2:
|
case 2:
|
||||||
|
|||||||
@ -17,6 +17,7 @@ type Client interface {
|
|||||||
Rename(ctx context.Context, source, target string) error
|
Rename(ctx context.Context, source, target string) error
|
||||||
Remove(ctx context.Context, path string) error
|
Remove(ctx context.Context, path string) error
|
||||||
Run(ctx context.Context, command []string) (CommandResult, error)
|
Run(ctx context.Context, command []string) (CommandResult, error)
|
||||||
|
InstallAuthorizedKey(ctx context.Context, publicKey string) error
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,16 @@ const (
|
|||||||
SSHAuthPassword SSHAuthMethod = "password"
|
SSHAuthPassword SSHAuthMethod = "password"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SSHKeyStatusNotConfigured SSHKeyStatus = "not_configured"
|
||||||
|
SSHKeyStatusPending SSHKeyStatus = "pending"
|
||||||
|
SSHKeyStatusDeployed SSHKeyStatus = "deployed"
|
||||||
|
SSHKeyStatusManual SSHKeyStatus = "manual"
|
||||||
|
SSHKeyStatusFailed SSHKeyStatus = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SSHKeyStatus string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
WebServerNginx WebServer = "nginx"
|
WebServerNginx WebServer = "nginx"
|
||||||
WebServerCaddy WebServer = "caddy"
|
WebServerCaddy WebServer = "caddy"
|
||||||
@ -44,6 +54,11 @@ type Server struct {
|
|||||||
PrivateKeyRef string `json:"private_key_ref"`
|
PrivateKeyRef string `json:"private_key_ref"`
|
||||||
PasswordRef string `json:"-"`
|
PasswordRef string `json:"-"`
|
||||||
PasswordConfigured bool `json:"password_configured"`
|
PasswordConfigured bool `json:"password_configured"`
|
||||||
|
SSHKeyStatus SSHKeyStatus `json:"ssh_key_status"`
|
||||||
|
SSHKeyFingerprint string `json:"ssh_key_fingerprint,omitempty"`
|
||||||
|
SSHKeyDeployedAt string `json:"ssh_key_deployed_at,omitempty"`
|
||||||
|
SSHKeyTestedAt string `json:"ssh_key_tested_at,omitempty"`
|
||||||
|
SSHKeyError string `json:"ssh_key_error,omitempty"`
|
||||||
HostKeyFingerprint string `json:"host_key_fingerprint"`
|
HostKeyFingerprint string `json:"host_key_fingerprint"`
|
||||||
CertificatePath string `json:"certificate_path"`
|
CertificatePath string `json:"certificate_path"`
|
||||||
PrivateKeyPath string `json:"private_key_path"`
|
PrivateKeyPath string `json:"private_key_path"`
|
||||||
@ -64,6 +79,11 @@ func (s Server) Normalize() Server {
|
|||||||
s.PrivateKeyRef = strings.TrimSpace(s.PrivateKeyRef)
|
s.PrivateKeyRef = strings.TrimSpace(s.PrivateKeyRef)
|
||||||
s.PasswordRef = strings.TrimSpace(s.PasswordRef)
|
s.PasswordRef = strings.TrimSpace(s.PasswordRef)
|
||||||
s.PasswordConfigured = s.PasswordRef != ""
|
s.PasswordConfigured = s.PasswordRef != ""
|
||||||
|
s.SSHKeyStatus = SSHKeyStatus(strings.ToLower(strings.TrimSpace(string(s.SSHKeyStatus))))
|
||||||
|
s.SSHKeyFingerprint = strings.TrimSpace(s.SSHKeyFingerprint)
|
||||||
|
s.SSHKeyDeployedAt = strings.TrimSpace(s.SSHKeyDeployedAt)
|
||||||
|
s.SSHKeyTestedAt = strings.TrimSpace(s.SSHKeyTestedAt)
|
||||||
|
s.SSHKeyError = strings.TrimSpace(s.SSHKeyError)
|
||||||
if s.AuthMethod == "" {
|
if s.AuthMethod == "" {
|
||||||
if s.PasswordRef != "" {
|
if s.PasswordRef != "" {
|
||||||
s.AuthMethod = SSHAuthPassword
|
s.AuthMethod = SSHAuthPassword
|
||||||
@ -71,6 +91,12 @@ func (s Server) Normalize() Server {
|
|||||||
s.AuthMethod = SSHAuthPrivateKey
|
s.AuthMethod = SSHAuthPrivateKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if s.SSHKeyStatus == "" {
|
||||||
|
s.SSHKeyStatus = SSHKeyStatusNotConfigured
|
||||||
|
if s.PrivateKeyRef != "" {
|
||||||
|
s.SSHKeyStatus = SSHKeyStatusManual
|
||||||
|
}
|
||||||
|
}
|
||||||
s.HostKeyFingerprint = strings.TrimSpace(s.HostKeyFingerprint)
|
s.HostKeyFingerprint = strings.TrimSpace(s.HostKeyFingerprint)
|
||||||
s.CertificatePath = strings.TrimSpace(s.CertificatePath)
|
s.CertificatePath = strings.TrimSpace(s.CertificatePath)
|
||||||
s.PrivateKeyPath = strings.TrimSpace(s.PrivateKeyPath)
|
s.PrivateKeyPath = strings.TrimSpace(s.PrivateKeyPath)
|
||||||
|
|||||||
@ -120,6 +120,8 @@ func (c *memoryClient) Run(_ context.Context, command []string) (CommandResult,
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *memoryClient) InstallAuthorizedKey(context.Context, string) error { return nil }
|
||||||
|
|
||||||
func (c *memoryClient) Close() error { return nil }
|
func (c *memoryClient) Close() error { return nil }
|
||||||
|
|
||||||
type memoryFactory struct {
|
type memoryFactory struct {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package deployment
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -332,6 +333,31 @@ func (c *sshClientAdapter) Remove(ctx context.Context, path string) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *sshClientAdapter) InstallAuthorizedKey(ctx context.Context, publicKey string) error {
|
||||||
|
publicKey = strings.TrimSpace(publicKey)
|
||||||
|
key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKey))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SSH 公钥格式无效:%w", err)
|
||||||
|
}
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(ssh.MarshalAuthorizedKey(key))
|
||||||
|
script := fmt.Sprintf(`set -eu
|
||||||
|
umask 077
|
||||||
|
install -d -m 700 "$HOME/.ssh"
|
||||||
|
touch "$HOME/.ssh/authorized_keys"
|
||||||
|
chmod 600 "$HOME/.ssh/authorized_keys"
|
||||||
|
decoded="$(printf '%s' '%s' | base64 -d)"
|
||||||
|
if ! grep -Fqx -- "$decoded" "$HOME/.ssh/authorized_keys"; then
|
||||||
|
printf '%%s\\n' "$decoded" >> "$HOME/.ssh/authorized_keys"
|
||||||
|
fi`, "%s", encoded)
|
||||||
|
result, err := c.Run(ctx, []string{"sh", "-c", script})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("部署 SSH 公钥失败:%w", err)
|
||||||
|
}
|
||||||
|
if result.ExitCode != 0 {
|
||||||
|
return fmt.Errorf("部署 SSH 公钥失败,远程退出码:%d", result.ExitCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
func (c *sshClientAdapter) Run(ctx context.Context, command []string) (CommandResult, error) {
|
func (c *sshClientAdapter) Run(ctx context.Context, command []string) (CommandResult, error) {
|
||||||
if len(command) == 0 {
|
if len(command) == 0 {
|
||||||
return CommandResult{}, errors.New("远程命令不能为空")
|
return CommandResult{}, errors.New("远程命令不能为空")
|
||||||
|
|||||||
@ -2,6 +2,9 @@ package sshkey
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/pem"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -90,6 +93,32 @@ func (s *Service) Create(ctx context.Context, name, privateKey, passphrase strin
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) CreateGenerated(ctx context.Context, name string) (Metadata, string, error) {
|
||||||
|
if s == nil || s.store == nil {
|
||||||
|
return Metadata{}, "", errors.New("SSH 私钥存储未初始化")
|
||||||
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return Metadata{}, "", errors.New("SSH 私钥名称不能为空")
|
||||||
|
}
|
||||||
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return Metadata{}, "", fmt.Errorf("生成 SSH 密钥失败:%w", err)
|
||||||
|
}
|
||||||
|
privatePEM, err := ssh.MarshalPrivateKey(privateKey, "scert-deploy")
|
||||||
|
if err != nil {
|
||||||
|
return Metadata{}, "", fmt.Errorf("编码 SSH 私钥失败:%w", err)
|
||||||
|
}
|
||||||
|
metadata, err := s.Create(ctx, name, string(pem.EncodeToMemory(privatePEM)), "")
|
||||||
|
if err != nil {
|
||||||
|
return Metadata{}, "", err
|
||||||
|
}
|
||||||
|
sshPublicKey, err := ssh.NewPublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return Metadata{}, "", fmt.Errorf("编码 SSH 公钥失败:%w", err)
|
||||||
|
}
|
||||||
|
return metadata, strings.TrimSpace(string(ssh.MarshalAuthorizedKey(sshPublicKey))), nil
|
||||||
|
}
|
||||||
func (s *Service) List(ctx context.Context) ([]Metadata, error) {
|
func (s *Service) List(ctx context.Context) ([]Metadata, error) {
|
||||||
if s == nil || s.store == nil {
|
if s == nil || s.store == nil {
|
||||||
return nil, errors.New("SSH 私钥存储未初始化")
|
return nil, errors.New("SSH 私钥存储未初始化")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user