Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ HTTP_TLS_KEY_FILE=

# Logger
LOG_LEVEL=info
GIN_MODE=release

# Remote Secret Store (Vault)
SECRET_ADDR=http://localhost:8200
Expand All @@ -32,6 +33,12 @@ SECRET_TOKEN=
DB_POOL_MAX=2
DB_PROVIDER=
DB_URL=
POSTGRES_USER=postgresadmin
POSTGRES_PASSWORD=
POSTGRES_DB=rpsdb
MONGO_INITDB_DATABASE=consoledb
MONGO_INITDB_ROOT_USERNAME=mongoadmin
MONGO_INITDB_ROOT_PASSWORD=

# EA
EA_URL=http://localhost:8000
Expand All @@ -43,12 +50,13 @@ AUTH_DISABLED=false
AUTH_ADMIN_USERNAME=standalone
# AUTH_ADMIN_PASSWORD: If not set, a random password is generated
AUTH_ADMIN_PASSWORD=
AUTH_JWT_KEY=your_secret_jwt_key
# AUTH_JWT_KEY: If unset, a strong key is generated and saved on first run.
# If set, the value is used as-is (must be non-empty; set to empty will cause startup failure).
AUTH_JWT_KEY=
AUTH_JWT_EXPIRATION=24h
AUTH_REDIRECTION_JWT_EXPIRATION=5m
AUTH_CLIENT_ID=
AUTH_ISSUER=GIN_MODE=release
# DB_URL=postgres://postgresadmin:admin123@localhost:5432/rpsdb
# DB_URL=postgres://<POSTGRES_USER>:<POSTGRES_PASSWORD>@localhost:5432/<POSTGRES_DB>
# OAUTH CONFIGURATION
AUTH_CLIENT_ID=
# ex. "https://login.microsoftonline.com/<tenant-id>/v2.0 for Azure Entra -- used for discovery
Expand Down
114 changes: 110 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package config

import (
"crypto/rand"
"encoding/hex"
"errors"
"flag"
"log"
"net"
"os"
"path/filepath"
"strings"
"time"

"github.com/ilyakaznacheev/cleanenv"
Expand All @@ -17,7 +21,15 @@ var ConsoleConfig *Config
// TrayMode indicates whether to run with system tray UI.
var TrayMode bool

const defaultHost = "localhost"
const (
defaultHost = "localhost"
hs256RecommendedMinKeyBytes = 32
)

// ErrEmptyJWTKeyEnv is returned when AUTH_JWT_KEY is set in the environment but
// has an empty or whitespace-only value. Operators must either provide a non-empty
// key or leave the variable unset so Console can auto-generate one.
var ErrEmptyJWTKeyEnv = errors.New("AUTH_JWT_KEY is set but empty; provide a non-empty key or unset the variable to auto-generate")

type (
// Config -.
Expand Down Expand Up @@ -94,7 +106,7 @@ type (
Disabled bool `yaml:"disabled" env:"AUTH_DISABLED"`
AdminUsername string `yaml:"adminUsername" env:"AUTH_ADMIN_USERNAME"`
AdminPassword string `yaml:"adminPassword" env:"AUTH_ADMIN_PASSWORD"`
JWTKey string `env-required:"true" yaml:"jwtKey" env:"AUTH_JWT_KEY"`
JWTKey string `yaml:"jwtKey" env:"AUTH_JWT_KEY"`
JWTExpiration time.Duration `yaml:"jwtExpiration" env:"AUTH_JWT_EXPIRATION"`
RedirectionJWTExpiration time.Duration `yaml:"redirectionJWTExpiration" env:"AUTH_REDIRECTION_JWT_EXPIRATION"`
ClientID string `yaml:"clientId" env:"AUTH_CLIENT_ID"`
Expand Down Expand Up @@ -187,7 +199,7 @@ func defaultConfig() *Config {
Auth: Auth{
AdminUsername: "standalone",
AdminPassword: "", // Generated and stored in config on first run if not provided
JWTKey: "your_secret_jwt_key",
JWTKey: "", // Generated and stored in config on first run if not provided
JWTExpiration: 24 * time.Hour,
RedirectionJWTExpiration: 5 * time.Minute,
// OAUTH CONFIG, if provided will not use basic auth
Expand Down Expand Up @@ -236,17 +248,42 @@ func resolveConfigPath(configPathFlag string) (string, error) {
func readOrInitConfig(configPath string, cfg *Config) error {
err := cleanenv.ReadConfig(configPath, cfg)
if err == nil {
return nil
if cfg.JWTKey != "" {
return nil
}

if hasNonEmptyJWTKeyEnv() {
return nil
}

jwtKey, genErr := generateAndSetJWTKey(cfg)
if genErr != nil {
return genErr
}

return saveJWTKey(configPath, jwtKey)
}

var pathErr *os.PathError
if errors.As(err, &pathErr) {
if !hasNonEmptyJWTKeyEnv() {
if _, genErr := generateAndSetJWTKey(cfg); genErr != nil {
return genErr
}
}

return writeConfig(configPath, cfg)
}

return err
}

func hasNonEmptyJWTKeyEnv() bool {
jwtKeyEnv, isSetByEnv := os.LookupEnv("AUTH_JWT_KEY")

return isSetByEnv && strings.TrimSpace(jwtKeyEnv) != ""
}

// writeConfig serializes cfg to configPath, creating the parent directory if needed.
func writeConfig(configPath string, cfg *Config) error {
configDir := filepath.Dir(configPath)
Expand Down Expand Up @@ -296,6 +333,69 @@ func SaveAdminPassword(adminPassword string) error {
return writeConfig(configPath, fileCfg)
}

func saveJWTKey(configPath, jwtKey string) error {
data, err := os.ReadFile(configPath)
if err != nil {
return err
}

fileCfg := defaultConfig()
if err := yaml.Unmarshal(data, fileCfg); err != nil {
return err
}

fileCfg.JWTKey = jwtKey

return writeConfig(configPath, fileCfg)
}

func generateJWTKey() (string, error) {
b := make([]byte, hs256RecommendedMinKeyBytes)
if _, err := rand.Read(b); err != nil {
return "", err
}

return hex.EncodeToString(b), nil
}

func generateAndSetJWTKey(cfg *Config) (string, error) {
jwtKey, err := generateJWTKey()
if err != nil {
return "", err
}

cfg.JWTKey = jwtKey

return jwtKey, nil
}

func warnIfWeakJWTKey(cfg *Config) {
keyLen := len([]byte(cfg.JWTKey))
if keyLen < hs256RecommendedMinKeyBytes {
log.Printf("WARNING: auth.jwtKey looks too short (%d bytes). Please use at least %d bytes for a strong key.", keyLen, hs256RecommendedMinKeyBytes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use log.Errorf or log.Faralf() to exit.

another way is possible warn and replace smaller key with 32 or more secure jwt key.

}
}

func ensureJWTKeyNotEmpty(cfg *Config, configPath string) error {
if strings.TrimSpace(cfg.JWTKey) != "" {
return nil
}

// If AUTH_JWT_KEY is explicitly set in the environment but empty, fail fast.
// Operators who opt in to env-driven config must supply a valid value.
jwtKeyEnv, isSetByEnv := os.LookupEnv("AUTH_JWT_KEY")
if isSetByEnv && strings.TrimSpace(jwtKeyEnv) == "" {
return ErrEmptyJWTKeyEnv
}

jwtKey, err := generateAndSetJWTKey(cfg)
if err != nil {
return err
}

return saveJWTKey(configPath, jwtKey)
}

// NewConfig returns app config.
func NewConfig() (*Config, error) {
// set defaults
Expand Down Expand Up @@ -329,5 +429,11 @@ func NewConfig() (*Config, error) {
return nil, err
}

if err := ensureJWTKeyNotEmpty(ConsoleConfig, configPath); err != nil {
return nil, err
}

warnIfWeakJWTKey(ConsoleConfig)

@sudhir-intc sudhir-intc Jun 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not exit if the key length is < 32 than instead of logging a warning.
For a customer or developer they would hardly notice the warning in the log and see the console coming up successfuly


return ConsoleConfig, nil
}
2 changes: 1 addition & 1 deletion config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ auth:
disabled: false
adminUsername: standalone
adminPassword:
jwtKey: your_secret_jwt_key
jwtKey:
jwtExpiration: 24h0m0s
redirectionJWTExpiration: 5m0s
clientId: ""
Expand Down
Loading
Loading