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:
parent
5fb0b111fc
commit
dce898bbdb
@ -29,9 +29,9 @@ WORKDIR /app
|
|||||||
COPY --from=builder /src/bin/upaasd /app/upaasd
|
COPY --from=builder /src/bin/upaasd /app/upaasd
|
||||||
|
|
||||||
# Create data directory
|
# 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
|
ENV UPAAS_PORT=8080
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@ -170,10 +170,12 @@ Environment variables:
|
|||||||
docker run -d \
|
docker run -d \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
-v upaas-data:/data \
|
-v upaas-data:/var/lib/upaas \
|
||||||
upaas
|
upaas
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Session secrets are automatically generated on first startup and persisted to `$UPAAS_DATA_DIR/session.key`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
@ -2,9 +2,13 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
@ -16,6 +20,18 @@ import (
|
|||||||
// defaultPort is the default HTTP server port.
|
// defaultPort is the default HTTP server port.
|
||||||
const defaultPort = 8080
|
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.
|
// Params contains dependencies for Config.
|
||||||
type Params struct {
|
type Params struct {
|
||||||
fx.In
|
fx.In
|
||||||
@ -112,19 +128,62 @@ func buildConfig(log *slog.Logger, params *Params) (*Config, error) {
|
|||||||
log: log,
|
log: log,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate session secret if not set
|
// Load or generate session secret
|
||||||
if cfg.SessionSecret == "" {
|
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(
|
cfg.SessionSecret = secret
|
||||||
"using default session secret, " +
|
|
||||||
"set UPAAS_SESSION_SECRET in production",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg, nil
|
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) {
|
func configureDebugLogging(cfg *Config, params Params) {
|
||||||
// Enable debug logging if configured
|
// Enable debug logging if configured
|
||||||
if cfg.Debug {
|
if cfg.Debug {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user