修复证书问题

This commit is contained in:
ryan
2026-06-22 22:56:10 +08:00
parent 92ceecc6ce
commit 8ed91dbf97
5 changed files with 101 additions and 2 deletions
+1 -1
View File
@@ -105,7 +105,7 @@ services:
- ./data/agent/:/data
environment:
OPENFLARE_SERVER_URL: "http://host.docker.internal:3000"
OPENFLARE_AGENT_TOKEN: "6ef051dba8b1b7c7e8a4b5dc1b138593"
OPENFLARE_AGENT_TOKEN: "3f7b66c9329a0cf253e71a86681223aa"
LOG_LEVEL: "debug"
extra_hosts:
- "host.docker.internal:host-gateway"
+1
View File
@@ -23,6 +23,7 @@ sidebar: false
### 修复
- 修复配置版本发布到 Agent 后 `openresty -t` 因证书私钥无法解析而失败的问题。根因是发布快照生成 `certs/{id}.key` 时直接写入库内加密的 `KeyPEM`(`enc:v1:`),未解密为 PEM;现与证书详情接口一致,发布前通过 `OpenKeyPEM` 解密后再下发。
- 修复 `/api/v1/d/option` 批量更新 OpenResty 等业务配置不生效的问题。根本原因是 option 模块在读写时做了 PascalCase 与 snake_case 的机械转换(如 `OpenRestyEventsUse` → `open_resty_events_use`),与 `w_system_configs` 中实际 key(`openresty_events_use`)不一致,更新写入了错误的幽灵配置行。现改为 API 直接使用与数据库一致的 snake_case key,并同步更新前端性能调优与运维设置页。
- 修复 PostgreSQL 数据库执行迁移时报 `duplicate key value violates unique constraint "goose_db_version_pkey"` 导致迁移中断的问题。根本原因:`goose_db_version.id` 自增序列落后于表内 `MAX(id)`(常见于从 dump 恢复或历史迁移以显式 id 复制版本记录后),goose 记录新版本号时自增 id 与既有行冲突。修复方式:在 `goose.Up` 前对 PostgreSQL 执行 `setval` 重新对齐 `goose_db_version` 的 id 序列。
- 修复 openflared(Tunnel Client)WebSocket 连接在 Cloudflare 代理环境下频繁收到 EOF 断连的问题。根本原因:服务端 `read_pump` 仅在收到 WebSocket 协议层 Pong 帧时刷新读超时,而客户端(`golang.org/x/net/websocket`)以 JSON 应用层 `{"type":"pong"}` 响应 ping,服务端 90s 读超时到期后主动关闭连接,客户端收到 EOF 并进入无限重连循环。修复方式:在 `clientPongType` 分支中同步调用 `conn.SetReadDeadline` 刷新超时。
@@ -0,0 +1,77 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package config_version
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"strings"
"testing"
"time"
oftls "github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBuildCertificateSupportFilesDecryptsSealedPrivateKey(t *testing.T) {
cleanup := setupConfigVersionTestDB(t)
defer cleanup()
require.NoError(t, db.DB(context.Background()).AutoMigrate(&model.TLSCertificate{}))
oldSecret := config.Config.App.SessionSecret
config.Config.App.SessionSecret = "test-session-secret-for-tls-seal"
t.Cleanup(func() { config.Config.App.SessionSecret = oldSecret })
ctx := context.Background()
certPEM, keyPEM := generateTestCertKeyPairForSnapshot(t)
certificate, err := oftls.CreateCertificate(ctx, oftls.CertificateInput{
Name: "publish-cert",
CertPEM: certPEM,
KeyPEM: keyPEM,
})
require.NoError(t, err)
files, err := buildCertificateSupportFiles(ctx, []snapshotRoute{
{CertIDs: []uint{certificate.ID}},
})
require.NoError(t, err)
require.Len(t, files, 2)
var keyContent string
for _, file := range files {
if file.Path == certificateKeyFileName(certificate.ID) {
keyContent = file.Content
}
assert.NotContains(t, file.Content, "enc:v1:")
}
assert.Contains(t, keyContent, "BEGIN")
assert.Equal(t, normalizePEM(strings.TrimSpace(keyPEM)), keyContent)
}
func generateTestCertKeyPairForSnapshot(t *testing.T) (certPEM string, keyPEM string) {
t.Helper()
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "test.example.com"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
}
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
require.NoError(t, err)
certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}))
return certPEM, keyPEM
}
@@ -12,6 +12,7 @@ import (
"strings"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/routeidentity"
oftls "github.com/Rain-kl/Wavelet/internal/apps/openflare/tls"
"github.com/Rain-kl/Wavelet/internal/apps/openflare/waf"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
@@ -538,11 +539,19 @@ func buildOpenRestyConfigSnapshot(ctx context.Context) openRestyConfigSnapshot {
func buildCertificateSupportFiles(ctx context.Context, routes []snapshotRoute) ([]SupportFile, error) {
certIDSet := make(map[uint]struct{})
for _, route := range routes {
if route.CertID != nil && *route.CertID != 0 {
certIDSet[*route.CertID] = struct{}{}
}
for _, certID := range route.CertIDs {
if certID != 0 {
certIDSet[certID] = struct{}{}
}
}
for _, certID := range route.DomainCertIDs {
if certID != 0 {
certIDSet[certID] = struct{}{}
}
}
}
if len(certIDSet) == 0 {
return nil, nil
@@ -558,9 +567,16 @@ func buildCertificateSupportFiles(ctx context.Context, routes []snapshotRoute) (
if err != nil {
return nil, err
}
keyPEM, err := oftls.OpenKeyPEM(certificate.KeyPEM)
if err != nil {
return nil, fmt.Errorf("certificate %d private key: %w", certificate.ID, err)
}
if strings.TrimSpace(keyPEM) == "" {
return nil, fmt.Errorf("certificate %d has no private key", certificate.ID)
}
files = append(files,
SupportFile{Path: certificateCertFileName(certificate.ID), Content: normalizePEM(certificate.CertPEM)},
SupportFile{Path: certificateKeyFileName(certificate.ID), Content: normalizePEM(certificate.KeyPEM)},
SupportFile{Path: certificateKeyFileName(certificate.ID), Content: normalizePEM(keyPEM)},
)
}
return dedupeSupportFiles(files), nil
+5
View File
@@ -39,6 +39,11 @@ func sealSensitive(plaintext string) (string, error) {
return sensitiveValuePrefix + encrypted, nil
}
// OpenKeyPEM decrypts a stored certificate private key for runtime distribution.
func OpenKeyPEM(stored string) (string, error) {
return openSensitive(stored)
}
func openSensitive(stored string) (string, error) {
stored = strings.TrimSpace(stored)
if stored == "" {