Provisioning: Support rotating secrets (#100705)
This commit is contained in:
1 parent
bdab859198
commit
09ee1233cf
3 files changed
+189
-2
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user