Secrets: Refactor data_key_id out of the encoded secure value payload (#111852)

* everything compiles

* tests pass

* remove file included by accident

* add entry to gitignore

* some scaffolding for the migration executor

* remove file

* implement and test the migration

* use xkube.Namespace in our interfaces

* add todo

* update wire deps

* add some logs

* fix wire dependency ordering

* create tests to validate error conditions during migrations
This commit is contained in:
Michael Mandrus
2025-10-03 15:25:46 -04:00
committed by GitHub
parent 0c0c66fda1
commit acad92864e
49 changed files with 782 additions and 232 deletions
@@ -1,7 +1,9 @@
package encryption
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"time"
@@ -10,6 +12,7 @@ import (
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
@@ -37,9 +40,9 @@ type encryptedValStorage struct {
tracer trace.Tracer
}
func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string, version int64, encryptedData []byte) (ev *contracts.EncryptedValue, err error) {
func (s *encryptedValStorage) Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) (ev *contracts.EncryptedValue, err error) {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("namespace", namespace.String()),
))
defer span.End()
@@ -56,10 +59,11 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string
createdTime := time.Now().Unix()
encryptedValue := &EncryptedValue{
Namespace: namespace,
Namespace: namespace.String(),
Name: name,
Version: version,
EncryptedData: encryptedData,
EncryptedData: encryptedData.EncryptedData,
DataKeyID: encryptedData.DataKeyID,
Created: createdTime,
Updated: createdTime,
}
@@ -88,18 +92,21 @@ func (s *encryptedValStorage) Create(ctx context.Context, namespace, name string
}
return &contracts.EncryptedValue{
Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedData: encryptedValue.EncryptedData,
Created: encryptedValue.Created,
Updated: encryptedValue.Updated,
Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedPayload: contracts.EncryptedPayload{
DataKeyID: encryptedValue.DataKeyID,
EncryptedData: encryptedValue.EncryptedData,
},
Created: encryptedValue.Created,
Updated: encryptedValue.Updated,
}, nil
}
func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string, version int64, encryptedData []byte) error {
func (s *encryptedValStorage) Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) error {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("namespace", namespace.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
@@ -107,10 +114,11 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string
req := updateEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace,
Namespace: namespace.String(),
Name: name,
Version: version,
EncryptedData: encryptedData,
EncryptedData: encryptedData.EncryptedData,
DataKeyID: encryptedData.DataKeyID,
Updated: time.Now().Unix(),
}
@@ -133,9 +141,9 @@ func (s *encryptedValStorage) Update(ctx context.Context, namespace, name string
return nil
}
func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, version int64) (*contracts.EncryptedValue, error) {
func (s *encryptedValStorage) Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*contracts.EncryptedValue, error) {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("namespace", namespace.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
@@ -143,7 +151,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v
req := &readEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace,
Namespace: namespace.String(),
Name: name,
Version: version,
}
@@ -163,7 +171,7 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v
}
var encryptedValue EncryptedValue
err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.Created, &encryptedValue.Updated)
err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.DataKeyID, &encryptedValue.Created, &encryptedValue.Updated)
if err != nil {
return nil, fmt.Errorf("failed to scan encrypted value row: %w", err)
}
@@ -172,18 +180,21 @@ func (s *encryptedValStorage) Get(ctx context.Context, namespace, name string, v
}
return &contracts.EncryptedValue{
Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedData: encryptedValue.EncryptedData,
Created: encryptedValue.Created,
Updated: encryptedValue.Updated,
Namespace: encryptedValue.Namespace,
Name: encryptedValue.Name,
Version: encryptedValue.Version,
EncryptedPayload: contracts.EncryptedPayload{
DataKeyID: encryptedValue.DataKeyID,
EncryptedData: encryptedValue.EncryptedData,
},
Created: encryptedValue.Created,
Updated: encryptedValue.Updated,
}, nil
}
func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string, version int64) error {
func (s *encryptedValStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error {
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("namespace", namespace.String()),
attribute.String("name", name),
attribute.Int64("version", version),
))
@@ -191,7 +202,7 @@ func (s *encryptedValStorage) Delete(ctx context.Context, namespace, name string
req := deleteEncryptedValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace,
Namespace: namespace.String(),
Name: name,
Version: version,
}
@@ -264,6 +275,7 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts.
&row.Name,
&row.Version,
&row.EncryptedData,
&row.DataKeyID,
&row.Created,
&row.Updated,
)
@@ -272,12 +284,15 @@ func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts.
}
encryptedValues = append(encryptedValues, &contracts.EncryptedValue{
Namespace: row.Namespace,
Name: row.Name,
Version: row.Version,
EncryptedData: row.EncryptedData,
Created: row.Created,
Updated: row.Updated,
Namespace: row.Namespace,
Name: row.Name,
Version: row.Version,
EncryptedPayload: contracts.EncryptedPayload{
DataKeyID: row.DataKeyID,
EncryptedData: row.EncryptedData,
},
Created: row.Created,
Updated: row.Updated,
})
}
if err := rows.Err(); err != nil {
@@ -329,3 +344,77 @@ func (s *globalEncryptedValStorage) CountAll(ctx context.Context, untilTime *int
return count, nil
}
type encryptedValMigrationExecutor struct {
db contracts.Database
dialect sqltemplate.Dialect
tracer trace.Tracer
encryptedValueStore contracts.EncryptedValueStorage
globalStore contracts.GlobalEncryptedValueStorage
}
func ProvideEncryptedValueMigrationExecutor(
db contracts.Database,
tracer trace.Tracer,
encryptedValueStore contracts.EncryptedValueStorage,
globalStore contracts.GlobalEncryptedValueStorage,
) (contracts.EncryptedValueMigrationExecutor, error) {
return &encryptedValMigrationExecutor{
db: db,
dialect: sqltemplate.DialectForDriver(db.DriverName()),
tracer: tracer,
encryptedValueStore: encryptedValueStore,
globalStore: globalStore,
}, nil
}
func (s *encryptedValMigrationExecutor) Execute(ctx context.Context) (int, error) {
ctx, span := s.tracer.Start(ctx, "EncryptedValueMigrationExecutor.Execute")
defer span.End()
// 1. Retrieve all encrypted values
encryptedValues, err := s.globalStore.ListAll(ctx, contracts.ListOpts{}, nil)
if err != nil {
return 0, fmt.Errorf("listing all encrypted values: %w", err)
}
// This doesn't need to be done in a single transaction because there's no risk to successful rows if other rows fail
rowsAffected := 0
for _, encryptedValue := range encryptedValues {
// 2. If the value already has the data key id broken out, skip it
if encryptedValue.DataKeyID != "" {
continue
}
// 3. Split the data key id and the encrypted data out from the encoded payload
payload := encryptedValue.EncryptedData
const keyIdDelimiter = '#'
payload = payload[1:]
endOfKey := bytes.Index(payload, []byte{keyIdDelimiter})
if endOfKey == -1 {
return rowsAffected, fmt.Errorf("could not find valid key id in encrypted payload with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version)
}
b64Key := payload[:endOfKey]
encryptedData := payload[endOfKey+1:]
if len(encryptedData) == 0 {
return rowsAffected, fmt.Errorf("encrypted data is empty with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version)
}
keyId := make([]byte, base64.RawStdEncoding.DecodedLen(len(b64Key)))
_, err := base64.RawStdEncoding.Decode(keyId, b64Key)
if err != nil {
return rowsAffected, fmt.Errorf("decoding key id with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err)
}
// 4. Update the encrypted value with the data key id and the encrypted data
err = s.encryptedValueStore.Update(ctx, xkube.Namespace(encryptedValue.Namespace), encryptedValue.Name, encryptedValue.Version, contracts.EncryptedPayload{
DataKeyID: string(keyId),
EncryptedData: encryptedData,
})
if err != nil {
return rowsAffected, fmt.Errorf("updating encrypted value with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err)
}
rowsAffected++
}
return rowsAffected, nil
}