refactor: auto-generate session key and store in database
All checks were successful
check / check (push) Successful in 57s

Remove SESSION_KEY env var requirement. On first startup, a
cryptographically secure 32-byte key is generated and stored in a new
settings table. Subsequent startups load the key from the database.

- Add Setting model (key-value table) for application config
- Add Database.GetOrCreateSessionKey() method
- Session manager initializes in OnStart after database is connected
- Remove DevSessionKey constant and SESSION_KEY env var handling
- Remove prod validation requiring SESSION_KEY
- Update README: config table, Docker instructions, security notes
- Update config.yaml.example
- Update all tests to remove SessionKey references

Addresses owner feedback on issue #15.
This commit is contained in:
2026-03-01 21:57:19 -08:00
parent 5e683af2a4
commit 9b9ee1718a
11 changed files with 131 additions and 218 deletions

View File

@@ -2,7 +2,11 @@ package database
import (
"context"
"crypto/rand"
"database/sql"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"go.uber.org/fx"
@@ -142,3 +146,35 @@ func (d *Database) close() error {
func (d *Database) DB() *gorm.DB {
return d.db
}
// GetOrCreateSessionKey retrieves the session encryption key from the
// settings table. If no key exists, a cryptographically secure random
// 32-byte key is generated, base64-encoded, and stored for future use.
func (d *Database) GetOrCreateSessionKey() (string, error) {
var setting Setting
result := d.db.Where(&Setting{Key: "session_key"}).First(&setting)
if result.Error == nil {
return setting.Value, nil
}
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return "", fmt.Errorf("failed to query session key: %w", result.Error)
}
// Generate a new cryptographically secure 32-byte key
keyBytes := make([]byte, 32)
if _, err := rand.Read(keyBytes); err != nil {
return "", fmt.Errorf("failed to generate session key: %w", err)
}
encoded := base64.StdEncoding.EncodeToString(keyBytes)
setting = Setting{
Key: "session_key",
Value: encoded,
}
if err := d.db.Create(&setting).Error; err != nil {
return "", fmt.Errorf("failed to store session key: %w", err)
}
d.log.Info("generated new session key and stored in database")
return encoded, nil
}

View File

@@ -26,7 +26,6 @@ environments:
environment: dev
dburl: "file::memory:?cache=shared"
secrets:
sessionKey: d2ViaG9va2VyLWRldi1zZXNzaW9uLWtleS1pbnNlY3VyZSE=
sentryDSN: ""
configDefaults:
port: 8080

View File

@@ -0,0 +1,8 @@
package database
// Setting stores application-level key-value configuration.
// Used for auto-generated values like the session encryption key.
type Setting struct {
Key string `gorm:"primaryKey" json:"key"`
Value string `gorm:"type:text;not null" json:"value"`
}

View File

@@ -6,6 +6,7 @@ package database
// per-webhook dedicated databases managed by WebhookDBManager.
func (d *Database) Migrate() error {
return d.db.AutoMigrate(
&Setting{},
&User{},
&APIKey{},
&Webhook{},

View File

@@ -28,8 +28,6 @@ environments:
port: 8080
debug: false
dburl: "file::memory:?cache=shared"
secrets:
sessionKey: d2ViaG9va2VyLWRldi1zZXNzaW9uLWtleS1pbnNlY3VyZSE=
configDefaults:
port: 8080
`
@@ -51,9 +49,8 @@ configDefaults:
dataDir := filepath.Join(t.TempDir(), "events")
cfg := &config.Config{
DBURL: "file::memory:?cache=shared",
DataDir: dataDir,
SessionKey: "d2ViaG9va2VyLWRldi1zZXNzaW9uLWtleS1pbnNlY3VyZSE=",
DBURL: "file::memory:?cache=shared",
DataDir: dataDir,
}
_ = cfg