mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 10:52:04 +00:00
Merge branch 'dev' of github.com:mulgadc/spinifex into dev
This commit is contained in:
@@ -1127,6 +1127,17 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("\n🔐 Generated predastore encryption key (per-node, never transmitted)")
|
||||
fmt.Printf(" Key: %s\n", predastoreKeyPath)
|
||||
|
||||
// Viperblock at-rest encryption key is cluster-wide; on a single-node init
|
||||
// there are no joiners, so generate it locally and enable encryption by
|
||||
// default for all volumes created on this install.
|
||||
viperblockKeyPath, err := writeViperblockEncryptionKey(configDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating viperblock encryption key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("\n🔐 Generated viperblock at-rest encryption key")
|
||||
fmt.Printf(" Key: %s\n", viperblockKeyPath)
|
||||
|
||||
fmt.Printf("\n🔑 Generated admin credentials (save these — they won't be shown again):\n")
|
||||
fmt.Printf(" Access Key: %s\n", bootstrapResult.AdminAccessKey)
|
||||
fmt.Printf(" Secret Key: %s\n", bootstrapResult.AdminSecretKey)
|
||||
@@ -1314,6 +1325,8 @@ func runAdminInit(cmd *cobra.Command, args []string) {
|
||||
|
||||
GPUPassthrough: gpuPassthrough,
|
||||
IPSecEnabled: ipsecEnabled,
|
||||
|
||||
EncryptionKeyFile: viperblockKeyPath,
|
||||
}
|
||||
|
||||
// Print external networking summary
|
||||
@@ -1422,6 +1435,22 @@ func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID,
|
||||
fmt.Println("\n🔐 Generated predastore encryption key (per-node, never transmitted)")
|
||||
fmt.Printf(" Key: %s\n", predastoreKeyPath)
|
||||
|
||||
// Viperblock at-rest encryption key is cluster-wide and shared: the leader
|
||||
// generates it once and distributes it to joiners via the formation server
|
||||
// so a volume sealed on any node can be opened on any other.
|
||||
viperblockKey, err := handlers_iam.GenerateMasterKey()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error generating viperblock encryption key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
viperblockKeyPath, err := saveViperblockEncryptionKey(configDir, viperblockKey)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error saving viperblock encryption key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("\n🔐 Generated viperblock at-rest encryption key (cluster-wide, shared with joiners)")
|
||||
fmt.Printf(" Key: %s\n", viperblockKeyPath)
|
||||
|
||||
// Read CA cert/key for distribution to joining nodes
|
||||
caCertData, err := os.ReadFile(filepath.Join(configDir, "ca.pem"))
|
||||
if err != nil {
|
||||
@@ -1450,6 +1479,9 @@ func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID,
|
||||
// Include master key in formation server for distribution to joining nodes
|
||||
fs.SetMasterKey(base64.StdEncoding.EncodeToString(masterKey))
|
||||
|
||||
// Distribute the cluster-wide viperblock encryption key to joiners
|
||||
fs.SetViperblockKey(base64.StdEncoding.EncodeToString(viperblockKey))
|
||||
|
||||
// Register self (init node) as the first node
|
||||
selfNode := formation.NodeInfo{
|
||||
Name: node,
|
||||
@@ -1557,6 +1589,8 @@ func runAdminInitMultiNode(cmd *cobra.Command, accessKey, secretKey, accountID,
|
||||
|
||||
OperatorEmail: email,
|
||||
|
||||
EncryptionKeyFile: viperblockKeyPath,
|
||||
|
||||
// Init node runs ovn-central locally
|
||||
OVNNBAddr: "tcp:127.0.0.1:6641",
|
||||
OVNSBAddr: "tcp:127.0.0.1:6642",
|
||||
@@ -1902,6 +1936,28 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
fmt.Printf("✅ Predastore encryption key generated: %s\n", predastoreKeyPath)
|
||||
|
||||
// Viperblock at-rest encryption key is cluster-wide: receive it from the
|
||||
// leader rather than generating one, so this node can open volumes sealed
|
||||
// elsewhere. Lenient on absence — an older leader that predates key
|
||||
// distribution returns nothing; fall back to cleartext rather than fail
|
||||
// the join.
|
||||
var viperblockKeyPath string
|
||||
if statusResp.ViperblockKey == "" {
|
||||
fmt.Println("⚠️ Leader did not provide a viperblock encryption key; at-rest encryption disabled on this node")
|
||||
} else {
|
||||
viperblockKeyBytes, err := base64.StdEncoding.DecodeString(statusResp.ViperblockKey)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error decoding viperblock encryption key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
viperblockKeyPath, err = saveViperblockEncryptionKey(configDir, viperblockKeyBytes)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "❌ Error saving viperblock encryption key: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("✅ Viperblock encryption key received from leader: %s\n", viperblockKeyPath)
|
||||
}
|
||||
|
||||
// Generate server cert signed by CA with this node's bind IP
|
||||
if err := admin.GenerateServerCertOnly(configDir, bindIP); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error generating server certificate: %v\n", err)
|
||||
@@ -1993,6 +2049,8 @@ func runAdminJoin(cmd *cobra.Command, args []string) {
|
||||
|
||||
OperatorEmail: email,
|
||||
|
||||
EncryptionKeyFile: viperblockKeyPath,
|
||||
|
||||
// Joining nodes connect to the init node's OVN NB/SB DB
|
||||
OVNNBAddr: fmt.Sprintf("tcp:%s:6641", leaderIP),
|
||||
OVNSBAddr: fmt.Sprintf("tcp:%s:6642", leaderIP),
|
||||
@@ -2278,6 +2336,7 @@ func runCertRenew(cmd *cobra.Command, _ []string) {
|
||||
type configDirs struct {
|
||||
AWSGW string
|
||||
Predastore string
|
||||
Viperblock string
|
||||
NATS string
|
||||
Spinifex string
|
||||
}
|
||||
@@ -2287,10 +2346,11 @@ func createConfigSubdirs(configDir string) (configDirs, error) {
|
||||
dirs := configDirs{
|
||||
AWSGW: filepath.Join(configDir, "awsgw"),
|
||||
Predastore: filepath.Join(configDir, "predastore"),
|
||||
Viperblock: filepath.Join(configDir, "viperblock"),
|
||||
NATS: filepath.Join(configDir, "nats"),
|
||||
Spinifex: filepath.Join(configDir, "spinifex"),
|
||||
}
|
||||
for _, dir := range []string{dirs.AWSGW, dirs.Predastore, dirs.NATS, dirs.Spinifex} {
|
||||
for _, dir := range []string{dirs.AWSGW, dirs.Predastore, dirs.Viperblock, dirs.NATS, dirs.Spinifex} {
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return configDirs{}, fmt.Errorf("create directory %s: %w", dir, err)
|
||||
}
|
||||
@@ -2412,6 +2472,43 @@ func writePredastoreEncryptionKey(configDir string) (string, error) {
|
||||
return keyPath, nil
|
||||
}
|
||||
|
||||
// writeViperblockEncryptionKey generates a fresh 32-byte AES-256 master key for
|
||||
// viperblock at-rest encryption and writes it to
|
||||
// <configDir>/viperblock/encryption.key with mode 0600. Unlike the predastore
|
||||
// key, this key is cluster-wide and shared: the leader generates it once at init
|
||||
// and distributes it to joiners via the formation server, so a volume sealed on
|
||||
// any node can be opened on any other. Loaded at startup via masterkey.LoadShared.
|
||||
func writeViperblockEncryptionKey(configDir string) (string, error) {
|
||||
viperblockDir := filepath.Join(configDir, "viperblock")
|
||||
if err := os.MkdirAll(viperblockDir, 0750); err != nil {
|
||||
return "", fmt.Errorf("create viperblock config dir: %w", err)
|
||||
}
|
||||
key, err := handlers_iam.GenerateMasterKey()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate viperblock encryption key: %w", err)
|
||||
}
|
||||
keyPath := filepath.Join(viperblockDir, "encryption.key")
|
||||
if err := handlers_iam.SaveMasterKey(keyPath, key); err != nil {
|
||||
return "", fmt.Errorf("save viperblock encryption key: %w", err)
|
||||
}
|
||||
return keyPath, nil
|
||||
}
|
||||
|
||||
// saveViperblockEncryptionKey writes an already-generated 32-byte viperblock
|
||||
// master key (received from the formation leader) to
|
||||
// <configDir>/viperblock/encryption.key with mode 0600.
|
||||
func saveViperblockEncryptionKey(configDir string, key []byte) (string, error) {
|
||||
viperblockDir := filepath.Join(configDir, "viperblock")
|
||||
if err := os.MkdirAll(viperblockDir, 0750); err != nil {
|
||||
return "", fmt.Errorf("create viperblock config dir: %w", err)
|
||||
}
|
||||
keyPath := filepath.Join(viperblockDir, "encryption.key")
|
||||
if err := handlers_iam.SaveMasterKey(keyPath, key); err != nil {
|
||||
return "", fmt.Errorf("save viperblock encryption key: %w", err)
|
||||
}
|
||||
return keyPath, nil
|
||||
}
|
||||
|
||||
// writeSystemCredentials writes the system access key to a plaintext JSON file.
|
||||
// The daemon reads this at startup to inject credentials into ALB VM cloud-init
|
||||
// for SigV4-authenticated communication with the AWS gateway.
|
||||
|
||||
@@ -252,6 +252,74 @@ func TestWritePredastoreEncryptionKey(t *testing.T) {
|
||||
assert.NotEqual(t, key1, key2, "per-node keys must differ")
|
||||
}
|
||||
|
||||
// The viperblock key gates at-rest encryption for every volume on a fresh
|
||||
// install. masterkey.LoadShared rejects anything that isn't exactly 32 bytes at
|
||||
// 0640-or-stricter, so a regression in size/mode would silently fall back to
|
||||
// cleartext (empty path) or fail service startup.
|
||||
func TestWriteViperblockEncryptionKey(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
|
||||
keyPath, err := writeViperblockEncryptionKey(configDir)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(configDir, "viperblock", "encryption.key"), keyPath)
|
||||
|
||||
info, err := os.Stat(keyPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(32), info.Size(), "viperblock master key must be exactly 32 bytes")
|
||||
assert.Equal(t, os.FileMode(0600), info.Mode().Perm(), "viperblock master key must be mode 0600")
|
||||
}
|
||||
|
||||
// Joiners persist the leader's shared key via saveViperblockEncryptionKey
|
||||
// against a configDir with no viperblock/ subdir yet. The bytes round-trip
|
||||
// verbatim — the whole cluster shares one key, so any mutation would orphan
|
||||
// volumes sealed on other nodes.
|
||||
func TestSaveViperblockEncryptionKey_RoundTrip(t *testing.T) {
|
||||
configDir := t.TempDir()
|
||||
key := make([]byte, 32)
|
||||
for i := range key {
|
||||
key[i] = byte(i)
|
||||
}
|
||||
|
||||
keyPath, err := saveViperblockEncryptionKey(configDir, key)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, filepath.Join(configDir, "viperblock", "encryption.key"), keyPath)
|
||||
|
||||
got, err := os.ReadFile(keyPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, key, got, "shared key must be written verbatim")
|
||||
}
|
||||
|
||||
// A fresh install must render encryption_key_file so viperblockd enables
|
||||
// at-rest encryption; an empty field (upgrade / legacy) must omit the line
|
||||
// entirely so the volume stays in cleartext legacy mode.
|
||||
func TestSpinifexTomlTemplate_EncryptionKeyFile(t *testing.T) {
|
||||
base := admin.ConfigSettings{
|
||||
Node: "node1", Az: "ap-southeast-2a", Port: "4432", Region: "ap-southeast-2",
|
||||
BindIP: "0.0.0.0", AccessKey: "AKIATEST", SecretKey: "SECRET",
|
||||
AccountID: "123456789012", NatsToken: "token",
|
||||
OVNNBAddr: "tcp:127.0.0.1:6641", OVNSBAddr: "tcp:127.0.0.1:6642",
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
withKey := base
|
||||
withKey.ConfigDir = dir
|
||||
withKey.EncryptionKeyFile = "/etc/spinifex/viperblock/encryption.key"
|
||||
path := filepath.Join(dir, "spinifex.toml")
|
||||
require.NoError(t, admin.GenerateConfigFile(path, spinifexTomlTemplate, withKey))
|
||||
data, err := os.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(data), `encryption_key_file = "/etc/spinifex/viperblock/encryption.key"`)
|
||||
|
||||
dir2 := t.TempDir()
|
||||
noKey := base
|
||||
noKey.ConfigDir = dir2
|
||||
path2 := filepath.Join(dir2, "spinifex.toml")
|
||||
require.NoError(t, admin.GenerateConfigFile(path2, spinifexTomlTemplate, noKey))
|
||||
data2, err := os.ReadFile(path2)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(data2), "encryption_key_file", "blank key must omit the field")
|
||||
}
|
||||
|
||||
// Joiners run `spx admin join` against a configDir that doesn't yet contain
|
||||
// a predastore/ subdir. The helper must create it (predastore subdir is
|
||||
// owned by spinifex-storage in prod, but the helper just needs the dir to
|
||||
|
||||
@@ -93,6 +93,9 @@ token = "{{.NatsToken}}"
|
||||
|
||||
[nodes.{{.Node}}.viperblock]
|
||||
shardwal = false
|
||||
{{- if .EncryptionKeyFile}}
|
||||
encryption_key_file = "{{.EncryptionKeyFile}}"
|
||||
{{- end}}
|
||||
|
||||
[nodes.{{.Node}}.vpcd]
|
||||
ovn_nb_addr = "{{.OVNNBAddr}}"
|
||||
|
||||
@@ -416,6 +416,14 @@ TMPEOF
|
||||
$SUDO chown "spinifex-gw:$SPINIFEX_GROUP" /etc/spinifex/awsgw
|
||||
$SUDO chmod 0750 /etc/spinifex/awsgw
|
||||
|
||||
# Viperblock's at-rest encryption key dir. 0750 group-traversable; the key
|
||||
# itself is set to root:spinifex 0640 by SetServiceOwnership because both
|
||||
# viperblockd (spinifex-viperblock) and the awsgw handlers (spinifex-gw)
|
||||
# load it.
|
||||
$SUDO mkdir -p /etc/spinifex/viperblock
|
||||
$SUDO chown "spinifex-viperblock:$SPINIFEX_GROUP" /etc/spinifex/viperblock
|
||||
$SUDO chmod 0750 /etc/spinifex/viperblock
|
||||
|
||||
# Per-service data directories
|
||||
$SUDO mkdir -p /var/lib/spinifex/nats
|
||||
$SUDO chown "spinifex-nats:$SPINIFEX_GROUP" /var/lib/spinifex/nats
|
||||
|
||||
+13
-5
@@ -104,6 +104,12 @@ type ConfigSettings struct {
|
||||
// IPSecEnabled toggles cluster-wide OVN native IPsec on intra-AZ Geneve.
|
||||
// Written under [network] in spinifex.toml; daemon reads it via cluster config.
|
||||
IPSecEnabled bool
|
||||
|
||||
// EncryptionKeyFile is the path to the cluster-wide viperblock at-rest
|
||||
// encryption key, rendered into [nodes.X.viperblock].encryption_key_file.
|
||||
// Empty means no key was provisioned and volumes are written cleartext
|
||||
// (legacy mode); the template omits the field entirely in that case.
|
||||
EncryptionKeyFile string
|
||||
}
|
||||
|
||||
// PredastoreNodeConfig describes a single Predastore node for multi-node config generation.
|
||||
@@ -308,6 +314,7 @@ func SetServiceOwnership() {
|
||||
"/var/lib/spinifex/nats": "spinifex-nats",
|
||||
"/etc/spinifex/predastore": "spinifex-storage",
|
||||
"/var/lib/spinifex/predastore": "spinifex-storage",
|
||||
"/etc/spinifex/viperblock": "spinifex-viperblock",
|
||||
"/var/lib/spinifex/spinifex": "spinifex-daemon",
|
||||
"/var/lib/spinifex/viperblock": "spinifex-viperblock",
|
||||
"/var/lib/spinifex/vpcd": "spinifex-vpcd",
|
||||
@@ -342,11 +349,12 @@ func SetServiceOwnership() {
|
||||
// bootstrap.json lives in the awsgw data dir (not /etc/spinifex),
|
||||
// so /etc/spinifex stays at 0750 (no group-write needed).
|
||||
for path, mode := range map[string]os.FileMode{
|
||||
"/etc/spinifex/spinifex.toml": 0640,
|
||||
"/etc/spinifex/master.key": 0640,
|
||||
"/etc/spinifex/server.pem": 0644,
|
||||
"/etc/spinifex/server.key": 0640,
|
||||
"/etc/spinifex/ca.pem": 0644,
|
||||
"/etc/spinifex/spinifex.toml": 0640,
|
||||
"/etc/spinifex/master.key": 0640,
|
||||
"/etc/spinifex/viperblock/encryption.key": 0640,
|
||||
"/etc/spinifex/server.pem": 0644,
|
||||
"/etc/spinifex/server.key": 0640,
|
||||
"/etc/spinifex/ca.pem": 0644,
|
||||
} {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
continue
|
||||
|
||||
@@ -318,7 +318,7 @@ func readAMIConfig(store objectstore.ObjectStore, bucket, imageID string) (viper
|
||||
}
|
||||
|
||||
var state viperblock.VBState
|
||||
if err := json.Unmarshal(body, &state); err != nil {
|
||||
if err := json.Unmarshal(viperblock.StateBody(body), &state); err != nil {
|
||||
return viperblock.AMIMetadata{}, fmt.Errorf("%w: %s: %v", handlers_ec2_image.ErrCorruptAMIConfig, key, err)
|
||||
}
|
||||
return state.VolumeConfig.AMIMetadata, nil
|
||||
@@ -350,7 +350,7 @@ func readVolumeConfig(store objectstore.ObjectStore, bucket, volumeID string) (*
|
||||
}
|
||||
|
||||
var w volumeConfigWrapper
|
||||
if err := json.Unmarshal(body, &w); err != nil {
|
||||
if err := json.Unmarshal(viperblock.StateBody(body), &w); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s: %v", errCorruptVolumeConfig, key, err)
|
||||
}
|
||||
return &w.VolumeConfig, nil
|
||||
|
||||
@@ -55,6 +55,7 @@ type StatusResponse struct {
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
CAKey string `json:"ca_key,omitempty"`
|
||||
MasterKey string `json:"master_key,omitempty"`
|
||||
ViperblockKey string `json:"viperblock_key,omitempty"`
|
||||
NetworkConfig *NetworkConfig `json:"network_config,omitempty"`
|
||||
}
|
||||
|
||||
@@ -110,6 +111,7 @@ type FormationServer struct {
|
||||
caCert string
|
||||
caKey string
|
||||
masterKey string
|
||||
viperblockKey string
|
||||
networkConfig *NetworkConfig
|
||||
joinToken string
|
||||
tokenExpiry time.Time
|
||||
@@ -218,6 +220,16 @@ func (fs *FormationServer) SetMasterKey(key string) {
|
||||
fs.masterKey = key
|
||||
}
|
||||
|
||||
// SetViperblockKey sets the base64-encoded cluster-wide viperblock at-rest
|
||||
// encryption key for distribution to joining nodes. Unlike the per-node
|
||||
// predastore key, this key is shared so a volume sealed on one node can be
|
||||
// opened on any other.
|
||||
func (fs *FormationServer) SetViperblockKey(key string) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
fs.viperblockKey = key
|
||||
}
|
||||
|
||||
// Start launches the HTTPS server on the given address (e.g. "10.0.0.1:4432")
|
||||
// using the cluster CA certificate and key for TLS.
|
||||
func (fs *FormationServer) Start(bindAddr string) error {
|
||||
@@ -360,6 +372,7 @@ func (fs *FormationServer) handleStatus(w http.ResponseWriter, r *http.Request)
|
||||
resp.CACert = fs.caCert
|
||||
resp.CAKey = fs.caKey
|
||||
resp.MasterKey = fs.masterKey
|
||||
resp.ViperblockKey = fs.viperblockKey
|
||||
resp.NetworkConfig = fs.networkConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -346,6 +346,31 @@ func TestStatusEndpointMasterKey(t *testing.T) {
|
||||
assert.Equal(t, "dGVzdC1tYXN0ZXIta2V5LWJhc2U2NC1lbmNvZGVk", sr.MasterKey)
|
||||
}
|
||||
|
||||
func TestStatusEndpointViperblockKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := NewFormationServer(2, testCreds(), "ca-cert", "ca-key", nil, testToken, testTokenTTL)
|
||||
fs.SetViperblockKey("dmlwZXJibG9jay1zaGFyZWQta2V5LWJhc2U2NA==")
|
||||
ts := testServer(t, fs)
|
||||
defer ts.Close()
|
||||
|
||||
// Before completion: shared key must not leak
|
||||
resp := authGet(t, ts.URL+"/formation/status", testToken)
|
||||
var sr StatusResponse
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&sr))
|
||||
resp.Body.Close()
|
||||
assert.Empty(t, sr.ViperblockKey)
|
||||
|
||||
require.NoError(t, fs.RegisterNode(testNode("node1", "10.0.0.1")))
|
||||
require.NoError(t, fs.RegisterNode(testNode("node2", "10.0.0.2")))
|
||||
|
||||
// After completion: joiners receive the cluster-wide key
|
||||
resp = authGet(t, ts.URL+"/formation/status", testToken)
|
||||
defer resp.Body.Close()
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&sr))
|
||||
assert.True(t, sr.Complete)
|
||||
assert.Equal(t, "dmlwZXJibG9jay1zaGFyZWQta2V5LWJhc2U2NA==", sr.ViperblockKey)
|
||||
}
|
||||
|
||||
func TestHealthEndpoint_NoAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
fs := NewFormationServer(1, testCreds(), "", "", nil, testToken, testTokenTTL)
|
||||
|
||||
@@ -170,11 +170,13 @@ func (s *ImageServiceImpl) DescribeImages(input *ec2.DescribeImagesInput, accoun
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the viperblock config which contains VolumeConfig with AMIMetadata
|
||||
// Parse the viperblock config which contains VolumeConfig with AMIMetadata.
|
||||
// StateBody unwraps the at-rest encryption envelope (the metadata ships
|
||||
// authenticated-but-plaintext); it is a no-op for unencrypted volumes.
|
||||
var vbConfig struct {
|
||||
VolumeConfig viperblock.VolumeConfig `json:"VolumeConfig"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &vbConfig); err != nil {
|
||||
if err := json.Unmarshal(viperblock.StateBody(body), &vbConfig); err != nil {
|
||||
slog.Debug("Failed to unmarshal config", "key", configKey, "err", err)
|
||||
continue
|
||||
}
|
||||
@@ -580,8 +582,12 @@ func (s *ImageServiceImpl) getVolumeConfig(volumeID string) (*viperblock.VolumeC
|
||||
}
|
||||
defer result.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var vbState viperblock.VBState
|
||||
if err := json.NewDecoder(result.Body).Decode(&vbState); err != nil {
|
||||
if err := json.Unmarshal(viperblock.StateBody(body), &vbState); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &vbState.VolumeConfig, nil
|
||||
@@ -616,10 +622,13 @@ func (s *ImageServiceImpl) amiNameExists(name string) (bool, error) {
|
||||
return false, fmt.Errorf("amiNameExists: failed to read %s: %w", configKey, err)
|
||||
}
|
||||
|
||||
var vbState viperblock.VBState
|
||||
decodeErr := json.NewDecoder(result.Body).Decode(&vbState)
|
||||
body, readErr := io.ReadAll(result.Body)
|
||||
_ = result.Body.Close()
|
||||
if decodeErr != nil {
|
||||
if readErr != nil {
|
||||
return false, fmt.Errorf("amiNameExists: failed to read %s: %w", configKey, readErr)
|
||||
}
|
||||
var vbState viperblock.VBState
|
||||
if decodeErr := json.Unmarshal(viperblock.StateBody(body), &vbState); decodeErr != nil {
|
||||
return false, fmt.Errorf("amiNameExists: failed to decode %s: %w", configKey, decodeErr)
|
||||
}
|
||||
|
||||
@@ -649,8 +658,12 @@ func (s *ImageServiceImpl) GetAMIConfig(imageID string) (viperblock.AMIMetadata,
|
||||
}
|
||||
defer result.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return viperblock.AMIMetadata{}, fmt.Errorf("%w: %s: %v", ErrCorruptAMIConfig, configKey, err)
|
||||
}
|
||||
var vbState viperblock.VBState
|
||||
if err := json.NewDecoder(result.Body).Decode(&vbState); err != nil {
|
||||
if err := json.Unmarshal(viperblock.StateBody(body), &vbState); err != nil {
|
||||
return viperblock.AMIMetadata{}, fmt.Errorf("%w: %s: %v", ErrCorruptAMIConfig, configKey, err)
|
||||
}
|
||||
return vbState.VolumeConfig.AMIMetadata, nil
|
||||
|
||||
@@ -1000,6 +1000,9 @@ func (s *VolumeServiceImpl) getVolumeConfigAndEncryption(volumeID string) (*vipe
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to read config body: %w", err)
|
||||
}
|
||||
// Unwrap the at-rest encryption envelope (authenticated-but-plaintext
|
||||
// metadata); no-op for unencrypted volumes.
|
||||
body = viperblock.StateBody(body)
|
||||
|
||||
// Try full VBState first (matches mergeVolumeConfig's careful decode
|
||||
// pattern). A populated BlockSize is the marker that the blob is a full
|
||||
@@ -1062,10 +1065,11 @@ func (s *VolumeServiceImpl) mergeVolumeConfig(configKey string, cfg *viperblock.
|
||||
return nil, fmt.Errorf("failed to read existing config: %w", err)
|
||||
}
|
||||
|
||||
// Use Decoder (not Unmarshal): a trailing AES-GCM tag must not cause a parse error
|
||||
// that would silently route to the wrapper-only path and brick the volume.
|
||||
// StateBody unwraps the at-rest encryption envelope so the guard below sees
|
||||
// the real VBState. Without it the wrapper decodes to a zero-valued state
|
||||
// (BlockSize==0), routing to the wrapper-only path and bricking the volume.
|
||||
var state viperblock.VBState
|
||||
if decodeErr := json.NewDecoder(bytes.NewReader(body)).Decode(&state); decodeErr != nil || state.BlockSize == 0 {
|
||||
if decodeErr := json.Unmarshal(viperblock.StateBody(body), &state); decodeErr != nil || state.BlockSize == 0 {
|
||||
// Not a full VBState (new volume or wrapper-only) -- write wrapper
|
||||
return json.Marshal(volumeConfigWrapper{VolumeConfig: *cfg})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user