From be0a460935072c56955a459c13ed8d09ee2c2c1e Mon Sep 17 00:00:00 2001 From: ssongliu <73214554+ssongliu@users.noreply.github.com> Date: Fri, 1 Aug 2025 10:43:20 +0800 Subject: [PATCH] feat: Optimize SSH key management (#9782) --- agent/app/api/v2/setting.go | 2 +- agent/app/api/v2/ssh.go | 90 +++- agent/app/dto/ssh.go | 21 +- agent/app/model/ssh.go | 11 + agent/app/repo/host.go | 89 ++++ agent/app/service/ssh.go | 229 +++++++-- agent/init/migration/migrations/init.go | 1 + agent/router/ro_host.go | 7 +- frontend/src/api/interface/host.ts | 20 +- frontend/src/api/modules/host.ts | 28 +- frontend/src/lang/modules/en.ts | 10 +- frontend/src/lang/modules/ja.ts | 10 +- frontend/src/lang/modules/ko.ts | 10 +- frontend/src/lang/modules/ms.ts | 12 +- frontend/src/lang/modules/pt-br.ts | 10 +- frontend/src/lang/modules/ru.ts | 10 +- frontend/src/lang/modules/tr.ts | 10 +- frontend/src/lang/modules/zh-Hant.ts | 10 +- frontend/src/lang/modules/zh.ts | 10 +- .../host/ssh/ssh/certification/index.vue | 476 ++++++++++++++++++ frontend/src/views/host/ssh/ssh/index.vue | 13 +- .../src/views/host/ssh/ssh/pubkey/index.vue | 142 ------ 22 files changed, 981 insertions(+), 240 deletions(-) create mode 100644 agent/app/model/ssh.go create mode 100644 frontend/src/views/host/ssh/ssh/certification/index.vue delete mode 100644 frontend/src/views/host/ssh/ssh/pubkey/index.vue diff --git a/agent/app/api/v2/setting.go b/agent/app/api/v2/setting.go index 62b30c983..af226d47f 100644 --- a/agent/app/api/v2/setting.go +++ b/agent/app/api/v2/setting.go @@ -113,7 +113,7 @@ func loadLocalConn() (*ssh.SSHClient, error) { itemPath = path.Join(currentInfo.HomeDir, ".ssh/id_ed25519_1panel") } if _, err := os.Stat(itemPath); err != nil { - _ = sshService.GenerateSSH(dto.GenerateSSH{EncryptionMode: "ed25519", Name: "_1panel"}) + _ = sshService.CreateRootCert(dto.CreateRootCert{EncryptionMode: "ed25519", Name: "1panel", Description: "1Panel Terminal"}) } privateKey, _ := os.ReadFile(itemPath) diff --git a/agent/app/api/v2/ssh.go b/agent/app/api/v2/ssh.go index 9215d960c..acd271b37 100644 --- a/agent/app/api/v2/ssh.go +++ b/agent/app/api/v2/ssh.go @@ -1,6 +1,8 @@ package v2 import ( + "encoding/base64" + "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/gin-gonic/gin" @@ -90,19 +92,58 @@ func (b *BaseApi) UpdateSSHByfile(c *gin.Context) { // @Tags SSH // @Summary Generate host SSH secret // @Accept json -// @Param request body dto.GenerateSSH true "request" +// @Param request body dto.CreateRootCert true "request" // @Success 200 // @Security ApiKeyAuth // @Security Timestamp -// @Router /hosts/ssh/generate [post] +// @Router /hosts/ssh/cert [post] // @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成 SSH 密钥 ","formatEN":"generate SSH secret"} -func (b *BaseApi) GenerateSSH(c *gin.Context) { - var req dto.GenerateSSH +func (b *BaseApi) CreateRootCert(c *gin.Context) { + var req dto.CreateRootCert if err := helper.CheckBindAndValidate(&req, c); err != nil { return } + if len(req.PassPhrase) != 0 { + passPhrase, err := base64.StdEncoding.DecodeString(req.PassPhrase) + if err != nil { + helper.BadRequest(c, err) + return + } + req.PassPhrase = string(passPhrase) + } + if len(req.PrivateKey) != 0 { + privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey) + if err != nil { + helper.BadRequest(c, err) + return + } + req.PrivateKey = string(privateKey) + } + if len(req.PublicKey) != 0 { + publicKey, err := base64.StdEncoding.DecodeString(req.PublicKey) + if err != nil { + helper.BadRequest(c, err) + return + } + req.PublicKey = string(publicKey) + } - if err := sshService.GenerateSSH(req); err != nil { + if err := sshService.CreateRootCert(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} + +// @Tags SSH +// @Summary Sycn host SSH secret +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/ssh/cert/sync [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 SSH 密钥 ","formatEN":"sync SSH secret"} +func (b *BaseApi) SyncRootCert(c *gin.Context) { + if err := sshService.SyncRootCert(); err != nil { helper.InternalServer(c, err) return } @@ -112,23 +153,48 @@ func (b *BaseApi) GenerateSSH(c *gin.Context) { // @Tags SSH // @Summary Load host SSH secret // @Accept json -// @Param request body dto.GenerateLoad true "request" -// @Success 200 {string} secret +// @Param request body dto.SearchWithPage true "request" +// @Success 200 {object} dto.PageResult // @Security ApiKeyAuth // @Security Timestamp -// @Router /hosts/ssh/secret [post] -func (b *BaseApi) LoadSSHSecret(c *gin.Context) { - var req dto.GenerateLoad +// @Router /hosts/ssh/cert/search [post] +func (b *BaseApi) SearchRootCert(c *gin.Context) { + var req dto.SearchWithPage if err := helper.CheckBindAndValidate(&req, c); err != nil { return } - data, err := sshService.LoadSSHSecret(req.EncryptionMode) + total, data, err := sshService.SearchRootCerts(req) if err != nil { helper.InternalServer(c, err) return } - helper.SuccessWithData(c, data) + helper.SuccessWithData(c, dto.PageResult{ + Total: total, + Items: data, + }) +} + +// @Tags SSH +// @Summary Delete host SSH secret +// @Accept json +// @Param request body dto.ForceDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /hosts/ssh/cert/delete [post] +// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 SSH 密钥 ","formatEN":"delete SSH secret"} +func (b *BaseApi) DeleteRootCert(c *gin.Context) { + var req dto.ForceDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + if err := sshService.DeleteRootCerts(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) } // @Tags SSH diff --git a/agent/app/dto/ssh.go b/agent/app/dto/ssh.go index be606b437..7a5018117 100644 --- a/agent/app/dto/ssh.go +++ b/agent/app/dto/ssh.go @@ -19,12 +19,27 @@ type SSHInfo struct { PubkeyAuthentication string `json:"pubkeyAuthentication"` PermitRootLogin string `json:"permitRootLogin"` UseDNS string `json:"useDNS"` + CurrentUser string `json:"currentUser"` } -type GenerateSSH struct { - EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"` - Password string `json:"password"` +type CreateRootCert struct { Name string `json:"name"` + Mode string `json:"mode"` + EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"` + PassPhrase string `json:"passPhrase"` + PublicKey string `json:"publicKey"` + PrivateKey string `json:"privateKey"` + Description string `json:"description"` +} +type RootCert struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"createdAt"` + Name string `json:"name"` + EncryptionMode string `json:"encryptionMode"` + PassPhrase string `json:"passPhrase"` + PublicKey string `json:"publicKey"` + PrivateKey string `json:"privateKey"` + Description string `json:"description"` } type GenerateLoad struct { diff --git a/agent/app/model/ssh.go b/agent/app/model/ssh.go new file mode 100644 index 000000000..04831d232 --- /dev/null +++ b/agent/app/model/ssh.go @@ -0,0 +1,11 @@ +package model + +type RootCert struct { + BaseModel + Name string `json:"name" gorm:"not null;"` + EncryptionMode string `json:"encryptionMode"` + PassPhrase string `json:"passPhrase"` + PublicKeyPath string `json:"publicKeyPath"` + PrivateKeyPath string `json:"privateKeyPath"` + Description string `json:"description"` +} diff --git a/agent/app/repo/host.go b/agent/app/repo/host.go index 05081136a..cd5e90b9b 100644 --- a/agent/app/repo/host.go +++ b/agent/app/repo/host.go @@ -2,7 +2,9 @@ package repo import ( "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" ) type HostRepo struct{} @@ -13,6 +15,14 @@ type IHostRepo interface { SaveFirewallRecord(firewall *model.Firewall) error DeleteFirewallRecordByID(id uint) error DeleteFirewallRecord(fType, port, protocol, address, strategy string) error + + SyncCert(data []model.RootCert) error + GetCert(opts ...DBOption) (model.RootCert, error) + PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error) + ListCert(opts ...DBOption) ([]model.RootCert, error) + CreateCert(cert *model.RootCert) error + UpdateCert(id uint, vars map[string]interface{}) error + DeleteCert(opts ...DBOption) error } func NewIHostRepo() IHostRepo { @@ -63,3 +73,82 @@ func (h *HostRepo) DeleteFirewallRecordByID(id uint) error { func (h *HostRepo) DeleteFirewallRecord(fType, port, protocol, address, strategy string) error { return global.DB.Where("type = ? AND port = ? AND protocol = ? AND address = ? AND strategy = ?", fType, port, protocol, address, strategy).Delete(&model.Firewall{}).Error } + +func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) { + var cert model.RootCert + db := global.DB + for _, opt := range opts { + db = opt(db) + } + err := db.First(&cert).Error + return cert, err +} + +func (u *HostRepo) PageCert(page, size int, opts ...DBOption) (int64, []model.RootCert, error) { + var ops []model.RootCert + db := global.DB.Model(&model.RootCert{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&ops).Error + return count, ops, err +} + +func (u *HostRepo) ListCert(opts ...DBOption) ([]model.RootCert, error) { + var ops []model.RootCert + db := global.DB.Model(&model.RootCert{}) + for _, opt := range opts { + db = opt(db) + } + count := int64(0) + db = db.Count(&count) + err := db.Find(&ops).Error + return ops, err +} + +func (u *HostRepo) CreateCert(cert *model.RootCert) error { + return global.DB.Create(cert).Error +} + +func (u *HostRepo) UpdateCert(id uint, vars map[string]interface{}) error { + return global.DB.Model(&model.RootCert{}).Where("id = ?", id).Updates(vars).Error +} + +func (u *HostRepo) DeleteCert(opts ...DBOption) error { + db := global.DB + for _, opt := range opts { + db = opt(db) + } + return db.Delete(&model.RootCert{}).Error +} + +func (u *HostRepo) SyncCert(data []model.RootCert) error { + tx := global.DB.Begin() + var oldCerts []model.RootCert + _ = tx.Where("1 = ?", 1).Find(&oldCerts).Error + oldCertsMap := make(map[string]uint) + for _, item := range oldCerts { + oldCertsMap[item.Name] = item.ID + } + for _, item := range data { + if _, ok := oldCertsMap[item.Name]; ok { + delete(oldCertsMap, item.Name) + continue + } + item.PassPhrase, _ = encrypt.StringEncrypt("") + if err := tx.Model(model.RootCert{}).Create(&item).Error; err != nil { + tx.Rollback() + return err + } + } + for _, val := range oldCertsMap { + if err := tx.Model(&model.RootCert{}).Where("id = ?", val).Updates(map[string]interface{}{"status": constant.StatusDeleted}).Error; err != nil { + tx.Rollback() + return err + } + } + tx.Commit() + return nil +} diff --git a/agent/app/service/ssh.go b/agent/app/service/ssh.go index ec50ea924..cf45d2cde 100644 --- a/agent/app/service/ssh.go +++ b/agent/app/service/ssh.go @@ -1,6 +1,8 @@ package service import ( + "bytes" + "encoding/base64" "fmt" "os" "os/user" @@ -9,16 +11,19 @@ import ( "strings" "time" + "github.com/1Panel-dev/1Panel/agent/utils/copier" + "github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/geo" "github.com/gin-gonic/gin" "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/common" - "github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/systemctl" "github.com/pkg/errors" ) @@ -32,11 +37,13 @@ type ISSHService interface { OperateSSH(operation string) error UpdateByFile(value string) error Update(req dto.SSHUpdate) error - GenerateSSH(req dto.GenerateSSH) error - LoadSSHSecret(mode string) (string, error) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog, error) - LoadSSHConf() (string, error) + + SyncRootCert() error + CreateRootCert(req dto.CreateRootCert) error + SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error) + DeleteRootCerts(req dto.ForceDelete) error } func NewISSHService() ISSHService { @@ -110,6 +117,14 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) { data.UseDNS = strings.ReplaceAll(line, "UseDNS ", "") } } + + currentUser, err := user.Current() + if err != nil || len(currentUser.Name) == 0 { + data.CurrentUser = "root" + } else { + data.CurrentUser = currentUser.Name + } + return &data, nil } @@ -214,70 +229,172 @@ func (u *SSHService) UpdateByFile(value string) error { return nil } -func (u *SSHService) GenerateSSH(req dto.GenerateSSH) error { - if cmd.CheckIllegal(req.EncryptionMode, req.Password) { +func (u *SSHService) SyncRootCert() error { + currentUser, err := user.Current() + if err != nil { + return fmt.Errorf("load current user failed, err: %v", err) + } + sshDir := fmt.Sprintf("%s/.ssh", currentUser.HomeDir) + authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys" + authItem, err := os.ReadFile(authFilePath) + if err != nil { + return err + } + + fileList, err := os.ReadDir(sshDir) + if err != nil { + return err + } + var rootCerts []model.RootCert + fileMap := make(map[string]bool) + for _, item := range fileList { + if !item.IsDir() { + fileMap[item.Name()] = true + } + } + for item := range fileMap { + if !strings.HasSuffix(item, ".pub") { + continue + } + if !fileMap[strings.TrimSuffix(item, ".pub")] { + continue + } + cert := model.RootCert{Name: strings.TrimSuffix(item, ".pub"), PublicKeyPath: path.Join(sshDir, item), PrivateKeyPath: path.Join(sshDir, strings.TrimSuffix(item, ".pub"))} + pubItem, err := os.ReadFile(path.Join(sshDir, item)) + if err != nil { + global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err) + continue + } + cert.EncryptionMode = loadEncryptioMode(string(pubItem)) + if !bytes.Contains(authItem, pubItem) { + global.LOG.Error("the public key is not in authorized_keys, skip...") + continue + } + rootCerts = append(rootCerts, cert) + } + return hostRepo.SyncCert(rootCerts) +} + +func (u *SSHService) CreateRootCert(req dto.CreateRootCert) error { + if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) { return buserr.New("ErrCmdIllegal") } + certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name)) + if certItem.ID != 0 { + return buserr.New("ErrRecordExist") + } currentUser, err := user.Current() if err != nil { return fmt.Errorf("load current user failed, err: %v", err) } - secretFile := fmt.Sprintf("%s/.ssh/id_item_%s", currentUser.HomeDir, req.EncryptionMode) - secretPubFile := fmt.Sprintf("%s/.ssh/id_item_%s.pub", currentUser.HomeDir, req.EncryptionMode) + var cert model.RootCert + if err := copier.Copy(&cert, req); err != nil { + return err + } + privatePath := fmt.Sprintf("%s/.ssh/%s", currentUser.HomeDir, req.Name) + publicPath := fmt.Sprintf("%s/.ssh/%s.pub", currentUser.HomeDir, req.Name) authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys" - command := fmt.Sprintf("ssh-keygen -t %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, currentUser.HomeDir, req.EncryptionMode) - if len(req.Password) != 0 { - command = fmt.Sprintf("ssh-keygen -t %s -P %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, req.Password, currentUser.HomeDir, req.EncryptionMode) + if req.Mode == "input" || req.Mode == "import" { + if err := os.WriteFile(privatePath, []byte(req.PrivateKey), constant.FilePerm); err != nil { + return err + } + if err := os.WriteFile(publicPath, []byte(req.PublicKey), constant.FilePerm); err != nil { + return err + } + } else { + command := fmt.Sprintf("ssh-keygen -t %s -f %s/.ssh/%s -N ''", req.EncryptionMode, currentUser.HomeDir, req.Name) + if len(req.PassPhrase) != 0 { + command = fmt.Sprintf("ssh-keygen -t %s -P %s -f %s/.ssh/%s | echo y", req.EncryptionMode, req.PassPhrase, currentUser.HomeDir, req.Name) + } + stdout, err := cmd.RunDefaultWithStdoutBashC(command) + if err != nil { + return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout) + } } - stdout, err := cmd.RunDefaultWithStdoutBashC(command) + + stdout, err := cmd.RunDefaultWithStdoutBashCf("cat %s >> %s", publicPath, authFilePath) if err != nil { return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout) } - defer func() { - _ = os.Remove(secretFile) - }() - defer func() { - _ = os.Remove(secretPubFile) - }() - if _, err := os.Stat(authFilePath); err != nil && errors.Is(err, os.ErrNotExist) { - authFile, err := os.Create(authFilePath) - if err != nil { + cert.PrivateKeyPath = privatePath + cert.PublicKeyPath = publicPath + if len(cert.PassPhrase) != 0 { + cert.PassPhrase, _ = encrypt.StringEncrypt(cert.PassPhrase) + } + return hostRepo.CreateCert(&cert) +} + +func (u *SSHService) SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error) { + total, records, err := hostRepo.PageCert(req.Page, req.PageSize) + if err != nil { + return 0, nil, err + } + var datas []dto.RootCert + for i := 0; i < len(records); i++ { + publicItem, err := os.ReadFile(records[i].PublicKeyPath) + var publicBase64 string + if err == nil && len(publicItem) != 0 { + publicBase64 = base64.StdEncoding.EncodeToString(publicItem) + } + privateItem, _ := os.ReadFile(records[i].PrivateKeyPath) + var privateBase64 string + if err == nil && len(publicItem) != 0 { + privateBase64 = base64.StdEncoding.EncodeToString(privateItem) + } + passPhrase, _ := encrypt.StringDecryptWithBase64(records[i].PassPhrase) + datas = append(datas, dto.RootCert{ + ID: records[i].ID, + CreatedAt: records[i].CreatedAt, + Name: records[i].Name, + EncryptionMode: records[i].EncryptionMode, + PassPhrase: passPhrase, + PublicKey: publicBase64, + PrivateKey: privateBase64, + Description: records[i].Description, + }) + } + return total, datas, err +} + +func (u *SSHService) DeleteRootCerts(req dto.ForceDelete) error { + currentUser, err := user.Current() + if err != nil && !req.ForceDelete { + return fmt.Errorf("load current user failed, err: %v", err) + } + authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys" + authItem, err := os.ReadFile(authFilePath) + if err != nil && !req.ForceDelete { + return err + } + for _, id := range req.IDs { + cert, _ := hostRepo.GetCert(repo.WithByID(id)) + if cert.ID == 0 { + if !req.ForceDelete { + return buserr.New("ErrRecordNotFound") + } else { + continue + } + } + publicItem, err := os.ReadFile(cert.PublicKeyPath) + if err != nil && !req.ForceDelete { + return err + } + newFile := bytes.ReplaceAll(authItem, publicItem, nil) + if err := os.WriteFile(authFilePath, newFile, constant.FilePerm); err != nil && !req.ForceDelete { + return fmt.Errorf("refresh authorized_keys failed, err: %v", err) + } + _ = os.Remove(cert.PublicKeyPath) + _ = os.Remove(cert.PrivateKeyPath) + if err := hostRepo.DeleteCert(repo.WithByID(id)); err != nil && !req.ForceDelete { return err } - defer authFile.Close() - } - stdout1, err := cmd.RunDefaultWithStdoutBashCf("cat %s >> %s/.ssh/authorized_keys", secretPubFile, currentUser.HomeDir) - if err != nil { - return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout1) - } - - fileOp := files.NewFileOp() - if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s%s", currentUser.HomeDir, req.EncryptionMode, req.Name)); err != nil { - return err - } - if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s%s.pub", currentUser.HomeDir, req.EncryptionMode, req.Name)); err != nil { - return err } return nil } -func (u *SSHService) LoadSSHSecret(mode string) (string, error) { - currentUser, err := user.Current() - if err != nil { - return "", fmt.Errorf("load current user failed, err: %v", err) - } - - homeDir := currentUser.HomeDir - if _, err := os.Stat(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)); err != nil { - return "", nil - } - file, err := os.ReadFile(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)) - return string(file), err -} - type sshFileItem struct { Name string Year int @@ -576,3 +693,19 @@ func analyzeDateStr(parts []string) (int, string) { } return 2, fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]) } + +func loadEncryptioMode(content string) string { + if strings.HasPrefix(content, "ssh-rsa") { + return "rsa" + } + if strings.HasPrefix(content, "ssh-ed25519") { + return "ed25519" + } + if strings.HasPrefix(content, "ssh-ecdsa") { + return "ecdsa" + } + if strings.HasPrefix(content, "ssh-dsa") { + return "dsa" + } + return "" +} diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 7dab00f6d..b029b0df1 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -63,6 +63,7 @@ var AddTable = &gormigrate.Migration{ &model.Group{}, &model.AppIgnoreUpgrade{}, &model.McpServer{}, + &model.RootCert{}, ) }, } diff --git a/agent/router/ro_host.go b/agent/router/ro_host.go index b601ae39a..47a3763ef 100644 --- a/agent/router/ro_host.go +++ b/agent/router/ro_host.go @@ -32,12 +32,15 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) { hostRouter.GET("/ssh/conf", baseApi.LoadSSHConf) hostRouter.POST("/ssh/search", baseApi.GetSSHInfo) hostRouter.POST("/ssh/update", baseApi.UpdateSSH) - hostRouter.POST("/ssh/generate", baseApi.GenerateSSH) - hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret) hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs) hostRouter.POST("/ssh/conffile/update", baseApi.UpdateSSHByfile) hostRouter.POST("/ssh/operate", baseApi.OperateSSH) + hostRouter.POST("/ssh/cert", baseApi.CreateRootCert) + hostRouter.POST("/ssh/cert/sync", baseApi.SyncRootCert) + hostRouter.POST("/ssh/cert/search", baseApi.SearchRootCert) + hostRouter.POST("/ssh/cert/delete", baseApi.DeleteRootCert) + hostRouter.POST("/tool", baseApi.GetToolStatus) hostRouter.POST("/tool/init", baseApi.InitToolConfig) hostRouter.POST("/tool/operate", baseApi.OperateTool) diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index a8b857115..78ce46000 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -161,15 +161,31 @@ export namespace Host { primaryKey: string; permitRootLogin: string; useDNS: string; + currentUser: string; } export interface SSHUpdate { key: string; oldValue: string; newValue: string; } - export interface SSHGenerate { + export interface RootCert { + name: string; + mode: string; encryptionMode: string; - password: string; + passPhrase: string; + privateKey: string; + publicKey: string; + description: string; + } + export interface RootCertInfo { + id: number; + createAt: Date; + name: string; + encryptionMode: string; + passPhrase: string; + description: string; + publicKey: string; + privateKey: string; } export interface searchSSHLog extends ReqPage { info: string; diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index a06276185..8ffc1927b 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -1,7 +1,9 @@ import http from '@/api'; -import { ResPage } from '../interface'; +import { ResPage, ReqPage } from '../interface'; import { Host } from '../interface/host'; import { TimeoutEnum } from '@/enums/http-enum'; +import { deepCopy } from '@/utils/util'; +import { Base64 } from 'js-base64'; // firewall export const loadFireBaseInfo = () => { @@ -71,11 +73,27 @@ export const updateSSH = (params: Host.SSHUpdate) => { export const updateSSHByfile = (file: string) => { return http.post(`/hosts/ssh/conffile/update`, { file: file }, TimeoutEnum.T_40S); }; -export const generateSecret = (params: Host.SSHGenerate) => { - return http.post(`/hosts/ssh/generate`, params); +export const createCert = (params: Host.RootCert) => { + let request = deepCopy(params) as Host.RootCert; + if (request.passPhrase) { + request.passPhrase = Base64.encode(request.passPhrase); + } + if (request.privateKey) { + request.privateKey = Base64.encode(request.privateKey); + } + if (request.publicKey) { + request.publicKey = Base64.encode(request.publicKey); + } + return http.post(`/hosts/ssh/cert`, request); }; -export const loadSecret = (mode: string) => { - return http.post(`/hosts/ssh/secret`, { encryptionMode: mode }); +export const searchCert = (params: ReqPage) => { + return http.post>(`/hosts/ssh/cert/search`, params); +}; +export const deleteCert = (ids: Array, forceDelete: boolean) => { + return http.post(`/hosts/ssh/cert/delete`, { ids: ids, forceDelete: forceDelete }); +}; +export const syncCert = () => { + return http.post(`/hosts/ssh/cert/sync`); }; export const loadSSHLogs = (params: Host.searchSSHLog) => { return http.post(`/hosts/ssh/log`, params); diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 7461289a2..86fbd156d 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -1527,12 +1527,18 @@ const message = { passwordAuthentication: 'Password auth', pwdAuthHelper: 'Whether to enable password authentication. This parameter is enabled by default.', pubkeyAuthentication: 'Key auth', - key: 'Key', + privateKey: 'Private Key', + publicKey: 'Public Key', password: 'Password', + createMode: 'Creation Method', + generate: 'Auto-generate', + unSyncPass: 'Key password cannot be synchronized', + input: 'Manual Input', + import: 'File Upload', pubkey: 'Key info', + pubKeyHelper: 'The current key information only takes effect for user {0}', encryptionMode: 'Encryption mode', passwordHelper: 'Can contain 6 to 10 digits and English cases', - generate: 'Generate key', reGenerate: 'Regenerate key', keyAuthHelper: 'Whether to enable key authentication.', useDNS: 'useDNS', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index faee02fa3..37df69774 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -1471,12 +1471,18 @@ const message = { passwordAuthentication: 'パスワード認証', pwdAuthHelper: 'パスワード認証を有効にするかどうか。このパラメーターはデフォルトで有効になります。', pubkeyAuthentication: '重要な認証', - key: '鍵', + privateKey: '秘密鍵', + publicKey: '公開鍵', password: 'パスワード', + createMode: '作成方法', + generate: '自動生成', + unSyncPass: '鍵パスワードは同期できません', + input: '手動入力', + import: 'ファイルアップロード', pubkey: '重要な情報', encryptionMode: '暗号化モード', + pubKeyHelper: '現在の鍵情報はユーザー {0} にのみ有効です', passwordHelper: '6〜10桁と英語のケースを含めることができます', - generate: 'キーを生成します', reGenerate: 'キーを再生します', keyAuthHelper: 'キー認証を有効にするかどうか。', useDNS: '使用済み', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 8afce23ec..26754cc01 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -1455,12 +1455,18 @@ const message = { passwordAuthentication: '비밀번호 인증', pwdAuthHelper: '비밀번호 인증을 활성화할지 여부입니다. 기본적으로 이 매개변수는 활성화되어 있습니다.', pubkeyAuthentication: '키 인증', - key: '키', + privateKey: '개인 키', + publicKey: '공개 키', password: '비밀번호', + createMode: '생성 방식', + generate: '자동 생성', + unSyncPass: '키 비밀번호 동기화 불가', + input: '수동 입력', + import: '파일 업로드', pubkey: '키 정보', encryptionMode: '암호화 모드', + pubKeyHelper: '현재 키 정보는 사용자 {0}에게만 적용됩니다', passwordHelper: '6~10자리 숫자 및 영어 대소문자를 포함할 수 있습니다.', - generate: '키 생성', reGenerate: '키 재생성', keyAuthHelper: '키 인증을 활성화할지 여부입니다.', useDNS: 'useDNS', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 035360f75..087c02665 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -1513,12 +1513,18 @@ const message = { passwordAuthentication: 'Pengesahan kata laluan', pwdAuthHelper: 'Sama ada untuk mengaktifkan pengesahan kata laluan. Parameter ini diaktifkan secara lalai.', pubkeyAuthentication: 'Pengesahan kunci', - key: 'Kunci', - password: 'Kata laluan', + privateKey: 'Kunci Persendirian', + publicKey: 'Kunci Awam', + password: 'Kata Laluan', + createMode: 'Kaedah Penciptaan', + generate: 'Jana Automatik', + unSyncPass: 'Kata laluan kunci tidak dapat diselaraskan', + input: 'Input Manual', + import: 'Muat Naik Fail', pubkey: 'Maklumat kunci', + pubKeyHelper: 'Maklumat kunci semasa hanya berkuat kuasa untuk pengguna {0}', encryptionMode: 'Mod penyulitan', passwordHelper: 'Boleh mengandungi 6 hingga 10 angka dan huruf dalam kedua-dua huruf besar dan kecil', - generate: 'Jana kunci', reGenerate: 'Jana semula kunci', keyAuthHelper: 'Sama ada untuk mengaktifkan pengesahan kunci.', useDNS: 'Gunakan DNS', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 594ada2aa..dde7403a9 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -1501,12 +1501,18 @@ const message = { passwordAuthentication: 'Autenticação por senha', pwdAuthHelper: 'Se deve ou não habilitar a autenticação por senha. Esse parâmetro está habilitado por padrão.', pubkeyAuthentication: 'Autenticação por chave', - key: 'Chave', + privateKey: 'Chave Privada', + publicKey: 'Chave Pública', password: 'Senha', + createMode: 'Método de Criação', + generate: 'Gerar Automaticamente', + unSyncPass: 'Senha da chave não pode ser sincronizada', + input: 'Entrada Manual', + import: 'Upload de Arquivo', pubkey: 'Informações da chave', + pubKeyHelper: 'A informação da chave atual só tem efeito para o usuário {0}', encryptionMode: 'Modo de criptografia', passwordHelper: 'Pode conter de 6 a 10 dígitos e letras maiúsculas e minúsculas', - generate: 'Gerar chave', reGenerate: 'Regenerar chave', keyAuthHelper: 'Se deve ou não habilitar a autenticação por chave.', useDNS: 'Usar DNS', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index f2fc11171..4c1c04927 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -1502,12 +1502,18 @@ const message = { passwordAuthentication: 'Аутентификация по паролю', pwdAuthHelper: 'Включить ли аутентификацию по паролю. Этот параметр включен по умолчанию.', pubkeyAuthentication: 'Аутентификация по ключу', - key: 'Ключ', + privateKey: 'Приватный ключ', + publicKey: 'Публичный ключ', password: 'Пароль', + createMode: 'Способ создания', + generate: 'Автогенерация', + unSyncPass: 'Пароль ключа не может быть синхронизирован', + input: 'Ручной ввод', + import: 'Загрузка файла', pubkey: 'Информация о ключе', + pubKeyHelper: 'Текущая информация о ключе действительна только для пользователя {0}', encryptionMode: 'Режим шифрования', passwordHelper: 'Может содержать от 6 до 10 цифр и английских букв в разных регистрах', - generate: 'Сгенерировать ключ', reGenerate: 'Перегенерировать ключ', keyAuthHelper: 'Включить ли аутентификацию по ключу.', useDNS: 'useDNS', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 2a753467c..868107803 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -1545,12 +1545,18 @@ const message = { pwdAuthHelper: 'Parola kimlik doğrulamasının etkinleştirilip etkinleştirilmeyeceği. Bu parametre varsayılan olarak etkindir.', pubkeyAuthentication: 'Anahtar kimlik doğrulaması', - key: 'Anahtar', + privateKey: 'Özel Anahtar', + publicKey: 'Genel Anahtar', password: 'Parola', + createMode: 'Oluşturma Yöntemi', + generate: 'Otomatik Oluştur', + unSyncPass: 'Anahtar parolası senkronize edilemez', + input: 'Manuel Giriş', + import: 'Dosya Yükleme', pubkey: 'Anahtar bilgisi', + pubKeyHelper: 'Mevcut anahtar bilgileri yalnızca {0} kullanıcısı için geçerlidir', encryptionMode: 'Şifreleme modu', passwordHelper: '6 ila 10 hane ve İngilizce harfler içerebilir', - generate: 'Anahtar oluştur', reGenerate: 'Anahtarı yeniden oluştur', keyAuthHelper: 'Anahtar kimlik doğrulamasının etkinleştirilip etkinleştirilmeyeceği.', useDNS: 'DNS kullanımı', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index ea957af0e..bc55c3afe 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -1454,12 +1454,18 @@ const message = { passwordAuthentication: '密碼認證', pwdAuthHelper: '是否啟用密碼認證,默認啟用。', pubkeyAuthentication: '密鑰認證', - key: '密鑰', + privateKey: '私鑰', + publicKey: '公鑰', password: '密碼', + createMode: '創建方式', + generate: '自動生成', + unSyncPass: '密鑰密碼無法同步', + input: '手動輸入', + import: '文件上傳', pubkey: '密鑰信息', + pubKeyHelper: '當前密鑰信息僅對用戶 {0} 生效', encryptionMode: '加密方式', passwordHelper: '支持大小寫英文、數字,長度6-10', - generate: '生成密鑰', reGenerate: '重新生成密鑰', keyAuthHelper: '是否啟用密鑰認證,默認啟用。', useDNS: '反向解析', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index fd4679296..b2d24d6c8 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -1449,12 +1449,18 @@ const message = { passwordAuthentication: '密码认证', pwdAuthHelper: '是否启用密码认证,默认启用。', pubkeyAuthentication: '密钥认证', - key: '密钥', + privateKey: '私钥', + publicKey: '公钥', password: '密码', + createMode: '创建方式', + generate: '自动生成', + unSyncPass: '密钥密码无法同步', + input: '手动输入', + import: '文件上传', pubkey: '密钥信息', + pubKeyHelper: '当前密钥信息仅对用户 {0} 生效', encryptionMode: '加密方式', passwordHelper: '支持大小写英文、数字,长度6-10', - generate: '生成密钥', reGenerate: '重新生成密钥', keyAuthHelper: '是否启用密钥认证,默认启用。', useDNS: '反向解析', diff --git a/frontend/src/views/host/ssh/ssh/certification/index.vue b/frontend/src/views/host/ssh/ssh/certification/index.vue new file mode 100644 index 000000000..a1958b1ac --- /dev/null +++ b/frontend/src/views/host/ssh/ssh/certification/index.vue @@ -0,0 +1,476 @@ + + + + diff --git a/frontend/src/views/host/ssh/ssh/index.vue b/frontend/src/views/host/ssh/ssh/index.vue index e8d97fbed..bd7f37d0c 100644 --- a/frontend/src/views/host/ssh/ssh/index.vue +++ b/frontend/src/views/host/ssh/ssh/index.vue @@ -43,6 +43,8 @@ {{ $t('database.baseConf') }} {{ $t('database.allConf') }} + + {{ $t('ssh.pubkey') }} @@ -93,9 +95,6 @@ v-model="form.pubkeyAuthentication" > {{ $t('ssh.keyAuthHelper') }} - - {{ $t('ssh.pubkey') }} - - +
@@ -135,7 +134,7 @@ - -