mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-11-13 15:57:22 +01:00
fix(tls): reuse the generated HTTPS certificate across updates
(cherry picked from commit 4f2af3371bd5ff89721f222462713bc16beed4b2)
This commit is contained in:
1 parent
48065ad75f
commit
83b0b7b909
10 files changed
+128
-26
No files matched your search
+1
-1
@@ -25,7 +25,7 @@ func main() {
|
||||
updateActivation := app.TakeUpdateActivation()
|
||||
updateSessions := app.TakeUpdateSessionHandoff()
|
||||
var configPath = flag.String("config", "config.yaml", "Path to the configuration file")
|
||||
var httpsBootstrap = flag.Bool("https", false, "Enable HTTPS for the main site; uses an in-memory self-signed certificate when no cert/key is configured")
|
||||
var httpsBootstrap = flag.Bool("https", false, "Enable HTTPS for the main site; creates and reuses a local self-signed certificate when no cert/key is configured")
|
||||
var httpBootstrap = flag.Bool("http", false, "Force plain HTTP for the main site, overriding TLS settings in the configuration file")
|
||||
var resetAdminPassword = flag.Bool("reset-admin-password", false, "Interactively reset the built-in admin password and exit")
|
||||
checkIsolation := flag.Bool("check-process-isolation", false, "Probe task containment and cleanup, then exit without starting services")
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ server:
|
||||
# 方式 A(推荐生产):PEM 证书与私钥路径
|
||||
# tls_cert_path: /path/to/fullchain.pem
|
||||
# tls_key_path: /path/to/privkey.pem
|
||||
# 方式 B(仅本地/测试):无证书文件时内存自签(浏览器会提示不受信任;SAN 含 localhost / 127.0.0.1)
|
||||
# 方式 B(仅本地/测试):在配置目录的 data/tls/self-signed.pem 保存并复用自签证书和私钥(SAN 含 localhost / 127.0.0.1)。
|
||||
tls_auto_self_sign: true
|
||||
# 认证配置
|
||||
auth:
|
||||
|
||||
@@ -64,6 +64,8 @@ server:
|
||||
tls_auto_self_sign: true
|
||||
```
|
||||
|
||||
The generated certificate and private key are saved in `data/tls/self-signed.pem` under the configuration directory and reused across restarts and updates. The first upgrade from an in-memory certificate generates a new certificate. To rotate it, stop the service and move the file aside before starting again.
|
||||
|
||||
For production, use real certificates or terminate TLS at a reverse proxy. If the proxy terminates TLS and forwards HTTP to the app, avoid enabling app-side TLS on the same upstream unless `proxy_pass` uses HTTPS.
|
||||
|
||||
Nginx must not buffer SSE:
|
||||
|
||||
@@ -43,6 +43,8 @@ https://127.0.0.1:8080/
|
||||
|
||||
自签证书会触发浏览器安全提示,这是本地测试的正常现象。生产环境建议配置真实证书。
|
||||
|
||||
自动生成的证书和私钥保存在配置目录下的 `data/tls/self-signed.pem`,后续启动和一键更新都会复用。首次从内存自签版本升级时会生成新的证书。需要轮换时,先停止服务并移走此文件,下次启动会生成新的证书。
|
||||
|
||||
`run.sh` 是最常用的启动入口,适合:
|
||||
|
||||
- 本机体验。
|
||||
|
||||
+6
-4
@@ -46,6 +46,7 @@ import (
|
||||
type App struct {
|
||||
shutdownOnce sync.Once
|
||||
config *config.Config
|
||||
configDir string
|
||||
logger *logger.Logger
|
||||
router *gin.Engine
|
||||
mcpServer *mcp.Server
|
||||
@@ -516,6 +517,7 @@ func New(cfg *config.Config, log *logger.Logger, configPath, updateActivation st
|
||||
// 创建 App 实例(部分字段稍后填充)
|
||||
app := &App{
|
||||
config: cfg,
|
||||
configDir: configDir,
|
||||
logger: log,
|
||||
router: router,
|
||||
mcpServer: mcpServer,
|
||||
@@ -730,7 +732,7 @@ func (a *App) RunWithContext(ctx context.Context) error {
|
||||
|
||||
// 启动主服务器(可选 HTTPS + HTTP/2,见 config server.tls_*)
|
||||
addr := fmt.Sprintf("%s:%d", a.config.Server.Host, a.config.Server.Port)
|
||||
tlsMode, tlsConf, certFile, keyFile, tlsErr := prepareMainServerTLS(&a.config.Server)
|
||||
tlsMode, tlsConf, certFile, keyFile, tlsErr := prepareMainServerTLS(&a.config.Server, a.configDir)
|
||||
if tlsErr != nil {
|
||||
return tlsErr
|
||||
}
|
||||
@@ -749,8 +751,8 @@ func (a *App) RunWithContext(ctx context.Context) error {
|
||||
zap.String("address", addr),
|
||||
zap.String("cert", certFile),
|
||||
)
|
||||
case mainTLSInMemorySelfSigned:
|
||||
a.logger.Debug("启动 HTTPS 主服务(内存自签证书,仅测试;已启用 HTTP/2 协商)",
|
||||
case mainTLSAutoSelfSigned:
|
||||
a.logger.Debug("启动 HTTPS 主服务(自动自签证书,仅测试;已启用 HTTP/2 协商)",
|
||||
zap.String("address", addr),
|
||||
)
|
||||
}
|
||||
@@ -800,7 +802,7 @@ func (a *App) RunWithContext(ctx context.Context) error {
|
||||
err = srv.ListenAndServe()
|
||||
case tlsMode == mainTLSFromFiles:
|
||||
err = srv.ListenAndServeTLS(certFile, keyFile)
|
||||
case tlsMode == mainTLSInMemorySelfSigned:
|
||||
case tlsMode == mainTLSAutoSelfSigned:
|
||||
var ln net.Listener
|
||||
ln, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
||||
if err == nil {
|
||||
|
||||
@@ -89,17 +89,14 @@ func TestServerHTTPRedirectEnabled(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMainServerMuxHTTPRedirectAndHTTPS(t *testing.T) {
|
||||
cert, err := generateMainServerSelfSignedCert()
|
||||
_, tlsConfig, _, _, err := prepareMainServerTLS(&config.ServerConfig{TLSAutoSelfSign: true}, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("generate cert: %v", err)
|
||||
}
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, "ok")
|
||||
})
|
||||
srv := &http.Server{Handler: handler, TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}}
|
||||
srv := &http.Server{Handler: handler, TLSConfig: tlsConfig}
|
||||
if err := http2.ConfigureServer(srv, &http2.Server{}); err != nil {
|
||||
t.Fatalf("configure http2: %v", err)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ import (
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -23,13 +27,13 @@ type mainTLSMode int
|
||||
const (
|
||||
mainTLSOff mainTLSMode = iota
|
||||
mainTLSFromFiles
|
||||
mainTLSInMemorySelfSigned
|
||||
mainTLSAutoSelfSigned
|
||||
)
|
||||
|
||||
// prepareMainServerTLS 根据 server 配置决定主站是否启用 HTTPS(及 HTTP/2 协商)。
|
||||
// fromFiles:使用 tls_cert_path + tls_key_path,由 http.Server.ListenAndServeTLS 加载 PEM。
|
||||
// inMemory:tls_auto_self_sign 生成的自签证书,仅用于本地/测试。
|
||||
func prepareMainServerTLS(cfg *config.ServerConfig) (mode mainTLSMode, tlsConf *tls.Config, certFile, keyFile string, err error) {
|
||||
// autoSelfSigned:tls_auto_self_sign 生成并复用的自签证书,仅用于本地/测试。
|
||||
func prepareMainServerTLS(cfg *config.ServerConfig, configDir string) (mode mainTLSMode, tlsConf *tls.Config, certFile, keyFile string, err error) {
|
||||
if cfg == nil || !config.MainWebUIUsesHTTPS(cfg) {
|
||||
return mainTLSOff, nil, "", "", nil
|
||||
}
|
||||
@@ -40,28 +44,71 @@ func prepareMainServerTLS(cfg *config.ServerConfig) (mode mainTLSMode, tlsConf *
|
||||
return mainTLSFromFiles, &tls.Config{MinVersion: tls.VersionTLS12}, certFile, keyFile, nil
|
||||
}
|
||||
if cfg.TLSAutoSelfSign {
|
||||
cert, genErr := generateMainServerSelfSignedCert()
|
||||
if genErr != nil {
|
||||
return mainTLSOff, nil, "", "", fmt.Errorf("生成自签 TLS 证书: %w", genErr)
|
||||
path := filepath.Join(configDir, "data", "tls", "self-signed.pem")
|
||||
cert, certErr := loadOrCreateMainServerSelfSignedCert(path)
|
||||
if certErr != nil {
|
||||
return mainTLSOff, nil, "", "", fmt.Errorf("读取或保存自签 TLS 证书 %s: %w", path, certErr)
|
||||
}
|
||||
tlsConf = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
return mainTLSInMemorySelfSigned, tlsConf, "", "", nil
|
||||
return mainTLSAutoSelfSigned, tlsConf, "", "", nil
|
||||
}
|
||||
return mainTLSOff, nil, "", "", fmt.Errorf("server: 已启用 TLS(tls_enabled / tls_auto_self_sign / 证书路径),请设置 tls_cert_path 与 tls_key_path,或将 tls_auto_self_sign 设为 true(仅测试环境)")
|
||||
}
|
||||
|
||||
func generateMainServerSelfSignedCert() (tls.Certificate, error) {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
func loadOrCreateMainServerSelfSignedCert(path string) (tls.Certificate, error) {
|
||||
bundle, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return tls.X509KeyPair(bundle, bundle)
|
||||
}
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
bundle, err = generateMainServerSelfSignedPEM()
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
|
||||
f, err := os.CreateTemp(dir, ".self-signed-*")
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
_, writeErr := f.Write(bundle)
|
||||
if writeErr == nil {
|
||||
writeErr = f.Sync()
|
||||
}
|
||||
if err := errors.Join(writeErr, f.Close()); err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
// Publish the complete pair once, even if two instances start concurrently.
|
||||
if err := os.Link(f.Name(), path); err != nil {
|
||||
if !errors.Is(err, fs.ErrExist) {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
bundle, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
}
|
||||
return tls.X509KeyPair(bundle, bundle)
|
||||
}
|
||||
|
||||
func generateMainServerSelfSignedPEM() ([]byte, error) {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{CommonName: "CyberStrikeAI"},
|
||||
@@ -74,13 +121,13 @@ func generateMainServerSelfSignedCert() (tls.Certificate, error) {
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return nil, err
|
||||
}
|
||||
keyDER, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
return nil, err
|
||||
}
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
|
||||
return tls.X509KeyPair(certPEM, keyPEM)
|
||||
return append(certPEM, keyPEM...), nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
)
|
||||
|
||||
func TestMainServerTLSReusesGeneratedIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
configDir := t.TempDir()
|
||||
settings := config.ServerConfig{TLSAutoSelfSign: true}
|
||||
_, first, _, _, err := prepareMainServerTLS(&settings, configDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, next, _, _, err := prepareMainServerTLS(&settings, configDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(first.Certificates[0].Certificate[0], next.Certificates[0].Certificate[0]) {
|
||||
t.Fatal("server restart changed its self-signed certificate")
|
||||
}
|
||||
|
||||
bundlePath := filepath.Join(configDir, "data", "tls", "self-signed.pem")
|
||||
info, err := os.Stat(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if runtime.GOOS != "windows" && info.Mode().Perm()&0077 != 0 {
|
||||
t.Fatalf("TLS private key is accessible by other users: %v", info.Mode().Perm())
|
||||
}
|
||||
|
||||
brokenBundle := []byte("incomplete certificate bundle")
|
||||
if err := os.WriteFile(bundlePath, brokenBundle, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, _, err := prepareMainServerTLS(&settings, configDir); err == nil {
|
||||
t.Fatal("invalid certificate bundle was accepted")
|
||||
}
|
||||
bundle, err := os.ReadFile(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(bundle, brokenBundle) {
|
||||
t.Fatal("startup silently replaced the certificate bundle")
|
||||
}
|
||||
}
|
||||
@@ -695,7 +695,7 @@ type ServerConfig struct {
|
||||
// TLSCertPath / TLSKeyPath 非空时从 PEM 文件加载证书(生产环境推荐)。
|
||||
TLSCertPath string `yaml:"tls_cert_path,omitempty" json:"tls_cert_path,omitempty"`
|
||||
TLSKeyPath string `yaml:"tls_key_path,omitempty" json:"tls_key_path,omitempty"`
|
||||
// TLSAutoSelfSign 为 true 且未配置有效证书路径时,启动时生成内存自签证书(仅本地/测试;浏览器会提示不受信任)。
|
||||
// TLSAutoSelfSign 为 true 且未配置证书路径时,生成并复用安装目录中的自签证书(仅本地/测试)。
|
||||
TLSAutoSelfSign bool `yaml:"tls_auto_self_sign,omitempty" json:"tls_auto_self_sign,omitempty"`
|
||||
// TLSHTTPRedirect 为 false 时禁用 HTTP→HTTPS 跳转;省略或为 true 且已启用 HTTPS 时,明文 HTTP 访问将 308 跳转到 HTTPS(同端口嗅探分流)。
|
||||
TLSHTTPRedirect *bool `yaml:"tls_http_redirect,omitempty" json:"tls_http_redirect,omitempty"`
|
||||
|
||||
@@ -30,7 +30,7 @@ func ServerHTTPRedirectEnabled(s *ServerConfig) bool {
|
||||
}
|
||||
|
||||
// ApplyDevHTTPSBootstrap 供 --https / 一键脚本使用:强制开启主站 TLS。
|
||||
// 若已配置 tls_cert_path 与 tls_key_path 则仅用 PEM,不开启自签;否则启用 tls_auto_self_sign(内存证书,仅本地测试)。
|
||||
// 若已配置 tls_cert_path 与 tls_key_path 则使用 PEM,否则启用 tls_auto_self_sign(仅本地测试)。
|
||||
func ApplyDevHTTPSBootstrap(cfg *Config) {
|
||||
if cfg == nil {
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user