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
This commit is contained in:
Jeffrey Paul 2025-12-29 16:12:30 +07:00
parent 5fb0b111fc
commit dce898bbdb
3 changed files with 70 additions and 9 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {