feat(s3): support WEED_S3_SSE_KEY env var for SSE-S3 KEK (#8904)
* feat(s3): support WEED_S3_SSE_KEY env var for SSE-S3 KEK Add support for providing the SSE-S3 Key Encryption Key (KEK) via the WEED_S3_SSE_KEY environment variable (hex-encoded 256-bit key). This avoids storing the master key in plaintext on the filer at /etc/s3/sse_kek. Key source priority: 1. WEED_S3_SSE_KEY environment variable (recommended) 2. Existing filer KEK at /etc/s3/sse_kek (backward compatible) 3. Auto-generate and save to filer (deprecated for new deployments) Existing deployments with a filer-stored KEK continue to work unchanged. A deprecation warning is logged when auto-generating a new filer KEK. * refactor(s3): derive KEK from any string via HKDF instead of requiring hex Accept any secret string in WEED_S3_SSE_KEY and derive a 256-bit key using HKDF-SHA256 instead of requiring a hex-encoded key. This is simpler for users — no need to generate hex, just set a passphrase. * feat(s3): add WEED_S3_SSE_KEK and WEED_S3_SSE_KEY env vars for KEK Two env vars for providing the SSE-S3 Key Encryption Key: - WEED_S3_SSE_KEK: hex-encoded, same format as /etc/s3/sse_kek. If the filer file also exists, they must match. - WEED_S3_SSE_KEY: any string, 256-bit key derived via HKDF-SHA256. Refuses to start if /etc/s3/sse_kek exists (must delete first). Only one may be set. Existing filer-stored KEKs continue to work. Auto-generating and storing new KEKs on filer is deprecated. * fix(s3): stop auto-generating KEK, fail only when SSE-S3 is used Instead of auto-generating a KEK and storing it on the filer when no key source is configured, simply leave SSE-S3 disabled. Encrypt and decrypt operations return a clear error directing the user to set WEED_S3_SSE_KEK or WEED_S3_SSE_KEY. * refactor(s3): move SSE-S3 KEK config to security.toml Move KEK configuration from standalone env vars to security.toml's new [sse_s3] section, following the same pattern as JWT keys and TLS certs. [sse_s3] kek = "" # hex-encoded 256-bit key (same format as /etc/s3/sse_kek) key = "" # any string, HKDF-derived Viper's WEED_ prefix auto-mapping provides env var support: WEED_SSE_S3_KEK and WEED_SSE_S3_KEY. All existing behavior is preserved: filer KEK fallback, mismatch detection, and HKDF derivation. * refactor(s3): rename SSE-S3 config keys to s3.sse.kek / s3.sse.key Use [s3.sse] section in security.toml, matching the existing naming convention (e.g. [s3.*]). Env vars: WEED_S3_SSE_KEK, WEED_S3_SSE_KEY. * fix(s3): address code review findings for SSE-S3 KEK - Don't hold mutex during filer retry loop (up to 20s of sleep). Lock only to write filerClient and superKey. - Remove dead generateAndSaveSuperKeyToFiler and unused constants. - Return error from deriveKeyFromSecret instead of ignoring it. - Fix outdated doc comment on InitializeWithFiler. - Use t.Setenv in tests instead of manual os.Setenv/Unsetenv. * fix(s3): don't block startup on filer errors when KEK is configured - When s3.sse.kek is set, a temporarily unreachable filer no longer prevents startup. The filer consistency check becomes best-effort with a warning. - Same treatment for s3.sse.key: filer unreachable logs a warning instead of failing. - Rewrite error messages to suggest migration instead of file deletion, avoiding the risk of orphaning encrypted data. Finding 3 (restore auto-generation) intentionally skipped — auto-gen was removed by design to avoid storing plaintext KEK on filer. * fix(test): set WEED_S3_SSE_KEY in SSE integration test server startup SSE-S3 no longer auto-generates a KEK, so integration tests must provide one. Set WEED_S3_SSE_KEY=test-sse-s3-key in all weed mini invocations in the test Makefile.
This commit is contained in:
@@ -2,6 +2,7 @@ package s3api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
// TestSSES3EncryptionDecryption tests basic SSE-S3 encryption and decryption
|
||||
@@ -681,6 +683,125 @@ func TestSSES3InvalidMetadataDeserialization(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// setViperKey is a test helper that sets a config key via its WEED_ env var.
|
||||
func setViperKey(t *testing.T, key, value string) {
|
||||
t.Helper()
|
||||
util.GetViper().SetDefault(key, "")
|
||||
t.Setenv("WEED_"+strings.ReplaceAll(strings.ToUpper(key), ".", "_"), value)
|
||||
}
|
||||
|
||||
// TestSSES3KEKConfig tests that sse_s3.kek (hex format) is used as KEK
|
||||
func TestSSES3KEKConfig(t *testing.T) {
|
||||
testKey := make([]byte, 32)
|
||||
for i := range testKey {
|
||||
testKey[i] = byte(i + 50)
|
||||
}
|
||||
setViperKey(t, sseS3KEKConfigKey, hex.EncodeToString(testKey))
|
||||
|
||||
km := NewSSES3KeyManager()
|
||||
err := km.InitializeWithFiler(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("InitializeWithFiler failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(km.superKey, testKey) {
|
||||
t.Errorf("superKey mismatch: expected %x, got %x", testKey, km.superKey)
|
||||
}
|
||||
|
||||
// Round-trip DEK encryption
|
||||
dek := make([]byte, 32)
|
||||
for i := range dek {
|
||||
dek[i] = byte(i)
|
||||
}
|
||||
encrypted, nonce, err := km.encryptKeyWithSuperKey(dek)
|
||||
if err != nil {
|
||||
t.Fatalf("encryptKeyWithSuperKey failed: %v", err)
|
||||
}
|
||||
decrypted, err := km.decryptKeyWithSuperKey(encrypted, nonce)
|
||||
if err != nil {
|
||||
t.Fatalf("decryptKeyWithSuperKey failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decrypted, dek) {
|
||||
t.Error("round-trip DEK mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSES3KEKConfigInvalidHex tests rejection of bad hex
|
||||
func TestSSES3KEKConfigInvalidHex(t *testing.T) {
|
||||
setViperKey(t, sseS3KEKConfigKey, "not-valid-hex")
|
||||
|
||||
km := NewSSES3KeyManager()
|
||||
err := km.InitializeWithFiler(nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid hex, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "hex-encoded") {
|
||||
t.Errorf("expected hex error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSES3KEKConfigWrongSize tests rejection of wrong-size hex key
|
||||
func TestSSES3KEKConfigWrongSize(t *testing.T) {
|
||||
setViperKey(t, sseS3KEKConfigKey, hex.EncodeToString(make([]byte, 16)))
|
||||
|
||||
km := NewSSES3KeyManager()
|
||||
err := km.InitializeWithFiler(nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong key size, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "32 bytes") {
|
||||
t.Errorf("expected size error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSES3KeyConfig tests that sse_s3.key (any string, HKDF) works
|
||||
func TestSSES3KeyConfig(t *testing.T) {
|
||||
setViperKey(t, sseS3KeyConfigKey, "my-secret-passphrase")
|
||||
|
||||
km := NewSSES3KeyManager()
|
||||
err := km.InitializeWithFiler(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("InitializeWithFiler failed: %v", err)
|
||||
}
|
||||
|
||||
if len(km.superKey) != SSES3KeySize {
|
||||
t.Fatalf("expected %d-byte superKey, got %d", SSES3KeySize, len(km.superKey))
|
||||
}
|
||||
|
||||
// Deterministic: same input → same output
|
||||
expected, err := deriveKeyFromSecret("my-secret-passphrase")
|
||||
if err != nil {
|
||||
t.Fatalf("deriveKeyFromSecret failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(km.superKey, expected) {
|
||||
t.Errorf("superKey mismatch: expected %x, got %x", expected, km.superKey)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSES3KeyConfigDifferentSecrets tests different strings produce different keys
|
||||
func TestSSES3KeyConfigDifferentSecrets(t *testing.T) {
|
||||
k1, _ := deriveKeyFromSecret("secret-one")
|
||||
k2, _ := deriveKeyFromSecret("secret-two")
|
||||
if bytes.Equal(k1, k2) {
|
||||
t.Error("different secrets should produce different keys")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSSES3BothConfigsReject tests that setting both config keys is rejected
|
||||
func TestSSES3BothConfigsReject(t *testing.T) {
|
||||
setViperKey(t, sseS3KEKConfigKey, hex.EncodeToString(make([]byte, 32)))
|
||||
setViperKey(t, sseS3KeyConfigKey, "some-passphrase")
|
||||
|
||||
km := NewSSES3KeyManager()
|
||||
err := km.InitializeWithFiler(nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when both configs set, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "only one") {
|
||||
t.Errorf("expected 'only one' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetSSES3Headers tests SSE-S3 header generation
|
||||
func TestGetSSES3Headers(t *testing.T) {
|
||||
headers := GetSSES3Headers()
|
||||
|
||||
Reference in New Issue
Block a user