diff --git a/cmd/config.go b/cmd/config.go index 19d6bba..0541f91 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -59,6 +59,7 @@ type EnvironmentConfig struct { MigrationTools MigrationToolsConfig `yaml:"migration_tools"` Clickhouse ClickhouseConfig `yaml:"clickhouse"` Fluentbit FluentbitConfig `yaml:"fluentbit"` + Nginx NginxConfig `yaml:"nginx"` } type GlobalConfig struct { @@ -72,6 +73,11 @@ type FeatureConfig struct { Utapi UtapiFeatureConfig `yaml:"utapi"` Migration MigrationFeatureConfig `yaml:"migration"` AccessLogging AccessLoggingFeatureConfig `yaml:"access_logging"` + S3Frontend S3FrontendFeatureConfig `yaml:"s3_frontend"` +} + +type S3FrontendFeatureConfig struct { + Enabled bool `yaml:"enabled"` } type ScubaFeatureConfig struct { @@ -228,6 +234,12 @@ type AccessLoggingFeatureConfig struct { Enabled bool `yaml:"enabled"` } +type NginxConfig struct { + Image string `yaml:"image"` + HTTPPort uint16 `yaml:"http_port"` + SSLPort uint16 `yaml:"ssl_port"` +} + func DefaultEnvironmentConfig() EnvironmentConfig { return EnvironmentConfig{ Global: GlobalConfig{ @@ -290,6 +302,10 @@ func DefaultEnvironmentConfig() EnvironmentConfig { MigrationTools: MigrationToolsConfig{}, Clickhouse: ClickhouseConfig{}, Fluentbit: FluentbitConfig{}, + Nginx: NginxConfig{ + HTTPPort: 80, + SSLPort: 443, + }, } } diff --git a/cmd/configure.go b/cmd/configure.go index 352b281..24291cd 100644 --- a/cmd/configure.go +++ b/cmd/configure.go @@ -1,9 +1,17 @@ package main import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" + "math/big" "os" "path/filepath" + "time" "github.com/rs/zerolog/log" ) @@ -77,6 +85,7 @@ func configureEnv(cfg EnvironmentConfig, envDir string) error { generateMigrationToolsConfig, generateClickhouseConfig, generateFluentbitConfig, + generateNginxConfig, } configDir := filepath.Join(envDir, "config") @@ -234,3 +243,92 @@ func generateFluentbitConfig(cfg EnvironmentConfig, path string) error { return renderTemplates(cfg, "templates/fluentbit", filepath.Join(path, "fluentbit"), templates) } + +func generateNginxConfig(cfg EnvironmentConfig, path string) error { + if !cfg.Features.S3Frontend.Enabled { + return nil + } + + nginxDir := filepath.Join(path, "nginx") + + if err := renderTemplateToFile( + getTemplates(), + "templates/nginx/nginx.conf", + cfg, + filepath.Join(nginxDir, "nginx.conf"), + ); err != nil { + return err + } + + return generateTLSCertificate(nginxDir, "s3-frontend.key", "s3-frontend.crt") +} + +// generateTLSCertificate creates a self-signed TLS certificate and key pair. +func generateTLSCertificate(dir, keyName, certName string) error { + keyPath := filepath.Join(dir, keyName) + certPath := filepath.Join(dir, certName) + + // Skip if both files already exist + _, keyErr := os.Stat(keyPath) + _, certErr := os.Stat(certPath) + if keyErr == nil && certErr == nil { + log.Debug().Str("dir", dir).Msg("TLS certificate already exists, skipping generation") + return nil + } + + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return fmt.Errorf("failed to generate TLS key: %w", err) + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return fmt.Errorf("failed to generate serial number: %w", err) + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Scality Workbench"}, + }, + DNSNames: []string{"localhost", "s3.docker.test"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + return fmt.Errorf("failed to create TLS certificate: %w", err) + } + + certFile, err := os.Create(certPath) + if err != nil { + return fmt.Errorf("failed to create cert file: %w", err) + } + defer func() { _ = certFile.Close() }() + + if err := pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}); err != nil { + return fmt.Errorf("failed to write TLS certificate: %w", err) + } + + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + return fmt.Errorf("failed to marshal TLS key: %w", err) + } + + keyFile, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to create key file: %w", err) + } + defer func() { _ = keyFile.Close() }() + + if err := pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil { + return fmt.Errorf("failed to write TLS key: %w", err) + } + + log.Info().Str("dir", dir).Msg("Generated self-signed TLS certificate") + return nil +} diff --git a/cmd/util.go b/cmd/util.go index 27d68f1..886ce88 100644 --- a/cmd/util.go +++ b/cmd/util.go @@ -92,6 +92,10 @@ func getComposeProfiles(cfg EnvironmentConfig) []string { profiles = append(profiles, "feature-access-logging") } + if cfg.Features.S3Frontend.Enabled { + profiles = append(profiles, "feature-s3-frontend") + } + return profiles } diff --git a/templates/cloudserver/config-v9.json b/templates/cloudserver/config-v9.json index e2c042f..23e5532 100644 --- a/templates/cloudserver/config-v9.json +++ b/templates/cloudserver/config-v9.json @@ -125,9 +125,16 @@ } }, "requests": { + {{- if .Features.S3Frontend.Enabled }} + "viaProxy": true, + "trustedProxyCIDRs": ["127.0.0.1/8"], + "extractClientIPFromHeader": "x-forwarded-for", + "extractProtocolFromHeader": "x-forwarded-proto" + {{- else }} "viaProxy": false, "trustedProxyCIDRs": [], "extractClientIPFromHeader": "" + {{- end }} }, {{ if .Features.BucketNotifications.Enabled }} "bucketNotificationDestinations": [ diff --git a/templates/global/defaults.env b/templates/global/defaults.env index ac25d3e..126ee95 100644 --- a/templates/global/defaults.env +++ b/templates/global/defaults.env @@ -11,6 +11,7 @@ UTAPI_IMAGE="{{ .Utapi.Image }}" MIGRATION_TOOLS_IMAGE="{{ .MigrationTools.Image }}" CLICKHOUSE_IMAGE="{{ .Clickhouse.Image }}" FLUENTBIT_IMAGE="{{ .Fluentbit.Image }}" +NGINX_IMAGE="{{ .Nginx.Image }}" METADATA_S3_DB_VERSION="{{ .S3Metadata.VFormat }}" CLOUDSERVER_ENABLE_NULL_VERSION_COMPAT_MODE="{{ .Cloudserver.EnableNullVersionCompatMode }}" diff --git a/templates/global/docker-compose.yaml b/templates/global/docker-compose.yaml index 62213c0..c2e0e6f 100644 --- a/templates/global/docker-compose.yaml +++ b/templates/global/docker-compose.yaml @@ -70,6 +70,17 @@ services: profiles: - base + s3-frontend: + image: ${NGINX_IMAGE} + container_name: workbench-s3-frontend + network_mode: host + volumes: + - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./config/nginx/s3-frontend.crt:/certs/s3-frontend.crt:ro + - ./config/nginx/s3-frontend.key:/certs/s3-frontend.key:ro + profiles: + - feature-s3-frontend + fluentbit: image: ${FLUENTBIT_IMAGE} container_name: workbench-fluentbit diff --git a/templates/global/values.yaml b/templates/global/values.yaml index 737f60f..017a332 100644 --- a/templates/global/values.yaml +++ b/templates/global/values.yaml @@ -25,6 +25,9 @@ features: access_logging: enabled: false + s3_frontend: + enabled: false + cloudserver: image: ghcr.io/scality/cloudserver:9.2.22 @@ -63,3 +66,8 @@ clickhouse: fluentbit: image: fluent/fluent-bit:3.2.2 + +nginx: + image: nginx:1.27-alpine + http_port: 80 + ssl_port: 443 diff --git a/templates/nginx/nginx.conf b/templates/nginx/nginx.conf new file mode 100644 index 0000000..f54b59d --- /dev/null +++ b/templates/nginx/nginx.conf @@ -0,0 +1,98 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + map $http_authorization $is_iam { + default "0"; + "~Credential=[^/]+\/[^/]+\/[^/]+\/iam\/aws4_request" "1"; + } + + map $http_authorization $is_sts { + default "0"; + "~Credential=[^/]+\/[^/]+\/[^/]+\/sts\/aws4_request" "1"; + } + + large_client_header_buffers 4 8224; + + proxy_cache off; + tcp_nodelay on; + underscores_in_headers on; + ignore_invalid_headers off; + + upstream s3 { + keepalive 256; + server 127.0.0.1:8000 fail_timeout=0s max_fails=0; + } + +{{ if .Features.Scuba.Enabled }} + upstream scuba { + keepalive 256; + server 127.0.0.1:9000; + } +{{ end }} + + upstream iam { + keepalive 256; + server 127.0.0.1:8600; + } + + upstream sts { + keepalive 256; + server 127.0.0.1:8800; + } + + server { + listen {{ .Nginx.HTTPPort }}; + listen {{ .Nginx.SSLPort }} ssl; + + server_tokens off; + + ssl_certificate /certs/s3-frontend.crt; + ssl_certificate_key /certs/s3-frontend.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_session_timeout 5m; + ssl_session_cache shared:SSL:10m; + + location / { + proxy_request_buffering off; + proxy_buffering off; + + proxy_pass http://s3; + + proxy_http_version 1.1; + proxy_set_header Host $http_host; + proxy_set_header Connection ""; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-SSL-Cipher $ssl_cipher; + proxy_set_header X-SSL-Protocol $ssl_protocol; + + proxy_send_timeout 300s; + proxy_read_timeout 300s; + + client_max_body_size 0; + + if ($is_iam) { + proxy_pass http://iam; + } + + if ($is_sts) { + proxy_pass http://sts; + } + +{{ if .Features.Scuba.Enabled }} + location /_/sur/ { + proxy_set_header Host $http_host; + proxy_set_header proxy_path $uri; + proxy_pass http://scuba/; + } +{{ end }} + } + } +}