继续实现按服务器管理SSH密钥
补充服务器SSH密钥状态模型、密钥生成及公钥部署接口,为自动部署流程继续开发做检查点。
This commit is contained in:
parent
5edf0fb092
commit
38c3e2a389
@ -9,7 +9,7 @@ import (
|
||||
"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
|
||||
var migrationFiles embed.FS
|
||||
|
||||
@ -39,7 +39,7 @@ func Migrate(ctx context.Context, db *sql.DB, driver string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const latestMigrationVersion = 8
|
||||
const latestMigrationVersion = 9
|
||||
|
||||
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))
|
||||
@ -119,7 +119,7 @@ func shouldSkipMySQLStatement(ctx context.Context, db *sql.DB, statement string)
|
||||
}
|
||||
|
||||
func migrationName(version int) string {
|
||||
switch version {
|
||||
switch version {`n`tcase 9:`n`t return "server_ssh_key_status"
|
||||
case 1:
|
||||
return "initial"
|
||||
case 2:
|
||||
|
||||
@ -17,6 +17,7 @@ type Client interface {
|
||||
Rename(ctx context.Context, source, target string) error
|
||||
Remove(ctx context.Context, path string) error
|
||||
Run(ctx context.Context, command []string) (CommandResult, error)
|
||||
InstallAuthorizedKey(ctx context.Context, publicKey string) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,16 @@ const (
|
||||
SSHAuthPassword SSHAuthMethod = "password"
|
||||
)
|
||||
|
||||
const (
|
||||
SSHKeyStatusNotConfigured SSHKeyStatus = "not_configured"
|
||||
SSHKeyStatusPending SSHKeyStatus = "pending"
|
||||
SSHKeyStatusDeployed SSHKeyStatus = "deployed"
|
||||
SSHKeyStatusManual SSHKeyStatus = "manual"
|
||||
SSHKeyStatusFailed SSHKeyStatus = "failed"
|
||||
)
|
||||
|
||||
type SSHKeyStatus string
|
||||
|
||||
const (
|
||||
WebServerNginx WebServer = "nginx"
|
||||
WebServerCaddy WebServer = "caddy"
|
||||
@ -44,6 +54,11 @@ type Server struct {
|
||||
PrivateKeyRef string `json:"private_key_ref"`
|
||||
PasswordRef string `json:"-"`
|
||||
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"`
|
||||
CertificatePath string `json:"certificate_path"`
|
||||
PrivateKeyPath string `json:"private_key_path"`
|
||||
@ -64,6 +79,11 @@ func (s Server) Normalize() Server {
|
||||
s.PrivateKeyRef = strings.TrimSpace(s.PrivateKeyRef)
|
||||
s.PasswordRef = strings.TrimSpace(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.PasswordRef != "" {
|
||||
s.AuthMethod = SSHAuthPassword
|
||||
@ -71,6 +91,12 @@ func (s Server) Normalize() Server {
|
||||
s.AuthMethod = SSHAuthPrivateKey
|
||||
}
|
||||
}
|
||||
if s.SSHKeyStatus == "" {
|
||||
s.SSHKeyStatus = SSHKeyStatusNotConfigured
|
||||
if s.PrivateKeyRef != "" {
|
||||
s.SSHKeyStatus = SSHKeyStatusManual
|
||||
}
|
||||
}
|
||||
s.HostKeyFingerprint = strings.TrimSpace(s.HostKeyFingerprint)
|
||||
s.CertificatePath = strings.TrimSpace(s.CertificatePath)
|
||||
s.PrivateKeyPath = strings.TrimSpace(s.PrivateKeyPath)
|
||||
|
||||
@ -120,6 +120,8 @@ func (c *memoryClient) Run(_ context.Context, command []string) (CommandResult,
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (c *memoryClient) InstallAuthorizedKey(context.Context, string) error { return nil }
|
||||
|
||||
func (c *memoryClient) Close() error { return nil }
|
||||
|
||||
type memoryFactory struct {
|
||||
|
||||
@ -2,6 +2,7 @@ package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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) {
|
||||
if len(command) == 0 {
|
||||
return CommandResult{}, errors.New("远程命令不能为空")
|
||||
|
||||
@ -2,6 +2,9 @@ package sshkey
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@ -90,6 +93,32 @@ func (s *Service) Create(ctx context.Context, name, privateKey, passphrase strin
|
||||
}, 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) {
|
||||
if s == nil || s.store == nil {
|
||||
return nil, errors.New("SSH 私钥存储未初始化")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user