FEATURE (secrets): Move secrets to the secret.key file instead of DB

This commit is contained in:
Rostislav Dugin
2025-11-19 18:53:58 +03:00
parent 95c833b619
commit 244a56d1bb
18 changed files with 144 additions and 86 deletions

View File

@@ -18,6 +18,7 @@ import (
backups_config "postgresus-backend/internal/features/backups/config"
"postgresus-backend/internal/features/databases"
"postgresus-backend/internal/features/disk"
"postgresus-backend/internal/features/encryption/secrets"
healthcheck_attempt "postgresus-backend/internal/features/healthcheck/attempt"
healthcheck_config "postgresus-backend/internal/features/healthcheck/config"
"postgresus-backend/internal/features/notifiers"
@@ -64,6 +65,12 @@ func main() {
os.Exit(1)
}
err = secrets.GetSecretKeyService().MigrateKeyFromDbToFileIfExist()
if err != nil {
log.Error("Failed to migrate secret key from database to file", "error", err)
os.Exit(1)
}
err = users_services.GetUserService().CreateInitialAdmin()
if err != nil {
log.Error("Failed to create initial admin", "error", err)

View File

@@ -26,8 +26,9 @@ type EnvVariables struct {
EnvMode env_utils.EnvMode `env:"ENV_MODE" required:"true"`
PostgresesInstallDir string `env:"POSTGRES_INSTALL_DIR"`
DataFolder string
TempFolder string
DataFolder string
TempFolder string
SecretKeyPath string
TestGoogleDriveClientID string `env:"TEST_GOOGLE_DRIVE_CLIENT_ID"`
TestGoogleDriveClientSecret string `env:"TEST_GOOGLE_DRIVE_CLIENT_SECRET"`
@@ -146,6 +147,7 @@ func loadEnvVariables() {
// (projectRoot/postgresus-data -> /postgresus-data)
env.DataFolder = filepath.Join(filepath.Dir(backendRoot), "postgresus-data", "backups")
env.TempFolder = filepath.Join(filepath.Dir(backendRoot), "postgresus-data", "temp")
env.SecretKeyPath = filepath.Join(filepath.Dir(backendRoot), "postgresus-data", "secret.key")
if env.IsTesting {
if env.TestPostgres12Port == "" {

View File

@@ -1,17 +1,18 @@
package backups
import (
"time"
audit_logs "postgresus-backend/internal/features/audit_logs"
"postgresus-backend/internal/features/backups/backups/usecases"
backups_config "postgresus-backend/internal/features/backups/config"
"postgresus-backend/internal/features/databases"
encryption_secrets "postgresus-backend/internal/features/encryption/secrets"
"postgresus-backend/internal/features/notifiers"
"postgresus-backend/internal/features/storages"
users_repositories "postgresus-backend/internal/features/users/repositories"
workspaces_services "postgresus-backend/internal/features/workspaces/services"
"postgresus-backend/internal/util/encryption"
"postgresus-backend/internal/util/logger"
"time"
)
var backupRepository = &BackupRepository{}
@@ -25,7 +26,7 @@ var backupService = &BackupService{
notifiers.GetNotifierService(),
notifiers.GetNotifierService(),
backups_config.GetBackupConfigService(),
users_repositories.GetSecretKeyRepository(),
encryption_secrets.GetSecretKeyService(),
encryption.GetFieldEncryptor(),
usecases.GetCreateBackupUsecase(),
logger.GetLogger(),

View File

@@ -7,19 +7,20 @@ import (
"fmt"
"io"
"log/slog"
"slices"
"strings"
"time"
audit_logs "postgresus-backend/internal/features/audit_logs"
"postgresus-backend/internal/features/backups/backups/encryption"
backups_config "postgresus-backend/internal/features/backups/config"
"postgresus-backend/internal/features/databases"
encryption_secrets "postgresus-backend/internal/features/encryption/secrets"
"postgresus-backend/internal/features/notifiers"
"postgresus-backend/internal/features/storages"
users_models "postgresus-backend/internal/features/users/models"
users_repositories "postgresus-backend/internal/features/users/repositories"
workspaces_services "postgresus-backend/internal/features/workspaces/services"
util_encryption "postgresus-backend/internal/util/encryption"
"slices"
"strings"
"time"
"github.com/google/uuid"
)
@@ -31,7 +32,7 @@ type BackupService struct {
notifierService *notifiers.NotifierService
notificationSender NotificationSender
backupConfigService *backups_config.BackupConfigService
secretKeyRepo *users_repositories.SecretKeyRepository
secretKeyService *encryption_secrets.SecretKeyService
fieldEncryptor util_encryption.FieldEncryptor
createBackupUseCase CreateBackupUsecase
@@ -628,7 +629,7 @@ func (s *BackupService) getBackupReader(backupID uuid.UUID) (io.ReadCloser, erro
}
// Get master key
masterKey, err := s.secretKeyRepo.GetSecretKey()
masterKey, err := s.secretKeyService.GetSecretKey()
if err != nil {
if closeErr := fileReader.Close(); closeErr != nil {
s.logger.Error("Failed to close file reader", "error", closeErr)

View File

@@ -3,21 +3,22 @@ package backups
import (
"context"
"errors"
"strings"
"testing"
"time"
usecases_postgresql "postgresus-backend/internal/features/backups/backups/usecases/postgresql"
backups_config "postgresus-backend/internal/features/backups/config"
"postgresus-backend/internal/features/databases"
encryption_secrets "postgresus-backend/internal/features/encryption/secrets"
"postgresus-backend/internal/features/notifiers"
"postgresus-backend/internal/features/storages"
users_enums "postgresus-backend/internal/features/users/enums"
users_repositories "postgresus-backend/internal/features/users/repositories"
users_testing "postgresus-backend/internal/features/users/testing"
workspaces_services "postgresus-backend/internal/features/workspaces/services"
workspaces_testing "postgresus-backend/internal/features/workspaces/testing"
"postgresus-backend/internal/util/encryption"
"postgresus-backend/internal/util/logger"
"strings"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
@@ -56,7 +57,7 @@ func Test_BackupExecuted_NotificationSent(t *testing.T) {
notifiers.GetNotifierService(),
mockNotificationSender,
backups_config.GetBackupConfigService(),
users_repositories.GetSecretKeyRepository(),
encryption_secrets.GetSecretKeyService(),
encryption.GetFieldEncryptor(),
&CreateFailedBackupUsecase{},
logger.GetLogger(),
@@ -104,7 +105,7 @@ func Test_BackupExecuted_NotificationSent(t *testing.T) {
notifiers.GetNotifierService(),
mockNotificationSender,
backups_config.GetBackupConfigService(),
users_repositories.GetSecretKeyRepository(),
encryption_secrets.GetSecretKeyService(),
encryption.GetFieldEncryptor(),
&CreateSuccessBackupUsecase{},
logger.GetLogger(),
@@ -129,7 +130,7 @@ func Test_BackupExecuted_NotificationSent(t *testing.T) {
notifiers.GetNotifierService(),
mockNotificationSender,
backups_config.GetBackupConfigService(),
users_repositories.GetSecretKeyRepository(),
encryption_secrets.GetSecretKeyService(),
encryption.GetFieldEncryptor(),
&CreateSuccessBackupUsecase{},
logger.GetLogger(),

View File

@@ -19,8 +19,8 @@ import (
backups_config "postgresus-backend/internal/features/backups/config"
"postgresus-backend/internal/features/databases"
pgtypes "postgresus-backend/internal/features/databases/databases/postgresql"
encryption_secrets "postgresus-backend/internal/features/encryption/secrets"
"postgresus-backend/internal/features/storages"
users_repositories "postgresus-backend/internal/features/users/repositories"
"postgresus-backend/internal/util/encryption"
"postgresus-backend/internal/util/tools"
@@ -34,16 +34,15 @@ const (
progressReportIntervalMB = 1.0
pgConnectTimeout = 30
compressionLevel = 5
defaultBackupLimit = 1000
exitCodeAccessViolation = -1073741819
exitCodeGenericError = 1
exitCodeConnectionError = 2
)
type CreatePostgresqlBackupUsecase struct {
logger *slog.Logger
secretKeyRepo *users_repositories.SecretKeyRepository
fieldEncryptor encryption.FieldEncryptor
logger *slog.Logger
secretKeyService *encryption_secrets.SecretKeyService
fieldEncryptor encryption.FieldEncryptor
}
// Execute creates a backup of the database
@@ -466,7 +465,7 @@ func (uc *CreatePostgresqlBackupUsecase) setupBackupEncryption(
return nil, nil, metadata, fmt.Errorf("failed to generate nonce: %w", err)
}
masterKey, err := uc.secretKeyRepo.GetSecretKey()
masterKey, err := uc.secretKeyService.GetSecretKey()
if err != nil {
return nil, nil, metadata, fmt.Errorf("failed to get master key: %w", err)
}

View File

@@ -1,14 +1,14 @@
package usecases_postgresql
import (
users_repositories "postgresus-backend/internal/features/users/repositories"
"postgresus-backend/internal/features/encryption/secrets"
"postgresus-backend/internal/util/encryption"
"postgresus-backend/internal/util/logger"
)
var createPostgresqlBackupUsecase = &CreatePostgresqlBackupUsecase{
logger.GetLogger(),
users_repositories.GetSecretKeyRepository(),
secrets.GetSecretKeyService(),
encryption.GetFieldEncryptor(),
}

View File

@@ -0,0 +1,9 @@
package secrets
var secretKeyService = &SecretKeyService{
nil,
}
func GetSecretKeyService() *SecretKeyService {
return secretKeyService
}

View File

@@ -0,0 +1 @@
package secrets

View File

@@ -0,0 +1,73 @@
package secrets
import (
"errors"
"fmt"
"os"
"postgresus-backend/internal/config"
user_models "postgresus-backend/internal/features/users/models"
"postgresus-backend/internal/storage"
"github.com/google/uuid"
"gorm.io/gorm"
)
type SecretKeyService struct {
cachedKey *string
}
func (s *SecretKeyService) MigrateKeyFromDbToFileIfExist() error {
var secretKey user_models.SecretKey
err := storage.GetDb().First(&secretKey).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return fmt.Errorf("failed to check for secret key in database: %w", err)
}
if secretKey.Secret == "" {
return nil
}
secretKeyPath := config.GetEnv().SecretKeyPath
if err := os.WriteFile(secretKeyPath, []byte(secretKey.Secret), 0600); err != nil {
return fmt.Errorf("failed to write secret key to file: %w", err)
}
if err := storage.GetDb().Exec("DELETE FROM secret_keys").Error; err != nil {
return fmt.Errorf("failed to delete secret key from database: %w", err)
}
return nil
}
func (s *SecretKeyService) GetSecretKey() (string, error) {
if s.cachedKey != nil {
return *s.cachedKey, nil
}
secretKeyPath := config.GetEnv().SecretKeyPath
data, err := os.ReadFile(secretKeyPath)
if err != nil {
if os.IsNotExist(err) {
newKey := s.generateNewSecretKey()
if err := os.WriteFile(secretKeyPath, []byte(newKey), 0600); err != nil {
return "", fmt.Errorf("failed to write new secret key: %w", err)
}
s.cachedKey = &newKey
return newKey, nil
}
return "", fmt.Errorf("failed to read secret key file: %w", err)
}
key := string(data)
s.cachedKey = &key
return key, nil
}
func (s *SecretKeyService) generateNewSecretKey() string {
return uuid.New().String() + uuid.New().String()
}

View File

@@ -1,13 +1,13 @@
package usecases_postgresql
import (
users_repositories "postgresus-backend/internal/features/users/repositories"
"postgresus-backend/internal/features/encryption/secrets"
"postgresus-backend/internal/util/logger"
)
var restorePostgresqlBackupUsecase = &RestorePostgresqlBackupUsecase{
logger.GetLogger(),
users_repositories.GetSecretKeyRepository(),
secrets.GetSecretKeyService(),
}
func GetRestorePostgresqlBackupUsecase() *RestorePostgresqlBackupUsecase {

View File

@@ -20,9 +20,9 @@ import (
backups_config "postgresus-backend/internal/features/backups/config"
"postgresus-backend/internal/features/databases"
pgtypes "postgresus-backend/internal/features/databases/databases/postgresql"
encryption_secrets "postgresus-backend/internal/features/encryption/secrets"
"postgresus-backend/internal/features/restores/models"
"postgresus-backend/internal/features/storages"
users_repositories "postgresus-backend/internal/features/users/repositories"
util_encryption "postgresus-backend/internal/util/encryption"
files_utils "postgresus-backend/internal/util/files"
"postgresus-backend/internal/util/tools"
@@ -31,8 +31,8 @@ import (
)
type RestorePostgresqlBackupUsecase struct {
logger *slog.Logger
secretKeyRepo *users_repositories.SecretKeyRepository
logger *slog.Logger
secretKeyService *encryption_secrets.SecretKeyService
}
func (uc *RestorePostgresqlBackupUsecase) Execute(
@@ -232,7 +232,7 @@ func (uc *RestorePostgresqlBackupUsecase) downloadBackupToTempFile(
}
// Get master key
masterKey, err := uc.secretKeyRepo.GetSecretKey()
masterKey, err := uc.secretKeyService.GetSecretKey()
if err != nil {
cleanupFunc()
return "", nil, fmt.Errorf("failed to get master key for decryption: %w", err)

View File

@@ -1,13 +1,8 @@
package users_repositories
var secretKeyRepository = &SecretKeyRepository{}
var userRepository = &UserRepository{}
var usersSettingsRepository = &UsersSettingsRepository{}
func GetSecretKeyRepository() *SecretKeyRepository {
return secretKeyRepository
}
func GetUserRepository() *UserRepository {
return userRepository
}

View File

@@ -1,34 +0,0 @@
package users_repositories
import (
"errors"
user_models "postgresus-backend/internal/features/users/models"
"postgresus-backend/internal/storage"
"github.com/google/uuid"
"gorm.io/gorm"
)
type SecretKeyRepository struct{}
func (r *SecretKeyRepository) GetSecretKey() (string, error) {
var secretKey user_models.SecretKey
if err := storage.GetDb().First(&secretKey).Error; err != nil {
// create a new secret key if not found
if errors.Is(err, gorm.ErrRecordNotFound) {
newSecretKey := user_models.SecretKey{
Secret: uuid.New().String() + uuid.New().String(),
}
if err := storage.GetDb().Create(&newSecretKey).Error; err != nil {
return "", errors.New("failed to create new secret key")
}
return newSecretKey.Secret, nil
}
return "", err
}
return secretKey.Secret, nil
}

View File

@@ -1,10 +1,13 @@
package users_services
import users_repositories "postgresus-backend/internal/features/users/repositories"
import (
"postgresus-backend/internal/features/encryption/secrets"
users_repositories "postgresus-backend/internal/features/users/repositories"
)
var userService = &UserService{
users_repositories.GetUserRepository(),
users_repositories.GetSecretKeyRepository(),
secrets.GetSecretKeyService(),
settingsService,
nil,
}

View File

@@ -17,6 +17,7 @@ import (
"golang.org/x/oauth2/google"
"postgresus-backend/internal/config"
"postgresus-backend/internal/features/encryption/secrets"
users_dto "postgresus-backend/internal/features/users/dto"
users_enums "postgresus-backend/internal/features/users/enums"
users_interfaces "postgresus-backend/internal/features/users/interfaces"
@@ -25,10 +26,10 @@ import (
)
type UserService struct {
userRepository *users_repositories.UserRepository
secretKeyRepository *users_repositories.SecretKeyRepository
settingsService *SettingsService
auditLogWriter users_interfaces.AuditLogWriter
userRepository *users_repositories.UserRepository
secretKeyService *secrets.SecretKeyService
settingsService *SettingsService
auditLogWriter users_interfaces.AuditLogWriter
}
func (s *UserService) SetAuditLogWriter(writer users_interfaces.AuditLogWriter) {
@@ -162,7 +163,7 @@ func (s *UserService) SignIn(
}
func (s *UserService) GetUserFromToken(token string) (*users_models.User, error) {
secretKey, err := s.secretKeyRepository.GetSecretKey()
secretKey, err := s.secretKeyService.GetSecretKey()
if err != nil {
return nil, fmt.Errorf("failed to get secret key: %w", err)
}
@@ -221,7 +222,7 @@ func (s *UserService) GetUserFromToken(token string) (*users_models.User, error)
func (s *UserService) GenerateAccessToken(
user *users_models.User,
) (*users_dto.SignInResponseDTO, error) {
secretKey, err := s.secretKeyRepository.GetSecretKey()
secretKey, err := s.secretKeyService.GetSecretKey()
if err != nil {
return nil, fmt.Errorf("failed to get secret key: %w", err)
}

View File

@@ -1,9 +1,9 @@
package encryption
import users_repositories "postgresus-backend/internal/features/users/repositories"
import "postgresus-backend/internal/features/encryption/secrets"
var fieldEncryptor = &SecretKeyFieldEncryptor{
users_repositories.GetSecretKeyRepository(),
secrets.GetSecretKeyService(),
}
func GetFieldEncryptor() FieldEncryptor {

View File

@@ -8,17 +8,16 @@ import (
"encoding/base64"
"errors"
"fmt"
"postgresus-backend/internal/features/encryption/secrets"
"strings"
users_repositories "postgresus-backend/internal/features/users/repositories"
"github.com/google/uuid"
)
const encryptedPrefix = "enc:"
type SecretKeyFieldEncryptor struct {
secretKeyRepository *users_repositories.SecretKeyRepository
secretKeyService *secrets.SecretKeyService
}
func (e *SecretKeyFieldEncryptor) Encrypt(itemID uuid.UUID, plaintext string) (string, error) {
@@ -30,7 +29,7 @@ func (e *SecretKeyFieldEncryptor) Encrypt(itemID uuid.UUID, plaintext string) (s
return plaintext, nil
}
masterKey, err := e.secretKeyRepository.GetSecretKey()
masterKey, err := e.secretKeyService.GetSecretKey()
if err != nil {
return "", fmt.Errorf("failed to get master key: %w", err)
}
@@ -82,7 +81,7 @@ func (e *SecretKeyFieldEncryptor) Decrypt(itemID uuid.UUID, ciphertext string) (
return "", fmt.Errorf("failed to decode ciphertext: %w", err)
}
masterKey, err := e.secretKeyRepository.GetSecretKey()
masterKey, err := e.secretKeyService.GetSecretKey()
if err != nil {
return "", fmt.Errorf("failed to get master key: %w", err)
}