From f3dc6deb2e4ddfe06ef3507bc97a07bb2afb2f92 Mon Sep 17 00:00:00 2001 From: Codex Local Date: Wed, 26 Aug 2026 10:26:06 +0800 Subject: [PATCH] fix: separate public object store TLS --- deploy/keycloak/realm-voice-local.json | 2 ++ deploy/server/README.md | 2 +- deploy/server/compose.override.yaml | 5 +++-- deploy/server/nginx.conf | 21 ++++++++++++++------- internal/app/app.go | 2 +- internal/config/config.go | 8 +++++++- internal/config/config_test.go | 3 +++ internal/objectstore/store.go | 9 +++++---- internal/objectstore/store_test.go | 8 ++++---- 9 files changed, 40 insertions(+), 20 deletions(-) diff --git a/deploy/keycloak/realm-voice-local.json b/deploy/keycloak/realm-voice-local.json index ba10069..5bfeb52 100644 --- a/deploy/keycloak/realm-voice-local.json +++ b/deploy/keycloak/realm-voice-local.json @@ -20,6 +20,7 @@ "http://127.0.0.1:3100/*", "http://121.41.46.238/*", "https://121.41.46.238/*", + "https://121.41.46.238:80/*", "http://localhost:5173/*", "http://127.0.0.1:5173/*" ], @@ -28,6 +29,7 @@ "http://127.0.0.1:3100", "http://121.41.46.238", "https://121.41.46.238", + "https://121.41.46.238:80", "http://localhost:5173", "http://127.0.0.1:5173" ], diff --git a/deploy/server/README.md b/deploy/server/README.md index 145aa12..7d63dfc 100644 --- a/deploy/server/README.md +++ b/deploy/server/README.md @@ -21,4 +21,4 @@ docker compose -f compose.yaml -f deploy/server/compose.override.yaml \ mock-provider api worker web gateway ``` -安全组和主机防火墙只保留TCP 80、443以及受限来源的SSH 22。正式环境仍应配置自有域名、生产OIDC和真实外部服务,并切换到Kubernetes生产覆盖层。 +安全组和主机防火墙只保留TCP 80、443以及受限来源的SSH 22。如果云安全组暂未放行443,可临时将宿主机80映射到网关443,并使用`https://SERVER_IP:80`;放行443后应恢复标准HTTPS地址。正式环境仍应配置自有域名、生产OIDC和真实外部服务,并切换到Kubernetes生产覆盖层。 diff --git a/deploy/server/compose.override.yaml b/deploy/server/compose.override.yaml index d460bfd..f656c43 100644 --- a/deploy/server/compose.override.yaml +++ b/deploy/server/compose.override.yaml @@ -15,7 +15,8 @@ x-server-backend-environment: &server-backend-environment OBJECT_STORE_SECRET_KEY: ${MINIO_SECRET_KEY} OBJECT_STORE_EXPORT_BUCKET: private-exports OBJECT_STORE_REGION: us-east-1 - OBJECT_STORE_SECURE: "true" + OBJECT_STORE_SECURE: "false" + OBJECT_STORE_PUBLIC_SECURE: "true" CURSOR_SIGNING_SECRET: ${CURSOR_SIGNING_SECRET} SHARE_TOKEN_SIGNING_SECRET: ${SHARE_TOKEN_SIGNING_SECRET} EMBEDDING_PROVIDER: local @@ -158,7 +159,7 @@ services: gateway: image: nginx:1.30-alpine ports: - - "80:80" + - "80:443" - "443:443" volumes: - ./deploy/server/nginx.conf:/etc/nginx/conf.d/default.conf:ro diff --git a/deploy/server/nginx.conf b/deploy/server/nginx.conf index 5a75aba..20201de 100644 --- a/deploy/server/nginx.conf +++ b/deploy/server/nginx.conf @@ -18,6 +18,13 @@ server { add_header Strict-Transport-Security "max-age=31536000" always; + resolver 127.0.0.11 ipv6=off valid=10s; + set $api_backend http://api:8080; + set $callbacks_backend http://callbacks:8081; + set $keycloak_backend http://keycloak:8080; + set $minio_backend http://minio:9000; + set $web_backend http://web:8080; + proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-Host $http_host; @@ -27,36 +34,36 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location /api/ { - proxy_pass http://api:8080; + proxy_pass $api_backend; proxy_buffering off; proxy_read_timeout 3600s; } location /device/ { - proxy_pass http://api:8080; + proxy_pass $api_backend; proxy_buffering off; } location /callbacks/ { - proxy_pass http://callbacks:8081; + proxy_pass $callbacks_backend; proxy_buffering off; } location /realms/ { - proxy_pass http://keycloak:8080; + proxy_pass $keycloak_backend; } location /resources/ { - proxy_pass http://keycloak:8080; + proxy_pass $keycloak_backend; } location /private-exports/ { - proxy_pass http://minio:9000; + proxy_pass $minio_backend; proxy_request_buffering off; proxy_buffering off; } location / { - proxy_pass http://web:8080; + proxy_pass $web_backend; } } diff --git a/internal/app/app.go b/internal/app/app.go index 9f640ac..676537e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -80,7 +80,7 @@ func Run(ctx context.Context, serviceName string) error { if err != nil { return fmt.Errorf("create memory embedding provider: %w", err) } - exportStore, err = objectstore.New(cfg.ObjectStoreEndpoint, cfg.ObjectStorePublic, cfg.ObjectStoreAccessKey, cfg.ObjectStoreSecretKey, cfg.ObjectStoreBucket, cfg.ObjectStoreRegion, cfg.ObjectStoreSecure) + exportStore, err = objectstore.New(cfg.ObjectStoreEndpoint, cfg.ObjectStorePublic, cfg.ObjectStoreAccessKey, cfg.ObjectStoreSecretKey, cfg.ObjectStoreBucket, cfg.ObjectStoreRegion, cfg.ObjectStoreSecure, cfg.ObjectStorePublicSecure) if err != nil { return fmt.Errorf("create private export object store: %w", err) } diff --git a/internal/config/config.go b/internal/config/config.go index ba515d0..a16b2ba 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -86,6 +86,7 @@ type Config struct { ObjectStoreBucket string ObjectStoreRegion string ObjectStoreSecure bool + ObjectStorePublicSecure bool ExportDownloadTTL time.Duration ExportObjectTTL time.Duration MediaLocalDeleteGrace time.Duration @@ -195,6 +196,10 @@ func Load(serviceName string) (Config, error) { if err != nil { return Config{}, err } + objectStorePublicSecure, err := boolEnv("OBJECT_STORE_PUBLIC_SECURE", objectStoreSecure) + if err != nil { + return Config{}, err + } environment := stringEnv("APP_ENV", EnvironmentLocal) secretValues, err := loadSecretValues( "DATABASE_URL", "REDIS_PASSWORD", "DEVICE_LOCAL_SECRET", "RTC_APP_KEY", "RTC_TOKEN_SIGNING_SECRET", @@ -304,6 +309,7 @@ func Load(serviceName string) (Config, error) { ObjectStoreBucket: objectStoreBucket, ObjectStoreRegion: objectStoreRegion, ObjectStoreSecure: objectStoreSecure, + ObjectStorePublicSecure: objectStorePublicSecure, ExportDownloadTTL: exportDownloadTTL, ExportObjectTTL: exportObjectTTL, MediaLocalDeleteGrace: mediaLocalDeleteGrace, @@ -590,7 +596,7 @@ func (c Config) validateProduction() error { } } if c.ServiceName == "api" || c.ServiceName == "worker" { - if !c.ObjectStoreSecure { + if !c.ObjectStoreSecure || !c.ObjectStorePublicSecure { return errors.New("production object storage must use TLS") } if isLocalValue(c.ObjectStoreEndpoint) || isLocalValue(c.ObjectStorePublic) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 1145021..13f8461 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -118,6 +118,7 @@ func TestProductionAPIAcceptsHardenedConfiguration(t *testing.T) { cfg.ObjectStoreAccessKey = strings.Repeat("a", 40) cfg.ObjectStoreSecretKey = strings.Repeat("s", 40) cfg.ObjectStoreSecure = true + cfg.ObjectStorePublicSecure = true cfg.EmbeddingProvider = "ark" cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3" cfg.ArkAPIKey = strings.Repeat("k", 40) @@ -150,6 +151,7 @@ func TestProductionRejectsReusedSigningSecrets(t *testing.T) { cfg.ObjectStoreAccessKey = strings.Repeat("a", 40) cfg.ObjectStoreSecretKey = strings.Repeat("s", 40) cfg.ObjectStoreSecure = true + cfg.ObjectStorePublicSecure = true cfg.EmbeddingProvider = "ark" cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3" cfg.ArkAPIKey = strings.Repeat("k", 40) @@ -182,6 +184,7 @@ func TestProductionWorkerRequiresRealArkContentGenerator(t *testing.T) { cfg.ObjectStoreAccessKey = strings.Repeat("a", 40) cfg.ObjectStoreSecretKey = strings.Repeat("s", 40) cfg.ObjectStoreSecure = true + cfg.ObjectStorePublicSecure = true if err = cfg.Validate(); err == nil || !strings.Contains(err.Error(), "content generation") { t.Fatalf("Validate() error=%v, want local content generator production gate", err) } diff --git a/internal/objectstore/store.go b/internal/objectstore/store.go index 6b269bf..95de612 100644 --- a/internal/objectstore/store.go +++ b/internal/objectstore/store.go @@ -26,16 +26,17 @@ type Store struct { bucket string } -func New(endpoint, publicEndpoint, accessKey, secretKey, bucket, region string, secure bool) (*Store, error) { +func New(endpoint, publicEndpoint, accessKey, secretKey, bucket, region string, secure, publicSecure bool) (*Store, error) { if endpoint == "" || publicEndpoint == "" || accessKey == "" || secretKey == "" || bucket == "" || region == "" { return nil, errors.New("object store endpoints, credentials, bucket and region are required") } - options := &minio.Options{Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: secure, Region: region} - internalClient, err := minio.New(endpoint, options) + internalOptions := &minio.Options{Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: secure, Region: region} + internalClient, err := minio.New(endpoint, internalOptions) if err != nil { return nil, fmt.Errorf("create internal object store client: %w", err) } - publicClient, err := minio.New(publicEndpoint, options) + publicOptions := &minio.Options{Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: publicSecure, Region: region} + publicClient, err := minio.New(publicEndpoint, publicOptions) if err != nil { return nil, fmt.Errorf("create public object store signer: %w", err) } diff --git a/internal/objectstore/store_test.go b/internal/objectstore/store_test.go index 0c5e24c..240d964 100644 --- a/internal/objectstore/store_test.go +++ b/internal/objectstore/store_test.go @@ -15,10 +15,10 @@ import ( ) func TestStoreValidationAndOfflineSigning(t *testing.T) { - if _, err := New("", "localhost:59000", "key", "secret", "bucket", "us-east-1", false); err == nil { + if _, err := New("", "localhost:59000", "key", "secret", "bucket", "us-east-1", false, false); err == nil { t.Fatal("missing internal endpoint was accepted") } - store, err := New("localhost:59000", "localhost:59000", "local_minio", "local_minio_password", "private-exports", "us-east-1", false) + store, err := New("localhost:59000", "localhost:59000", "local_minio", "local_minio_password", "private-exports", "us-east-1", false, true) if err != nil { t.Fatal(err) } @@ -31,7 +31,7 @@ func TestStoreValidationAndOfflineSigning(t *testing.T) { t.Fatal(err) } parsed, err := url.Parse(download) - if err != nil || parsed.Query().Get("X-Amz-Signature") == "" || parsed.Query().Get("response-content-disposition") != `attachment; filename="export.json"` { + if err != nil || parsed.Scheme != "https" || parsed.Query().Get("X-Amz-Signature") == "" || parsed.Query().Get("response-content-disposition") != `attachment; filename="export.json"` { t.Fatalf("download URL=%q parsed=%+v err=%v", download, parsed, err) } if _, err := store.PresignUpload(ctx, "synthetic/firmware.bin", 0); err == nil { @@ -65,7 +65,7 @@ func TestStoreMinIOObjectLifecycle(t *testing.T) { if endpoint == "" { t.Skip("TEST_OBJECT_STORE_ENDPOINT is not set") } - store, err := New(endpoint, endpoint, "local_minio", "local_minio_password", "private-exports", "us-east-1", false) + store, err := New(endpoint, endpoint, "local_minio", "local_minio_password", "private-exports", "us-east-1", false, false) if err != nil { t.Fatal(err) }