From dce898bbdb5927be1c1d1e6d8c9ec6fae70c383a Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 29 Dec 2025 16:12:30 +0700 Subject: [PATCH] Auto-generate and persist session secret on first startup - Generate random 32-byte session secret if not set via env var - Persist to $UPAAS_DATA_DIR/session.key for container restarts - Load existing secret from file on subsequent startups - Change container data directory to /var/lib/upaas --- Dockerfile | 4 +-- README.md | 4 ++- internal/config/config.go | 71 +++++++++++++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index a3c6c1d..8ccfb27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,9 +29,9 @@ WORKDIR /app COPY --from=builder /src/bin/upaasd /app/upaasd # Create data directory -RUN mkdir -p /data +RUN mkdir -p /var/lib/upaas -ENV UPAAS_DATA_DIR=/data +ENV UPAAS_DATA_DIR=/var/lib/upaas ENV UPAAS_PORT=8080 EXPOSE 8080 diff --git a/README.md b/README.md index e736af6..dd22758 100644 --- a/README.md +++ b/README.md @@ -170,10 +170,12 @@ Environment variables: docker run -d \ -p 8080:8080 \ -v /var/run/docker.sock:/var/run/docker.sock \ - -v upaas-data:/data \ + -v upaas-data:/var/lib/upaas \ upaas ``` +Session secrets are automatically generated on first startup and persisted to `$UPAAS_DATA_DIR/session.key`. + ## License MIT diff --git a/internal/config/config.go b/internal/config/config.go index 0607ead..4881983 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,9 +2,13 @@ package config import ( + "crypto/rand" + "encoding/hex" "errors" "fmt" "log/slog" + "os" + "path/filepath" "github.com/spf13/viper" "go.uber.org/fx" @@ -16,6 +20,18 @@ import ( // defaultPort is the default HTTP server port. const defaultPort = 8080 +// sessionSecretFile is the filename for the persisted session secret. +const sessionSecretFile = "session.key" + +// sessionSecretBytes is the number of random bytes for session secret. +const sessionSecretBytes = 32 + +// File permission constants. +const ( + dirPermissions = 0o700 + filePermissions = 0o600 +) + // Params contains dependencies for Config. type Params struct { fx.In @@ -112,19 +128,62 @@ func buildConfig(log *slog.Logger, params *Params) (*Config, error) { log: log, } - // Generate session secret if not set + // Load or generate session secret if cfg.SessionSecret == "" { - cfg.SessionSecret = "change-me-in-production-please" + secret, err := loadOrCreateSessionSecret(log, cfg.DataDir) + if err != nil { + return nil, fmt.Errorf("failed to initialize session secret: %w", err) + } - log.Warn( - "using default session secret, " + - "set UPAAS_SESSION_SECRET in production", - ) + cfg.SessionSecret = secret } return cfg, nil } +func loadOrCreateSessionSecret(log *slog.Logger, dataDir string) (string, error) { + secretPath := filepath.Join(dataDir, sessionSecretFile) + + // Try to read existing secret + //nolint:gosec // secretPath is constructed from trusted config, not user input + data, err := os.ReadFile(secretPath) + if err == nil { + log.Info("loaded session secret from file", "path", secretPath) + + return string(data), nil + } + + if !os.IsNotExist(err) { + return "", fmt.Errorf("failed to read session secret file: %w", err) + } + + // Generate new secret + secretBytes := make([]byte, sessionSecretBytes) + + _, err = rand.Read(secretBytes) + if err != nil { + return "", fmt.Errorf("failed to generate random secret: %w", err) + } + + secret := hex.EncodeToString(secretBytes) + + // Ensure data directory exists + err = os.MkdirAll(dataDir, dirPermissions) + if err != nil { + return "", fmt.Errorf("failed to create data directory: %w", err) + } + + // Write secret to file + err = os.WriteFile(secretPath, []byte(secret), filePermissions) + if err != nil { + return "", fmt.Errorf("failed to write session secret file: %w", err) + } + + log.Info("generated new session secret", "path", secretPath) + + return secret, nil +} + func configureDebugLogging(cfg *Config, params Params) { // Enable debug logging if configured if cfg.Debug {