fix: separate public object store TLS
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
"http://127.0.0.1:3100/*",
|
"http://127.0.0.1:3100/*",
|
||||||
"http://121.41.46.238/*",
|
"http://121.41.46.238/*",
|
||||||
"https://121.41.46.238/*",
|
"https://121.41.46.238/*",
|
||||||
|
"https://121.41.46.238:80/*",
|
||||||
"http://localhost:5173/*",
|
"http://localhost:5173/*",
|
||||||
"http://127.0.0.1:5173/*"
|
"http://127.0.0.1:5173/*"
|
||||||
],
|
],
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"http://127.0.0.1:3100",
|
"http://127.0.0.1:3100",
|
||||||
"http://121.41.46.238",
|
"http://121.41.46.238",
|
||||||
"https://121.41.46.238",
|
"https://121.41.46.238",
|
||||||
|
"https://121.41.46.238:80",
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
"http://127.0.0.1:5173"
|
"http://127.0.0.1:5173"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,4 +21,4 @@ docker compose -f compose.yaml -f deploy/server/compose.override.yaml \
|
|||||||
mock-provider api worker web gateway
|
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生产覆盖层。
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ x-server-backend-environment: &server-backend-environment
|
|||||||
OBJECT_STORE_SECRET_KEY: ${MINIO_SECRET_KEY}
|
OBJECT_STORE_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||||
OBJECT_STORE_EXPORT_BUCKET: private-exports
|
OBJECT_STORE_EXPORT_BUCKET: private-exports
|
||||||
OBJECT_STORE_REGION: us-east-1
|
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}
|
CURSOR_SIGNING_SECRET: ${CURSOR_SIGNING_SECRET}
|
||||||
SHARE_TOKEN_SIGNING_SECRET: ${SHARE_TOKEN_SIGNING_SECRET}
|
SHARE_TOKEN_SIGNING_SECRET: ${SHARE_TOKEN_SIGNING_SECRET}
|
||||||
EMBEDDING_PROVIDER: local
|
EMBEDDING_PROVIDER: local
|
||||||
@@ -158,7 +159,7 @@ services:
|
|||||||
gateway:
|
gateway:
|
||||||
image: nginx:1.30-alpine
|
image: nginx:1.30-alpine
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:443"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./deploy/server/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./deploy/server/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ server {
|
|||||||
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
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_http_version 1.1;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Forwarded-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;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://api:8080;
|
proxy_pass $api_backend;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
proxy_read_timeout 3600s;
|
proxy_read_timeout 3600s;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /device/ {
|
location /device/ {
|
||||||
proxy_pass http://api:8080;
|
proxy_pass $api_backend;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /callbacks/ {
|
location /callbacks/ {
|
||||||
proxy_pass http://callbacks:8081;
|
proxy_pass $callbacks_backend;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /realms/ {
|
location /realms/ {
|
||||||
proxy_pass http://keycloak:8080;
|
proxy_pass $keycloak_backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /resources/ {
|
location /resources/ {
|
||||||
proxy_pass http://keycloak:8080;
|
proxy_pass $keycloak_backend;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /private-exports/ {
|
location /private-exports/ {
|
||||||
proxy_pass http://minio:9000;
|
proxy_pass $minio_backend;
|
||||||
proxy_request_buffering off;
|
proxy_request_buffering off;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://web:8080;
|
proxy_pass $web_backend;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -80,7 +80,7 @@ func Run(ctx context.Context, serviceName string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create memory embedding provider: %w", err)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("create private export object store: %w", err)
|
return fmt.Errorf("create private export object store: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ type Config struct {
|
|||||||
ObjectStoreBucket string
|
ObjectStoreBucket string
|
||||||
ObjectStoreRegion string
|
ObjectStoreRegion string
|
||||||
ObjectStoreSecure bool
|
ObjectStoreSecure bool
|
||||||
|
ObjectStorePublicSecure bool
|
||||||
ExportDownloadTTL time.Duration
|
ExportDownloadTTL time.Duration
|
||||||
ExportObjectTTL time.Duration
|
ExportObjectTTL time.Duration
|
||||||
MediaLocalDeleteGrace time.Duration
|
MediaLocalDeleteGrace time.Duration
|
||||||
@@ -195,6 +196,10 @@ func Load(serviceName string) (Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
|
objectStorePublicSecure, err := boolEnv("OBJECT_STORE_PUBLIC_SECURE", objectStoreSecure)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
environment := stringEnv("APP_ENV", EnvironmentLocal)
|
environment := stringEnv("APP_ENV", EnvironmentLocal)
|
||||||
secretValues, err := loadSecretValues(
|
secretValues, err := loadSecretValues(
|
||||||
"DATABASE_URL", "REDIS_PASSWORD", "DEVICE_LOCAL_SECRET", "RTC_APP_KEY", "RTC_TOKEN_SIGNING_SECRET",
|
"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,
|
ObjectStoreBucket: objectStoreBucket,
|
||||||
ObjectStoreRegion: objectStoreRegion,
|
ObjectStoreRegion: objectStoreRegion,
|
||||||
ObjectStoreSecure: objectStoreSecure,
|
ObjectStoreSecure: objectStoreSecure,
|
||||||
|
ObjectStorePublicSecure: objectStorePublicSecure,
|
||||||
ExportDownloadTTL: exportDownloadTTL,
|
ExportDownloadTTL: exportDownloadTTL,
|
||||||
ExportObjectTTL: exportObjectTTL,
|
ExportObjectTTL: exportObjectTTL,
|
||||||
MediaLocalDeleteGrace: mediaLocalDeleteGrace,
|
MediaLocalDeleteGrace: mediaLocalDeleteGrace,
|
||||||
@@ -590,7 +596,7 @@ func (c Config) validateProduction() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.ServiceName == "api" || c.ServiceName == "worker" {
|
if c.ServiceName == "api" || c.ServiceName == "worker" {
|
||||||
if !c.ObjectStoreSecure {
|
if !c.ObjectStoreSecure || !c.ObjectStorePublicSecure {
|
||||||
return errors.New("production object storage must use TLS")
|
return errors.New("production object storage must use TLS")
|
||||||
}
|
}
|
||||||
if isLocalValue(c.ObjectStoreEndpoint) || isLocalValue(c.ObjectStorePublic) {
|
if isLocalValue(c.ObjectStoreEndpoint) || isLocalValue(c.ObjectStorePublic) {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ func TestProductionAPIAcceptsHardenedConfiguration(t *testing.T) {
|
|||||||
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
||||||
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
||||||
cfg.ObjectStoreSecure = true
|
cfg.ObjectStoreSecure = true
|
||||||
|
cfg.ObjectStorePublicSecure = true
|
||||||
cfg.EmbeddingProvider = "ark"
|
cfg.EmbeddingProvider = "ark"
|
||||||
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
|
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
|
||||||
cfg.ArkAPIKey = strings.Repeat("k", 40)
|
cfg.ArkAPIKey = strings.Repeat("k", 40)
|
||||||
@@ -150,6 +151,7 @@ func TestProductionRejectsReusedSigningSecrets(t *testing.T) {
|
|||||||
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
||||||
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
||||||
cfg.ObjectStoreSecure = true
|
cfg.ObjectStoreSecure = true
|
||||||
|
cfg.ObjectStorePublicSecure = true
|
||||||
cfg.EmbeddingProvider = "ark"
|
cfg.EmbeddingProvider = "ark"
|
||||||
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
|
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
|
||||||
cfg.ArkAPIKey = strings.Repeat("k", 40)
|
cfg.ArkAPIKey = strings.Repeat("k", 40)
|
||||||
@@ -182,6 +184,7 @@ func TestProductionWorkerRequiresRealArkContentGenerator(t *testing.T) {
|
|||||||
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
||||||
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
||||||
cfg.ObjectStoreSecure = true
|
cfg.ObjectStoreSecure = true
|
||||||
|
cfg.ObjectStorePublicSecure = true
|
||||||
if err = cfg.Validate(); err == nil || !strings.Contains(err.Error(), "content generation") {
|
if err = cfg.Validate(); err == nil || !strings.Contains(err.Error(), "content generation") {
|
||||||
t.Fatalf("Validate() error=%v, want local content generator production gate", err)
|
t.Fatalf("Validate() error=%v, want local content generator production gate", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,17 @@ type Store struct {
|
|||||||
bucket string
|
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 == "" {
|
if endpoint == "" || publicEndpoint == "" || accessKey == "" || secretKey == "" || bucket == "" || region == "" {
|
||||||
return nil, errors.New("object store endpoints, credentials, bucket and region are required")
|
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}
|
internalOptions := &minio.Options{Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: secure, Region: region}
|
||||||
internalClient, err := minio.New(endpoint, options)
|
internalClient, err := minio.New(endpoint, internalOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create internal object store client: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create public object store signer: %w", err)
|
return nil, fmt.Errorf("create public object store signer: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestStoreValidationAndOfflineSigning(t *testing.T) {
|
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")
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ func TestStoreValidationAndOfflineSigning(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
parsed, err := url.Parse(download)
|
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)
|
t.Fatalf("download URL=%q parsed=%+v err=%v", download, parsed, err)
|
||||||
}
|
}
|
||||||
if _, err := store.PresignUpload(ctx, "synthetic/firmware.bin", 0); err == nil {
|
if _, err := store.PresignUpload(ctx, "synthetic/firmware.bin", 0); err == nil {
|
||||||
@@ -65,7 +65,7 @@ func TestStoreMinIOObjectLifecycle(t *testing.T) {
|
|||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
t.Skip("TEST_OBJECT_STORE_ENDPOINT is not set")
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user