mirror of
https://github.com/veops/oneterm.git
synced 2026-09-03 07:25:15 +08:00
feat(backend): store SSH private key encrypted in database instead of config file
This commit is contained in:
@@ -1,63 +0,0 @@
|
||||
mode: debug
|
||||
|
||||
http:
|
||||
host: 0.0.0.0
|
||||
port: 8888
|
||||
|
||||
ssh:
|
||||
host: 0.0.0.0
|
||||
port: 2222
|
||||
privateKey: --BEGIN PRIVATE KEY-----END PRIVATE KEY-----
|
||||
|
||||
guacd:
|
||||
host: oneterm-guacd
|
||||
port: 4822
|
||||
|
||||
mysql:
|
||||
host: oneterm-mysql
|
||||
port: 3306
|
||||
user: root
|
||||
password: root
|
||||
|
||||
database:
|
||||
type: mysql # alternative: postgres, tidb, tdsql, dm
|
||||
host: oneterm-mysql
|
||||
port: 3306
|
||||
user: root
|
||||
password: root
|
||||
database: oneterm
|
||||
charset: utf8mb4
|
||||
max_idle_conns: 10
|
||||
max_open_conns: 100
|
||||
conn_max_lifetime: 3600 # seconds
|
||||
conn_max_idle_time: 600 # seconds
|
||||
ssl_mode: disable
|
||||
|
||||
redis:
|
||||
addr: oneterm-redis:6379
|
||||
password: root
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: json
|
||||
maxSize: 1
|
||||
consoleEnable: true
|
||||
|
||||
auth:
|
||||
acl:
|
||||
appId: acl app id
|
||||
secretKey: acl app secret key
|
||||
url: http://host/api/v1
|
||||
resourceNames:
|
||||
- key: account
|
||||
value: account
|
||||
- key: asset
|
||||
value: asset
|
||||
- key: command
|
||||
value: command
|
||||
- key: gateway
|
||||
value: gateway
|
||||
- key: authorization
|
||||
value: authorization
|
||||
|
||||
secretKey: acl secret key
|
||||
@@ -34,7 +34,7 @@ func initDB() {
|
||||
model.DefaultGateway, model.DefaultHistory, model.DefaultNode, model.DefaultPublicKey,
|
||||
model.DefaultSession, model.DefaultSessionCmd, model.DefaultShare, model.DefaultQuickCommand,
|
||||
model.DefaultUserPreference, model.DefaultStorageConfig, model.DefaultStorageMetrics,
|
||||
model.DefaultTimeTemplate, model.DefaultMigrationRecord,
|
||||
model.DefaultTimeTemplate, model.DefaultMigrationRecord, model.DefaultSystemConfig,
|
||||
); err != nil {
|
||||
logger.L().Fatal("Failed to init database", zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -20,4 +20,5 @@ var (
|
||||
DefaultStorageConfig = &StorageConfig{}
|
||||
DefaultStorageMetrics = &StorageMetrics{}
|
||||
DefaultMigrationRecord = &MigrationRecord{}
|
||||
DefaultSystemConfig = &SystemConfig{}
|
||||
)
|
||||
|
||||
30
backend/internal/model/system_config.go
Normal file
30
backend/internal/model/system_config.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
// SystemConfig stores sensitive system-level configurations
|
||||
// This model is for internal use only and should never be exposed via API
|
||||
type SystemConfig struct {
|
||||
Id int `json:"id" gorm:"column:id;primarykey;autoIncrement"`
|
||||
Key string `json:"key" gorm:"column:config_key;size:191;uniqueIndex;not null"`
|
||||
Value string `json:"value" gorm:"column:value;type:text"`
|
||||
|
||||
CreatorId int `json:"creator_id" gorm:"column:creator_id"`
|
||||
UpdaterId int `json:"updater_id" gorm:"column:updater_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at"`
|
||||
DeletedAt soft_delete.DeletedAt `json:"-" gorm:"column:deleted_at"`
|
||||
}
|
||||
|
||||
func (m *SystemConfig) TableName() string {
|
||||
return "system_config"
|
||||
}
|
||||
|
||||
// System config key constants
|
||||
const (
|
||||
SysConfigSSHPrivateKey = "ssh_private_key"
|
||||
)
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
kFmtAssetIds = "assetIds-%d"
|
||||
kAuthorizationIds = "authorizationIds"
|
||||
kNodeIds = "nodeIds"
|
||||
kAccountIds = "accountIds"
|
||||
|
||||
44
backend/internal/repository/system_config.go
Normal file
44
backend/internal/repository/system_config.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/veops/oneterm/internal/model"
|
||||
dbpkg "github.com/veops/oneterm/pkg/db"
|
||||
)
|
||||
|
||||
// SystemConfigRepository interface for system config data access
|
||||
type SystemConfigRepository interface {
|
||||
GetByKey(ctx context.Context, key string) (*model.SystemConfig, error)
|
||||
SetByKey(ctx context.Context, key, value string) error
|
||||
}
|
||||
|
||||
// systemConfigRepository implements SystemConfigRepository
|
||||
type systemConfigRepository struct{}
|
||||
|
||||
// NewSystemConfigRepository creates a new system config repository
|
||||
func NewSystemConfigRepository() SystemConfigRepository {
|
||||
return &systemConfigRepository{}
|
||||
}
|
||||
|
||||
// GetByKey gets system config by key
|
||||
func (r *systemConfigRepository) GetByKey(ctx context.Context, key string) (*model.SystemConfig, error) {
|
||||
var config model.SystemConfig
|
||||
err := dbpkg.DB.WithContext(ctx).Where("config_key = ?", key).First(&config).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// SetByKey sets system config by key
|
||||
func (r *systemConfigRepository) SetByKey(ctx context.Context, key, value string) error {
|
||||
config := model.SystemConfig{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
return dbpkg.DB.WithContext(ctx).Where("config_key = ?", key).
|
||||
Assign(model.SystemConfig{Value: value}).
|
||||
FirstOrCreate(&config).Error
|
||||
}
|
||||
128
backend/internal/service/system_config.go
Normal file
128
backend/internal/service/system_config.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/veops/oneterm/internal/model"
|
||||
"github.com/veops/oneterm/internal/repository"
|
||||
"github.com/veops/oneterm/pkg/utils"
|
||||
)
|
||||
|
||||
type SystemConfigService struct {
|
||||
repo repository.SystemConfigRepository
|
||||
}
|
||||
|
||||
func NewSystemConfigService() *SystemConfigService {
|
||||
return &SystemConfigService{
|
||||
repo: repository.NewSystemConfigRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetSSHPrivateKey gets SSH private key from database and decrypts it
|
||||
func (s *SystemConfigService) GetSSHPrivateKey() (string, error) {
|
||||
ctx := context.Background()
|
||||
config, err := s.repo.GetByKey(ctx, model.SysConfigSSHPrivateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Decrypt the private key
|
||||
return utils.DecryptAES(config.Value), nil
|
||||
}
|
||||
|
||||
// SetSSHPrivateKey encrypts and sets SSH private key to database
|
||||
func (s *SystemConfigService) SetSSHPrivateKey(privateKey string) error {
|
||||
// Encrypt the private key before storing
|
||||
encryptedPrivateKey := utils.EncryptAES(privateKey)
|
||||
ctx := context.Background()
|
||||
return s.repo.SetByKey(ctx, model.SysConfigSSHPrivateKey, encryptedPrivateKey)
|
||||
}
|
||||
|
||||
// GenerateSSHKeyPair generates a new ED25519 SSH key pair
|
||||
func (s *SystemConfigService) GenerateSSHKeyPair() (privateKey string, publicKey string, err error) {
|
||||
// Generate ED25519 key pair
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to generate ED25519 key: %w", err)
|
||||
}
|
||||
|
||||
// Convert to SSH private key format
|
||||
sshPrivKey, err := ssh.MarshalPrivateKey(privKey, "")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to marshal private key: %w", err)
|
||||
}
|
||||
|
||||
// Encode private key to PEM format
|
||||
privateKeyPEM := pem.EncodeToMemory(sshPrivKey)
|
||||
|
||||
// Convert to SSH public key format
|
||||
sshPubKey, err := ssh.NewPublicKey(pubKey)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to create SSH public key: %w", err)
|
||||
}
|
||||
|
||||
publicKeyStr := string(ssh.MarshalAuthorizedKey(sshPubKey))
|
||||
|
||||
return string(privateKeyPEM), strings.TrimSpace(publicKeyStr), nil
|
||||
}
|
||||
|
||||
// EnsureSSHPrivateKey ensures SSH private key exists, generates one if not
|
||||
func (s *SystemConfigService) EnsureSSHPrivateKey() (string, error) {
|
||||
// Try to get existing key from database
|
||||
privateKey, err := s.GetSSHPrivateKey()
|
||||
if err == nil && privateKey != "" {
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// If not found or error, check if it's a "not found" error
|
||||
if err != nil && !strings.Contains(err.Error(), "record not found") {
|
||||
return "", fmt.Errorf("failed to query SSH private key: %w", err)
|
||||
}
|
||||
|
||||
// Generate new key pair
|
||||
privateKey, _, err = s.GenerateSSHKeyPair()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate SSH key pair: %w", err)
|
||||
}
|
||||
|
||||
// Save to database
|
||||
err = s.SetSSHPrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to save SSH private key: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// MigrateFromConfig migrates SSH private key from config file to database
|
||||
func (s *SystemConfigService) MigrateFromConfig(configPrivateKey string) error {
|
||||
if configPrivateKey == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if database already has a key
|
||||
_, err := s.GetSSHPrivateKey()
|
||||
if err == nil {
|
||||
// Key already exists in database, skip migration
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "record not found") {
|
||||
return fmt.Errorf("failed to check existing SSH private key: %w", err)
|
||||
}
|
||||
|
||||
// Validate the private key from config
|
||||
_, err = ssh.ParsePrivateKey([]byte(configPrivateKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid SSH private key in config: %w", err)
|
||||
}
|
||||
|
||||
// Save to database
|
||||
return s.SetSSHPrivateKey(configPrivateKey)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
@@ -17,8 +18,8 @@ import (
|
||||
|
||||
"github.com/veops/oneterm/internal/acl"
|
||||
"github.com/veops/oneterm/internal/model"
|
||||
"github.com/veops/oneterm/internal/service"
|
||||
"github.com/veops/oneterm/internal/version"
|
||||
"github.com/veops/oneterm/pkg/config"
|
||||
"github.com/veops/oneterm/pkg/logger"
|
||||
)
|
||||
|
||||
@@ -95,7 +96,34 @@ func handler(sess ssh.Session) {
|
||||
}
|
||||
|
||||
func signer() ssh.Signer {
|
||||
s, err := gossh.ParsePrivateKey([]byte(config.Cfg.Ssh.PrivateKey))
|
||||
sysConfigService := service.NewSystemConfigService()
|
||||
|
||||
// Retry logic to wait for database table creation
|
||||
var privateKey string
|
||||
var err error
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
privateKey, err = sysConfigService.EnsureSSHPrivateKey()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// If table doesn't exist, wait and retry
|
||||
if strings.Contains(err.Error(), "doesn't exist") {
|
||||
logger.L().Info("Waiting for database initialization...", zap.Int("attempt", i+1))
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// Other errors are fatal
|
||||
logger.L().Fatal("failed to ensure SSH private key", zap.Error(err))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.L().Fatal("failed to ensure SSH private key after retries", zap.Error(err))
|
||||
}
|
||||
|
||||
s, err := gossh.ParsePrivateKey([]byte(privateKey))
|
||||
if err != nil {
|
||||
logger.L().Fatal("failed parse signer", zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -32,11 +32,13 @@ func init() {
|
||||
ctx.SetValue("session", sess)
|
||||
return err == nil
|
||||
},
|
||||
HostSigners: []ssh.Signer{signer()},
|
||||
HostSigners: []ssh.Signer{},
|
||||
}
|
||||
}
|
||||
|
||||
func RunSsh() error {
|
||||
// Initialize host signer after database is ready
|
||||
server.HostSigners = []ssh.Signer{signer()}
|
||||
return server.ListenAndServe()
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ type Auth struct {
|
||||
type SshConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
PrivateKey string `yaml:"privateKey"`
|
||||
PrivateKey string `yaml:"privateKey,omitempty"` // Deprecated: now stored encrypted in database SystemConfig table
|
||||
}
|
||||
|
||||
type GuacdConfig struct {
|
||||
|
||||
Reference in New Issue
Block a user