Provisioning: Support rotating secrets (#100705)

This commit is contained in:
Mariell Hoversholm authored and GitHub committed 2025-02-14 13:40:43 +01:00
1 parent bdab859198
commit 09ee1233cf
3 files changed
+189 -2

No files matched your search

+1 -1
View File
@@ -111,7 +111,7 @@ require (
github.com/influxdata/influxql v1.4.0 // @grafana/partner-datasources
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // @grafana/grafana-app-platform-squad
github.com/jmespath-community/go-jmespath v1.1.1 // @grafana/identity-access-team
github.com/jmespath/go-jmespath v0.4.0 // // @grafana/grafana-backend-group
github.com/jmespath/go-jmespath v0.4.0 // indirect; // @grafana/grafana-backend-group
github.com/jmoiron/sqlx v1.3.5 // @grafana/grafana-backend-group
github.com/json-iterator/go v1.1.12 // @grafana/grafana-backend-group
github.com/lib/pq v1.10.9 // @grafana/grafana-backend-group
+1 -1
View File
@@ -51,7 +51,7 @@ func ProvideSecretsMigrator(
b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "refresh_token"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "session_id"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "name_id"}, encoding: base64.StdEncoding},
// FIXME: Rotate provisioning secrets
provisioningSecrets{},
}
return &SecretsMigrator{
@@ -0,0 +1,187 @@
package migrator
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/encryption"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/manager"
)
var errNoEncryptedValue = errors.New("no encrypted value was found")
var _ SecretsRotator = provisioningSecrets{}
type provisioningSecrets struct{}
func (p provisioningSecrets) ReEncrypt(ctx context.Context, svc *manager.SecretsService, sqlStore db.DB) bool {
return p.reEncrypt(ctx, svc, func(ctx context.Context, payload []byte) ([]byte, error) {
return svc.Encrypt(ctx, payload, secrets.WithoutScope())
}, sqlStore, "rotation")
}
func (p provisioningSecrets) Rollback(ctx context.Context, svc *manager.SecretsService, internal encryption.Internal, sqlStore db.DB, secretKey string) bool {
return p.reEncrypt(ctx, svc, func(ctx context.Context, payload []byte) ([]byte, error) {
return internal.Encrypt(ctx, payload, secretKey)
}, sqlStore, "rollback")
}
func (p provisioningSecrets) reEncrypt(
ctx context.Context,
svc *manager.SecretsService,
encrypt func(context.Context, []byte) ([]byte, error),
sqlStore db.DB,
action string,
) bool {
var rows []struct {
Guid string
Value []byte
}
if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
return sess.Table("resource").Where("`group` = 'provisioning.grafana.app' AND `resource` = 'repositories'").
Select("guid, value").
Find(&rows)
}); err != nil {
logger.Warn("Could not find any provisioning secrets to re-encrypt", "error", err, "action", action)
return false
}
var failures int
for _, row := range rows {
var resource map[string]any
if err := json.Unmarshal(row.Value, &resource); err != nil {
logger.Error("Failed to decode resource", "guid", row.Guid, "error", err, "action", action)
failures++
continue
}
err := p.reEncryptGitHubToken(ctx, svc, encrypt, sqlStore, resource)
if err != nil && !errors.Is(err, errNoEncryptedValue) {
logger.Error("Failed to rotate GitHub token", "guid", row.Guid, "error", err, "action", action)
failures++
}
update := err == nil
err = p.reEncryptWebhookSecret(ctx, svc, encrypt, sqlStore, resource)
if err != nil && !errors.Is(err, errNoEncryptedValue) {
logger.Error("Failed to rotate webhook secret", "guid", row.Guid, "error", err, "action", action)
failures++
}
update = update || err == nil
if update {
// Do it...
encoded, err := json.Marshal(resource)
if err != nil {
logger.Error("Failed to marshal resource to JSON", "guid", row.Guid, "error", err, "action", action)
failures++
continue
}
if err := sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
// TODO: Figure out what resource_version is supposed to contain, and update it.
_, err = sess.Exec("UPDATE resource SET value = ? WHERE guid = ?", string(encoded), row.Guid)
return err
}); err != nil {
logger.Error("Failed to update resource with re-encrypted values", "guid", row.Guid, "error", err, "action", action)
failures++
}
}
}
if failures > 0 {
logger.Warn("Failed to rotate provisioning secrets", "failures", failures, "action", action)
} else {
logger.Info("Successfully rotated provisioning secrets", "action", action)
}
return failures > 0
}
func (provisioningSecrets) reEncryptGitHubToken(
ctx context.Context,
svc *manager.SecretsService,
encrypt func(context.Context, []byte) ([]byte, error),
sqlStore db.DB,
obj map[string]any,
) error {
spec, _ := getCast[map[string]any](obj, "spec")
github, _ := getCast[map[string]any](spec, "github")
b64Token, ok := getCast[string](github, "encryptedToken")
if !ok {
return errNoEncryptedValue
}
token, err := base64.StdEncoding.DecodeString(b64Token)
if err != nil {
return fmt.Errorf("failed to decode base64 encrypted token: %w", err)
}
decrypted, err := svc.Decrypt(ctx, token)
if err != nil {
return fmt.Errorf("failed to decrypt existing encrypted token: %w", err)
}
encrypted, err := encrypt(ctx, decrypted)
if err != nil {
return fmt.Errorf("failed to encrypt decrypted token: %w", err)
}
b64Token = base64.StdEncoding.EncodeToString(encrypted)
github["encryptedToken"] = b64Token
return nil
}
func (provisioningSecrets) reEncryptWebhookSecret(
ctx context.Context,
svc *manager.SecretsService,
encrypt func(context.Context, []byte) ([]byte, error),
sqlStore db.DB,
obj map[string]any,
) error {
status, _ := getCast[map[string]any](obj, "status")
webhook, _ := getCast[map[string]any](status, "webhook")
b64Token, ok := getCast[string](webhook, "encryptedSecret")
if !ok {
return errNoEncryptedValue
}
token, err := base64.StdEncoding.DecodeString(b64Token)
if err != nil {
return fmt.Errorf("failed to decode base64 encrypted secret: %w", err)
}
decrypted, err := svc.Decrypt(ctx, token)
if err != nil {
return fmt.Errorf("failed to decrypt existing encrypted secret: %w", err)
}
encrypted, err := encrypt(ctx, decrypted)
if err != nil {
return fmt.Errorf("failed to encrypt decrypted secret: %w", err)
}
b64Token = base64.StdEncoding.EncodeToString(encrypted)
webhook["encryptedSecret"] = b64Token
return nil
}
func getCast[T any](m map[string]any, name string) (T, bool) {
if m == nil {
var zero T
return zero, false
}
anyV, ok := m[name]
if ok {
t, ok := anyV.(T)
return t, ok
}
var zero T
return zero, false
}