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
4 changes: 4 additions & 0 deletions charts/kite/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ metadata:
{{- include "kite.labels" . | nindent 4 }}
type: Opaque
data:
{{- if .Values.jwtSecret }}
JWT_SECRET: {{ .Values.jwtSecret | b64enc | quote }}
{{- end }}
{{- if .Values.encryptKey }}
KITE_ENCRYPT_KEY: {{ .Values.encryptKey | b64enc | quote }}
{{- end }}
{{- if ne .Values.db.type "sqlite" }}
DB_TYPE: {{ .Values.db.type | b64enc | quote }}
DB_DSN: {{ .Values.db.dsn | b64enc | quote }}
Expand Down
10 changes: 4 additions & 6 deletions charts/kite/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,13 @@ basePath: ""
# Be careful with this setting in production
anonymousUserEnabled: false

# This is the key used for signing JWT tokens
# Change this in production
# Secret key for signing JWT tokens. Auto-generated if empty.
# Ignored if using existingSecret
jwtSecret: "kite-default-jwt-secret-key-change-in-production"
jwtSecret: ""

# This is the key used for encrypting sensitive data
# Change this in production
# Key for encrypting sensitive data. Auto-generated if empty.
# Ignored if using existingSecret
encryptKey: "kite-default-encryption-key-change-in-production"
encryptKey: ""

# Superuser configuration
# Used to create an initial superuser account on first startup
Expand Down
4 changes: 2 additions & 2 deletions docs/config/chart-values.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ This document describes all available configuration options for the Kite Helm Ch
| Parameter | Description | Default |
| ---------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `anonymousUserEnabled` | Enable anonymous user access with full admin privileges. Use with caution in production. | `false` |
| `jwtSecret` | Secret key used for signing JWT tokens. Change this in production. | `"kite-default-jwt-secret-key-change-in-production"` |
| `encryptKey` | Secret key used for encrypting sensitive data. Change this in production. | `"kite-default-encryption-key-change-in-production"` |
| `jwtSecret` | Secret key for signing JWT tokens. Auto-generated on first boot if empty. | `""` |
| `encryptKey` | Secret key for encrypting sensitive data. Auto-generated on first boot if empty. | `""` |
| `host` | Hostname for the application | `""` |

## Database Configuration
Expand Down
4 changes: 2 additions & 2 deletions docs/zh/config/chart-values.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
| 参数 | 描述 | 默认值 |
| ---------------------- | ---------------------------------------------------------- | ---------------------------------------------------- |
| `anonymousUserEnabled` | 启用匿名用户访问,拥有完全管理员权限。生产环境请谨慎使用。 | `false` |
| `jwtSecret` | 用于签名 JWT 令牌的密钥。生产环境请修改此值 | `"kite-default-jwt-secret-key-change-in-production"` |
| `encryptKey` | 用于加密敏感数据的密钥。生产环境请修改此值 | `"kite-default-encryption-key-change-in-production"` |
| `jwtSecret` | 用于签名 JWT 令牌的密钥。为空时首次启动自动生成。 | `""` |
| `encryptKey` | 用于加密敏感数据的密钥。为空时首次启动自动生成。 | `""` |
| `host` | 应用程序的主机名 | `""` |

## 数据库配置
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ func main() {
r.Use(middleware.Logger())
r.Use(middleware.CORS())
model.InitDB()
model.EnsureSecrets()
if _, err := model.GetGeneralSetting(); err != nil {
klog.Warningf("Failed to load general setting: %v", err)
}
Expand Down
2 changes: 0 additions & 2 deletions pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ func LoadEnvs() {

if key := os.Getenv("KITE_ENCRYPT_KEY"); key != "" {
KiteEncryptKey = key
} else {
klog.Warningf("KITE_ENCRYPT_KEY is not set, using default key, this is not secure for production!")
}

if v := os.Getenv("ANONYMOUS_USER_ENABLED"); v == "true" {
Expand Down
1 change: 1 addition & 0 deletions pkg/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func InitDB() {
}
}
models := []interface{}{
SystemSecret{},
User{},
Cluster{},
GeneralSetting{},
Expand Down
140 changes: 140 additions & 0 deletions pkg/model/system_secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package model

import (
"crypto/rand"
"encoding/base64"
"errors"
"os"

"github.com/zxh326/kite/pkg/common"
"gorm.io/gorm"
"k8s.io/klog/v2"
)

// SystemSecret stores auto-generated application secrets in the database.
// Values are plain text (not SecretString) to avoid circular encryption.
type SystemSecret struct {
Name string `json:"name" gorm:"primaryKey;column:name;type:varchar(64)"`
Value string `json:"value" gorm:"column:value;type:text;not null"`
}

const (
secretNameJWT = "jwt_secret"
secretNameEncrypt = "encrypt_key"

defaultJWTSecret = "kite-default-jwt-secret-key-change-in-production"
defaultEncryptKey = "kite-default-encryption-key-change-in-production"
)

// EnsureSecrets guarantees that JwtSecret and KiteEncryptKey hold
// cryptographically secure values. Must be called after InitDB()
// and before any code that reads SecretString columns.
//
// Priority: env var > DB stored value > auto-generated.
func EnsureSecrets() {
common.JwtSecret = ensureOneSecret(
secretNameJWT, common.JwtSecret, "JWT_SECRET", defaultJWTSecret, false,
os.Getenv("JWT_SECRET") != "",
)
common.KiteEncryptKey = ensureOneSecret(
secretNameEncrypt, common.KiteEncryptKey, "KITE_ENCRYPT_KEY", defaultEncryptKey, true,
os.Getenv("KITE_ENCRYPT_KEY") != "",
)
}

func ensureOneSecret(dbName, currentValue, envName, knownDefault string, isEncryptionKey, envWasSet bool) string {
if envWasSet {
return currentValue
}

stored, dbErr := loadSecret(dbName)
if dbErr != nil {
klog.Fatalf("Cannot read %s from database: %v (refusing to proceed with ambiguous secret state)", envName, dbErr)
}
if stored != "" {
return stored
}

if isEncryptionKey && hasExistingEncryptedData() {
effective := persistSecret(dbName, currentValue)
klog.Warningf("════════════════════════════════════════════════════════════")
klog.Warningf(" %s is using the insecure hardcoded default.", envName)
klog.Warningf(" Existing encrypted data has been preserved.")
klog.Warningf(" Please set %s to a secure random value", envName)
klog.Warningf(" and re-encrypt your data.")
klog.Warningf("════════════════════════════════════════════════════════════")
return effective
}

secret := persistSecret(dbName, generateRandomSecret(32))
klog.Infof("Auto-generated %s and stored in database (first boot)", envName)
return secret
}

func generateRandomSecret(n int) string {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
klog.Fatalf("Failed to generate random secret: %v", err)
}
return base64.RawURLEncoding.EncodeToString(b)
}

func loadSecret(name string) (string, error) {
var s SystemSecret
err := DB.Where("name = ?", name).First(&s).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", nil
}
if err != nil {
return "", err
}
return s.Value, nil
}

// persistSecret inserts the secret if no row exists yet. If a row already
// exists the stored value is returned (first writer wins). Fatals on
// unrecoverable DB errors.
func persistSecret(name, value string) string {
var existing SystemSecret
err := DB.Where("name = ?", name).First(&existing).Error

if errors.Is(err, gorm.ErrRecordNotFound) {
if err := DB.Create(&SystemSecret{Name: name, Value: value}).Error; err != nil {
if stored, readErr := loadSecret(name); readErr == nil && stored != "" {
klog.Infof("Secret %q was created by another instance, adopting its value", name)
return stored
}
klog.Fatalf("Failed to persist secret %q and no stored winner found: %v", name, err)
}
return value
}
if err != nil {
klog.Fatalf("Failed to read secret %q from database: %v", name, err)
}
return existing.Value
}

// hasExistingEncryptedData returns true when the database contains rows with
// non-empty SecretString columns. Returns true on query errors (fail-safe).
func hasExistingEncryptedData() bool {
checks := []struct {
model interface{}
where string
}{
{&Cluster{}, "config IS NOT NULL AND config != ''"},
{&OAuthProvider{}, "client_secret IS NOT NULL AND client_secret != ''"},
{&User{}, "api_key IS NOT NULL AND api_key != ''"},
{&GeneralSetting{}, "ai_api_key IS NOT NULL AND ai_api_key != ''"},
}
for _, c := range checks {
var count int64
if err := DB.Model(c.model).Where(c.where).Count(&count).Error; err != nil {
klog.Warningf("Failed to check for encrypted data (%T): %v — assuming data exists (fail-safe)", c.model, err)
return true
}
if count > 0 {
return true
}
}
return false
}