fix(security): jwt.key 移出可浏览工作区(存项目根,自动从 data 迁移)
工作空间文件管理器的根目录是 dataDir(s.m.dir),而 jwt.key 原来就存在 dataDir/jwt.key —— 会被列出/下载(签名密钥泄露→可伪造 token)/删除(重启重生 成→全体登出)。 - loadOrCreateJWTKey(keyDir, dataDir):密钥改存 keyDir/jwt.key;若旧 data/jwt.key 仍在,启动时迁移(保留密钥不登出、删旧文件从工作区消失) - New() 增加 keyDir 参数;生产传 config.BaseDir()(项目根),在 dataDir 之外, wsResolve 的路径限定使其不可列/读/下载/删 - 测试传临时目录,隔离且不触发迁移;.gitignore 忽略根目录 /jwt.key 注:项目根在 Docker 里不持久化,容器重建会重新生成密钥、需重新登录(单人系统可接受)。
This commit is contained in:
1 parent
1bc629ce4c
commit
289cf6a24f
6 files changed
+28
-9
No files matched your search
@@ -39,5 +39,8 @@ Thumbs.db
|
|||||||
/server/webui/dist/
|
/server/webui/dist/
|
||||||
/dist/
|
/dist/
|
||||||
|
|
||||||
|
# JWT 签名密钥(运行时生成,存项目根,绝不提交)
|
||||||
|
/jwt.key
|
||||||
|
|
||||||
# Docker 部署生成的本地环境(含密码/密钥,不提交)
|
# Docker 部署生成的本地环境(含密码/密钥,不提交)
|
||||||
.env
|
.env
|
||||||
+1
-1
@@ -80,7 +80,7 @@ func main() {
|
|||||||
skillDir = abs
|
skillDir = abs
|
||||||
}
|
}
|
||||||
log.Printf("[config] skill 目录: %s", skillDir)
|
log.Printf("[config] skill 目录: %s", skillDir)
|
||||||
srv := server.New(ctx, mgr, skillDir, *dataDir)
|
srv := server.New(ctx, mgr, skillDir, *dataDir, config.BaseDir())
|
||||||
httpSrv := &http.Server{
|
httpSrv := &http.Server{
|
||||||
Addr: *addr,
|
Addr: *addr,
|
||||||
Handler: srv.Handler(),
|
Handler: srv.Handler(),
|
||||||
|
|||||||
+20
-4
@@ -22,10 +22,26 @@ const (
|
|||||||
keyChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
keyChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loadOrCreateJWTKey reads the 32-byte signing key from dataDir/jwt.key.
|
// loadOrCreateJWTKey reads the 32-byte signing key from keyDir/jwt.key. keyDir is
|
||||||
// On first run it generates a cryptographically random key and persists it.
|
// the project base dir (next to the executable), NOT the browsable workspace root
|
||||||
func loadOrCreateJWTKey(dataDir string) ([]byte, error) {
|
// (dataDir) — the signing key must never be listable/downloadable via the file
|
||||||
path := filepath.Join(dataDir, jwtKeyFilename)
|
// manager. Legacy installs kept it at dataDir/jwt.key; if present there and not yet
|
||||||
|
// at the new location, it is migrated (key preserved, so sessions stay valid) and
|
||||||
|
// the old file removed so it disappears from the workspace. On first run a random
|
||||||
|
// key is generated and persisted.
|
||||||
|
func loadOrCreateJWTKey(keyDir, dataDir string) ([]byte, error) {
|
||||||
|
path := filepath.Join(keyDir, jwtKeyFilename)
|
||||||
|
// one-time migration out of the old in-workspace location.
|
||||||
|
if legacy := filepath.Join(dataDir, jwtKeyFilename); legacy != path {
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
if data, rerr := os.ReadFile(legacy); rerr == nil {
|
||||||
|
if werr := os.WriteFile(path, data, 0o600); werr == nil {
|
||||||
|
_ = os.Remove(legacy)
|
||||||
|
log.Printf("[auth] JWT key 已从 %s 迁移到 %s(移出可浏览工作区)", legacy, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if data, err := os.ReadFile(path); err == nil && len(strings.TrimSpace(string(data))) >= 32 {
|
if data, err := os.ReadFile(path); err == nil && len(strings.TrimSpace(string(data))) >= 32 {
|
||||||
return []byte(strings.TrimSpace(string(data))), nil
|
return []byte(strings.TrimSpace(string(data))), nil
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -22,7 +22,7 @@ func TestCoreTaskLifecyclePG(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer m.Close()
|
defer m.Close()
|
||||||
td := t.TempDir()
|
td := t.TempDir()
|
||||||
s := New(context.Background(), m, td, td)
|
s := New(context.Background(), m, td, td, td)
|
||||||
h := s.Handler()
|
h := s.Handler()
|
||||||
tok, err := signJWT(s.jwtKey)
|
tok, err := signJWT(s.jwtKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ func TestMgmtAPI(t *testing.T) {
|
|||||||
t.Skip("postgres unavailable — skipping management API test")
|
t.Skip("postgres unavailable — skipping management API test")
|
||||||
}
|
}
|
||||||
td := t.TempDir()
|
td := t.TempDir()
|
||||||
s := New(context.Background(), m, td, td)
|
s := New(context.Background(), m, td, td, td)
|
||||||
h := s.Handler()
|
h := s.Handler()
|
||||||
tok, err := signJWT(s.jwtKey)
|
tok, err := signJWT(s.jwtKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+2
-2
@@ -97,8 +97,8 @@ type triggeredRun struct {
|
|||||||
mergeable bool // true for finding/goal event triggers (merge by taskID)
|
mergeable bool // true for finding/goal event triggers (merge by taskID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, m *Manager, skillDir string, dataDir string) *Server {
|
func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDir string) *Server {
|
||||||
key, err := loadOrCreateJWTKey(dataDir)
|
key, err := loadOrCreateJWTKey(keyDir, dataDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("[auth] JWT key: %v", err)
|
log.Fatalf("[auth] JWT key: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user