fix(apiserver): never sign or accept JWTs with the shipped default secret (bug-1945)
Cause: config/app.yaml shipped secretKey "xce-api-key" and resolveJWTSecret used it whenever XCE_JWT_SECRET was unset, so anyone could forge tokens for any user on such a deployment. Fix: resolveJWTSecret uses XCE_JWT_SECRET, else the config value, but treats an empty value or the old public default as unset; then it generates a random 256-bit per-process secret and InitConf logs a warning (secret never logged): tokens do not survive a restart and apiservers sharing one endpoint must set the same XCE_JWT_SECRET. The shipped app.yaml no longer carries a secret. The controllers test secret is no longer the old default. Tests: TestResolveJWTSecret_NeverUsesKnownDefaultOrEmpty, TestShippedAppYAML_NoJWTSecret, TestInitConf_NoJWTSecretGeneratesAndWarns, TestInitConf_JWTSecretFromEnvNoWarning. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c4c63f11e7
commit
19a1a6f023
4 changed files with 131 additions and 8 deletions
|
|
@ -1,6 +1,8 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
|
@ -164,10 +166,18 @@ func InitConf() error {
|
|||
AppVersion: viper.GetString("app.version"),
|
||||
}
|
||||
// parse authentication section
|
||||
configuredSecret := viper.GetString("authentication.token.secretKey")
|
||||
jwtSecret := resolveJWTSecret(configuredSecret)
|
||||
if jwtSecret != configuredSecret && jwtSecret != os.Getenv("XCE_JWT_SECRET") {
|
||||
Warn("No JWT secret configured (XCE_JWT_SECRET or authentication.token.secretKey), " +
|
||||
"or it is the publicly known default: using a random per-process secret. " +
|
||||
"Tokens do not survive an apiserver restart, and apiservers sharing one endpoint " +
|
||||
"must all set the same XCE_JWT_SECRET")
|
||||
}
|
||||
AuthenticationYamlConfig = &Authentication{
|
||||
Token: TokenConfig{
|
||||
Type: viper.GetString("authentication.token.type"),
|
||||
SecretKey: resolveJWTSecret(viper.GetString("authentication.token.secretKey")),
|
||||
SecretKey: jwtSecret,
|
||||
TokenExpireTime: int(viper.GetFloat64("authentication.token.token_expire_time")),
|
||||
},
|
||||
}
|
||||
|
|
@ -340,11 +350,31 @@ func resolveTrustedProxies(entries []string) ([]string, error) {
|
|||
return proxies, nil
|
||||
}
|
||||
|
||||
// resolveJWTSecret returns the JWT secret from XCE_JWT_SECRET env var if set and non-empty,
|
||||
// otherwise falls back to the value from config file.
|
||||
// knownDefaultJWTSecret is the secret older app.yaml files shipped with; it is
|
||||
// public, so it is treated as "no secret configured" (bug-1945).
|
||||
const knownDefaultJWTSecret = "xce-api-key"
|
||||
|
||||
// usableJWTSecret reports whether s may sign tokens: non-empty and not the
|
||||
// publicly known default.
|
||||
func usableJWTSecret(s string) bool {
|
||||
return s != "" && s != knownDefaultJWTSecret
|
||||
}
|
||||
|
||||
// resolveJWTSecret returns the JWT secret: XCE_JWT_SECRET if usable, else
|
||||
// the config file value if usable, else a random per-process secret (the
|
||||
// caller detects that case because the result differs from both inputs).
|
||||
func resolveJWTSecret(configValue string) string {
|
||||
if env := os.Getenv("XCE_JWT_SECRET"); env != "" {
|
||||
if env := os.Getenv("XCE_JWT_SECRET"); usableJWTSecret(env) {
|
||||
return env
|
||||
}
|
||||
return configValue
|
||||
if usableJWTSecret(configValue) {
|
||||
return configValue
|
||||
}
|
||||
buf := make([]byte, 32)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
// crypto/rand never fails on supported platforms; refuse to fall
|
||||
// back to a predictable secret if it ever does.
|
||||
panic(fmt.Sprintf("cannot generate a JWT secret: %v", err))
|
||||
}
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue