From efdb5eeb2bc6525c7d5957813a66ff3bc05d6f96 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 22 Sep 2026 08:37:46 +0000 Subject: [PATCH] Default a scheme-less s3.* endpoint to TLS (closes #158) With the s3.* config form and an endpoint written without a scheme, use_ssl being omitted built an http:// endpoint, while config.example.yml documented use_ssl as defaulting to true. Over plain HTTP a network observer sees manifests, object names, sizes and the access key id, and can alter responses. use_ssl is now *bool: omitted (nil) means the default, TLS; only an explicit use_ssl: false forces plain HTTP. This matches the s3:// URL form, which already defaults to TLS. The config init template dropped its misleading use_ssl line from the s3:// block (that key is never read for URLs; ?ssl=false controls TLS there) and points at ?ssl=false instead. Model: opus-4-8 --- internal/cli/config.go | 2 +- internal/config/config.go | 6 ++-- internal/storage/module.go | 5 +-- internal/storage/module_test.go | 61 +++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 internal/storage/module_test.go diff --git a/internal/cli/config.go b/internal/cli/config.go index 98fc46a..212fb5a 100644 --- a/internal/cli/config.go +++ b/internal/cli/config.go @@ -192,8 +192,8 @@ storage_url: "" # access_key_id: YOUR_ACCESS_KEY # secret_access_key: YOUR_SECRET_KEY # # region: us-east-1 # Default: us-east-1 -# # use_ssl: true # Default: true # # part_size: 5MB # Multipart upload part size. Default: 5MB +# # For the s3:// form, disable TLS with ?ssl=false in the URL, not use_ssl. # ─── OPTIONAL ──────────────────────────────────────────────────────────────── diff --git a/internal/config/config.go b/internal/config/config.go index 27e7196..4e9fcc8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -162,8 +162,10 @@ type S3Config struct { AccessKeyID string `yaml:"access_key_id"` SecretAccessKey string `yaml:"secret_access_key"` Region string `yaml:"region"` - UseSSL bool `yaml:"use_ssl"` - PartSize Size `yaml:"part_size"` + // UseSSL selects HTTPS for a scheme-less endpoint. Omitted (nil) means + // the default, TLS; set it to false only to force plain HTTP. + UseSSL *bool `yaml:"use_ssl"` + PartSize Size `yaml:"part_size"` } // Path wraps the config file path for fx dependency injection. diff --git a/internal/storage/module.go b/internal/storage/module.go index 8699a7c..0e400fd 100644 --- a/internal/storage/module.go +++ b/internal/storage/module.go @@ -111,10 +111,11 @@ func storerFromParsedS3URL(parsed *URL, cfg *config.Config) (Storer, error) { func storerFromLegacyS3Config(cfg *config.Config) (Storer, error) { endpoint := cfg.S3.Endpoint - // Ensure protocol is present + // Ensure protocol is present. Absent an explicit use_ssl, default to TLS; + // plain HTTP only when use_ssl is written as false. if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { - if cfg.S3.UseSSL { + if cfg.S3.UseSSL == nil || *cfg.S3.UseSSL { endpoint = "https://" + endpoint } else { endpoint = "http://" + endpoint diff --git a/internal/storage/module_test.go b/internal/storage/module_test.go new file mode 100644 index 0000000..c2ae67d --- /dev/null +++ b/internal/storage/module_test.go @@ -0,0 +1,61 @@ +package storage_test + +import ( + "strings" + "testing" + + "sneak.berlin/go/vaultik/internal/config" + "sneak.berlin/go/vaultik/internal/storage" +) + +// legacyS3Config returns a minimal s3.* (no storage_url) configuration with a +// scheme-less endpoint. useSSL mirrors the config file: nil means the key is +// omitted, a pointer means it was written explicitly. +func legacyS3Config(useSSL *bool) *config.Config { + return &config.Config{ + S3: config.S3Config{ + Endpoint: "s3.example.com", + Bucket: "bucket", + AccessKeyID: "key", + SecretAccessKey: "secret", + Region: "us-east-1", + UseSSL: useSSL, + }, + } +} + +// endpointScheme builds the storer from cfg and returns the scheme its +// resolved endpoint carries (Info().Location is "endpoint/bucket"). +func endpointScheme(t *testing.T, cfg *config.Config) string { + t.Helper() + + storer, err := storage.NewStorer(cfg) + if err != nil { + t.Fatalf("NewStorer: %v", err) + } + + location := storer.Info().Location + switch { + case strings.HasPrefix(location, "https://"): + return "https" + case strings.HasPrefix(location, "http://"): + return "http" + default: + t.Fatalf("endpoint has no http(s) scheme: %q", location) + + return "" + } +} + +func TestLegacyS3SchemelessEndpointDefaultsToTLS(t *testing.T) { + t.Parallel() + + if got := endpointScheme(t, legacyS3Config(nil)); got != "https" { + t.Errorf("use_ssl omitted: got %q scheme, want https", got) + } + + no := false + if got := endpointScheme(t, legacyS3Config(&no)); got != "http" { + t.Errorf("use_ssl: false: got %q scheme, want http", got) + } +}