mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
SupportBundles: Add bundle encryption based on age (#62501)
* add bundle encryption based on age * undo changes to grafana-data * sort deps * test bundle creation and encryption * use whitespace separator * add support bundle config documentation * Update docs/sources/troubleshooting/support-bundles/index.md * Apply suggestions from code review Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> * touch up docs * extract encrypt * Update docs/sources/troubleshooting/support-bundles/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * Update docs/sources/troubleshooting/support-bundles/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> --------- Co-authored-by: Ieva <ieva.vasiljeva@grafana.com> Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
This commit is contained in:
parent
966bcd3545
commit
af987ae636
@ -1377,6 +1377,14 @@ max_crawl_duration =
|
|||||||
# This setting should be expressed as a duration. Examples: 10s (seconds), 1m (minutes).
|
# This setting should be expressed as a duration. Examples: 10s (seconds), 1m (minutes).
|
||||||
scheduler_interval =
|
scheduler_interval =
|
||||||
|
|
||||||
|
#################################### Support Bundles #####################################
|
||||||
|
[support_bundles]
|
||||||
|
# Enable support bundle creation (default: true)
|
||||||
|
enabled = true
|
||||||
|
# Only server admins can generate and view support bundles (default: true)
|
||||||
|
server_admin_only = true
|
||||||
|
# If set, bundles will be encrypted with the provided public keys separated by whitespace
|
||||||
|
public_keys = ""
|
||||||
|
|
||||||
#################################### Storage ################################################
|
#################################### Storage ################################################
|
||||||
|
|
||||||
|
@ -1263,6 +1263,14 @@
|
|||||||
;grpc_host =
|
;grpc_host =
|
||||||
;grpc_port =
|
;grpc_port =
|
||||||
|
|
||||||
|
[support_bundles]
|
||||||
|
# Enable support bundle creation (default: true)
|
||||||
|
#enabled = true
|
||||||
|
# Only server admins can generate and view support bundles (default: true)
|
||||||
|
#server_admin_only = true
|
||||||
|
# If set, bundles will be encrypted with the provided public keys separated by whitespace
|
||||||
|
#public_keys = ""
|
||||||
|
|
||||||
[enterprise]
|
[enterprise]
|
||||||
# Path to a valid Grafana Enterprise license.jwt file
|
# Path to a valid Grafana Enterprise license.jwt file
|
||||||
;license_path =
|
;license_path =
|
||||||
|
@ -53,3 +53,71 @@ To generate a support bundle and send the support bundle to Grafana Labs via a s
|
|||||||
Grafana downloads the support bundle to an archive (tar.gz) file.
|
Grafana downloads the support bundle to an archive (tar.gz) file.
|
||||||
|
|
||||||
1. Attach the archive (tar.gz) file to a support ticket that you send to Grafana Labs Technical Support.
|
1. Attach the archive (tar.gz) file to a support ticket that you send to Grafana Labs Technical Support.
|
||||||
|
|
||||||
|
## Support bundle configuration
|
||||||
|
|
||||||
|
You can configure the following settings for support bundles:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# Enable support bundle creation (default: true)
|
||||||
|
enabled = true
|
||||||
|
# Only server admins can generate and view support bundles. When set to false, organization admins can generate and view support bundles (default: true)
|
||||||
|
server_admin_only = true
|
||||||
|
# If set, bundles will be encrypted with the provided public keys separated by whitespace
|
||||||
|
public_keys = ""
|
||||||
|
```
|
||||||
|
|
||||||
|
## Encrypting a support bundle
|
||||||
|
|
||||||
|
Support bundles can be encrypted with [age](age-encryption.org) before they are sent to
|
||||||
|
recipients. This is useful when you want to send a support bundle to Grafana through a
|
||||||
|
channel that is not private.
|
||||||
|
|
||||||
|
### Generate a key pair
|
||||||
|
|
||||||
|
Ensure [age](https://github.com/FiloSottile/age#installation) is installed on your system.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ age-keygen -o key.txt
|
||||||
|
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||||
|
```
|
||||||
|
|
||||||
|
### Support bundle encryption
|
||||||
|
|
||||||
|
Ensure [age](https://github.com/FiloSottile/age#installation) is installed on your system.
|
||||||
|
|
||||||
|
Add the public key to the `public_keys` setting in the `support_bundle` section of the Grafana configuration file.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[support_bundle]
|
||||||
|
public_keys = "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
|
||||||
|
```
|
||||||
|
|
||||||
|
> Multiple public keys can be defined by separating them with whitespace.
|
||||||
|
> All included public keys will be able to decrypt the support bundle.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[support_bundle]
|
||||||
|
public_keys = "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p age1yu8vzu554pv3klw46yhdv4raz36k5w3vy30lpxn46923lqngudyqvxacer"
|
||||||
|
```
|
||||||
|
|
||||||
|
When you restart Grafana, new support bundles will be encrypted with the provided
|
||||||
|
public keys. The support bundle file extension is `tar.gz.age`.
|
||||||
|
|
||||||
|
#### Decrypt a support bundle
|
||||||
|
|
||||||
|
Ensure [age](https://github.com/FiloSottile/age#installation) is installed on your system.
|
||||||
|
|
||||||
|
Execute the following command to decrypt the support bundle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
age --decrypt -i keyfile -o output.tar.gz downloaded.tar.gz.age
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
age --decrypt -i key.txt -o data.tar.gz af6684b4-d613-4b31-9fc3-7cb579199bea.tar.gz.age
|
||||||
|
```
|
||||||
|
3
go.mod
3
go.mod
@ -131,7 +131,7 @@ require (
|
|||||||
gopkg.in/square/go-jose.v2 v2.5.1
|
gopkg.in/square/go-jose.v2 v2.5.1
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
xorm.io/builder v0.3.6 // indirect
|
xorm.io/builder v0.3.6
|
||||||
xorm.io/core v0.7.3
|
xorm.io/core v0.7.3
|
||||||
xorm.io/xorm v0.8.2
|
xorm.io/xorm v0.8.2
|
||||||
)
|
)
|
||||||
@ -348,6 +348,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute v1.13.0 // indirect
|
cloud.google.com/go/compute v1.13.0 // indirect
|
||||||
cloud.google.com/go/iam v0.8.0 // indirect
|
cloud.google.com/go/iam v0.8.0 // indirect
|
||||||
|
filippo.io/age v1.1.1
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 // indirect
|
||||||
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect
|
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.0 // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -101,6 +101,8 @@ contrib.go.opencensus.io/exporter/prometheus v0.3.0/go.mod h1:rpCPVQKhiyH8oomWgm
|
|||||||
contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8=
|
contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8=
|
||||||
contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
|
contrib.go.opencensus.io/integrations/ocsql v0.1.7/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg=
|
||||||
|
filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE=
|
||||||
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
||||||
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs=
|
github.com/99designs/basicauth-go v0.0.0-20160802081356-2a93ba0f464d/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs=
|
||||||
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY=
|
github.com/99designs/httpsignatures-go v0.0.0-20170731043157-88528bf4ca7e/go.mod h1:Xa6lInWHNQnuWoF0YPSsx+INFA9qk7/7pTjwb3PInkY=
|
||||||
@ -1264,8 +1266,6 @@ github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58 h1:2
|
|||||||
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
|
github.com/grafana/grafana-google-sdk-go v0.0.0-20211104130251-b190293eaf58/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M=
|
github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.148.0 h1:M8v6L9agAFMlZMnak1yInII+aVF5FjZ1Qv4Q+GANyk4=
|
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.148.0/go.mod h1:NMgO3t2gR5wyLx8bWZ9CTmpDk5Txp4wYFccFLHdYn3Q=
|
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.149.1 h1:n4Mx8oUE+exa1DGdWSUmp2DuZUDhURQEzPG05HUGfnc=
|
github.com/grafana/grafana-plugin-sdk-go v0.149.1 h1:n4Mx8oUE+exa1DGdWSUmp2DuZUDhURQEzPG05HUGfnc=
|
||||||
github.com/grafana/grafana-plugin-sdk-go v0.149.1/go.mod h1:NMgO3t2gR5wyLx8bWZ9CTmpDk5Txp4wYFccFLHdYn3Q=
|
github.com/grafana/grafana-plugin-sdk-go v0.149.1/go.mod h1:NMgO3t2gR5wyLx8bWZ9CTmpDk5Txp4wYFccFLHdYn3Q=
|
||||||
github.com/grafana/phlare/api v0.1.2 h1:1jrwd3KnsXMzj/tJih9likx5EvbY3pbvLbDqAAYem30=
|
github.com/grafana/phlare/api v0.1.2 h1:1jrwd3KnsXMzj/tJih9likx5EvbY3pbvLbDqAAYem30=
|
||||||
|
77
pkg/infra/kvstore/test_utils.go
Normal file
77
pkg/infra/kvstore/test_utils.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package kvstore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// In memory kv store used for testing
|
||||||
|
type FakeKVStore struct {
|
||||||
|
store map[Key]string
|
||||||
|
delError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFakeKVStore() *FakeKVStore {
|
||||||
|
return &FakeKVStore{store: make(map[Key]string)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeKVStore) DeletionError(shouldErr bool) {
|
||||||
|
f.delError = shouldErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeKVStore) Get(ctx context.Context, orgId int64, namespace string, key string) (string, bool, error) {
|
||||||
|
value := f.store[buildKey(orgId, namespace, key)]
|
||||||
|
found := value != ""
|
||||||
|
return value, found, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeKVStore) Set(ctx context.Context, orgId int64, namespace string, key string, value string) error {
|
||||||
|
f.store[buildKey(orgId, namespace, key)] = value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeKVStore) Del(ctx context.Context, orgId int64, namespace string, key string) error {
|
||||||
|
if f.delError {
|
||||||
|
return errors.New("mocked del error")
|
||||||
|
}
|
||||||
|
delete(f.store, buildKey(orgId, namespace, key))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all keys with an optional filter. If default values are provided, filter is not applied.
|
||||||
|
func (f *FakeKVStore) Keys(ctx context.Context, orgId int64, namespace string, keyPrefix string) ([]Key, error) {
|
||||||
|
res := make([]Key, 0)
|
||||||
|
for k := range f.store {
|
||||||
|
if orgId == AllOrganizations && namespace == "" && keyPrefix == "" {
|
||||||
|
res = append(res, k)
|
||||||
|
} else if k.OrgId == orgId && k.Namespace == namespace && strings.HasPrefix(k.Key, keyPrefix) {
|
||||||
|
res = append(res, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeKVStore) GetAll(ctx context.Context, orgId int64, namespace string) (map[int64]map[string]string, error) {
|
||||||
|
items := make(map[int64]map[string]string)
|
||||||
|
for k := range f.store {
|
||||||
|
orgId := k.OrgId
|
||||||
|
namespace := k.Namespace
|
||||||
|
|
||||||
|
if _, ok := items[orgId]; !ok {
|
||||||
|
items[orgId] = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
items[orgId][namespace] = f.store[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildKey(orgId int64, namespace string, key string) Key {
|
||||||
|
return Key{
|
||||||
|
OrgId: orgId,
|
||||||
|
Namespace: namespace,
|
||||||
|
Key: key,
|
||||||
|
}
|
||||||
|
}
|
@ -99,6 +99,10 @@ func (s *Service) handleDownload(ctx *contextmodel.ReqContext) response.Response
|
|||||||
|
|
||||||
ctx.Resp.Header().Set("Content-Type", "application/tar+gzip")
|
ctx.Resp.Header().Set("Content-Type", "application/tar+gzip")
|
||||||
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.tar.gz", uid))
|
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.tar.gz", uid))
|
||||||
|
if len(s.encryptionPublicKeys) > 0 {
|
||||||
|
ctx.Resp.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.tar.gz.age", uid))
|
||||||
|
}
|
||||||
|
|
||||||
return response.CreateNormalResponse(ctx.Resp.Header(), bundle.TarBytes, http.StatusOK)
|
return response.CreateNormalResponse(ctx.Resp.Header(), bundle.TarBytes, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,45 +27,48 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
cfg *setting.Cfg
|
|
||||||
store bundleStore
|
|
||||||
pluginStore plugins.Store
|
|
||||||
pluginSettings pluginsettings.Service
|
|
||||||
accessControl ac.AccessControl
|
accessControl ac.AccessControl
|
||||||
features *featuremgmt.FeatureManager
|
|
||||||
bundleRegistry *bundleregistry.Service
|
bundleRegistry *bundleregistry.Service
|
||||||
|
cfg *setting.Cfg
|
||||||
|
features *featuremgmt.FeatureManager
|
||||||
|
pluginSettings pluginsettings.Service
|
||||||
|
pluginStore plugins.Store
|
||||||
|
store bundleStore
|
||||||
|
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
encryptionPublicKeys []string
|
||||||
|
|
||||||
enabled bool
|
enabled bool
|
||||||
serverAdminOnly bool
|
serverAdminOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg,
|
func ProvideService(
|
||||||
bundleRegistry *bundleregistry.Service,
|
|
||||||
sql db.DB,
|
|
||||||
kvStore kvstore.KVStore,
|
|
||||||
accessControl ac.AccessControl,
|
accessControl ac.AccessControl,
|
||||||
accesscontrolService ac.Service,
|
accesscontrolService ac.Service,
|
||||||
routeRegister routing.RouteRegister,
|
bundleRegistry *bundleregistry.Service,
|
||||||
settings setting.Provider,
|
cfg *setting.Cfg,
|
||||||
pluginStore plugins.Store,
|
|
||||||
pluginSettings pluginsettings.Service,
|
|
||||||
features *featuremgmt.FeatureManager,
|
features *featuremgmt.FeatureManager,
|
||||||
httpServer *grafanaApi.HTTPServer,
|
httpServer *grafanaApi.HTTPServer,
|
||||||
|
kvStore kvstore.KVStore,
|
||||||
|
pluginSettings pluginsettings.Service,
|
||||||
|
pluginStore plugins.Store,
|
||||||
|
routeRegister routing.RouteRegister,
|
||||||
|
settings setting.Provider,
|
||||||
|
sql db.DB,
|
||||||
usageStats usagestats.Service) (*Service, error) {
|
usageStats usagestats.Service) (*Service, error) {
|
||||||
section := cfg.SectionWithEnvOverrides("support_bundles")
|
section := cfg.SectionWithEnvOverrides("support_bundles")
|
||||||
s := &Service{
|
s := &Service{
|
||||||
cfg: cfg,
|
accessControl: accessControl,
|
||||||
store: newStore(kvStore),
|
bundleRegistry: bundleRegistry,
|
||||||
pluginStore: pluginStore,
|
cfg: cfg,
|
||||||
pluginSettings: pluginSettings,
|
enabled: section.Key("enabled").MustBool(true),
|
||||||
accessControl: accessControl,
|
encryptionPublicKeys: section.Key("public_keys").Strings(" "),
|
||||||
features: features,
|
features: features,
|
||||||
bundleRegistry: bundleRegistry,
|
log: log.New("supportbundle.service"),
|
||||||
log: log.New("supportbundle.service"),
|
pluginSettings: pluginSettings,
|
||||||
enabled: section.Key("enabled").MustBool(true),
|
pluginStore: pluginStore,
|
||||||
serverAdminOnly: section.Key("server_admin_only").MustBool(true),
|
serverAdminOnly: section.Key("server_admin_only").MustBool(true),
|
||||||
|
store: newStore(kvStore),
|
||||||
}
|
}
|
||||||
|
|
||||||
usageStats.RegisterMetricsFunc(s.getUsageStats)
|
usageStats.RegisterMetricsFunc(s.getUsageStats)
|
||||||
|
@ -6,11 +6,14 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/supportbundles"
|
"github.com/grafana/grafana/pkg/services/supportbundles"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -92,7 +95,43 @@ func (s *Service) bundle(ctx context.Context, collectors []string, uid string) (
|
|||||||
return nil, errCompress
|
return nil, errCompress
|
||||||
}
|
}
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
final := buf
|
||||||
|
if len(s.encryptionPublicKeys) > 0 {
|
||||||
|
var err error
|
||||||
|
final, err = encrypt(buf, s.encryptionPublicKeys...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return final.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encrypt(buf bytes.Buffer, publicKeys ...string) (bytes.Buffer, error) {
|
||||||
|
final := bytes.Buffer{}
|
||||||
|
recipients := make([]age.Recipient, 0, len(publicKeys))
|
||||||
|
for _, key := range publicKeys {
|
||||||
|
recipient, err := age.ParseX25519Recipient(key)
|
||||||
|
if err != nil {
|
||||||
|
return final, fmt.Errorf("unable to parse support bundle recipient public key: %w", err)
|
||||||
|
}
|
||||||
|
recipients = append(recipients, recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := age.Encrypt(&final, recipients...)
|
||||||
|
if err != nil {
|
||||||
|
return final, fmt.Errorf("unable to open support bundle encryption header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = w.Write(buf.Bytes()); err != nil {
|
||||||
|
return final, fmt.Errorf("unable to write support bundle encryption: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
|
return final, fmt.Errorf("unable to close support bundle encryption: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return final, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func compress(files map[string][]byte, buf io.Writer) error {
|
func compress(files map[string][]byte, buf io.Writer) error {
|
||||||
|
@ -0,0 +1,162 @@
|
|||||||
|
package supportbundlesimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/kvstore"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/supportbundles"
|
||||||
|
"github.com/grafana/grafana/pkg/services/supportbundles/bundleregistry"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testAgePublicKey = "age15xgfm9gg89nz92pqjat2hy9h7lpwwfwwgvchg67lqtzzhjtmg5vq9utnd4"
|
||||||
|
testAgePrivateKey = "AGE-SECRET-KEY-1HGMLT8VSC95UXN2R5LUZECXT42WW7TSEYQKCWLX7PKH3YHS6HGCQ0XVEFD"
|
||||||
|
testAgePublicKey2 = "age1q02sf508xetfa5ztzhuw0hxweyd50n27qndufaffvdth25knueds8w99c5"
|
||||||
|
testAgePrivateKey2 = "AGE-SECRET-KEY-1DSW2P60F2ZRY4D4M57PEKTFVYCXXDYYZ0VZWG5RTUZCWHR3EJ9TQP92JXQ"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_bundleCreate(t *testing.T) {
|
||||||
|
s := &Service{
|
||||||
|
log: log.New("test"),
|
||||||
|
bundleRegistry: bundleregistry.ProvideService(),
|
||||||
|
store: newStore(kvstore.NewFakeKVStore()),
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
|
||||||
|
collector := basicCollector(cfg)
|
||||||
|
s.bundleRegistry.RegisterSupportItemCollector(collector)
|
||||||
|
|
||||||
|
createdBundle, err := s.store.Create(context.Background(), &user.SignedInUser{UserID: 1, Login: "bob"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.startBundleWork(context.Background(), []string{collector.UID}, createdBundle.UID)
|
||||||
|
|
||||||
|
bundle, err := s.get(context.Background(), createdBundle.UID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, createdBundle.UID, bundle.UID)
|
||||||
|
assert.Equal(t, supportbundles.StateComplete, bundle.State)
|
||||||
|
assert.Equal(t, "bob", bundle.Creator)
|
||||||
|
assert.NotZero(t, len(bundle.TarBytes))
|
||||||
|
|
||||||
|
confirmFilesInTar(t, bundle.TarBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_bundleEncryptDecrypt(t *testing.T) {
|
||||||
|
s := &Service{
|
||||||
|
log: log.New("test"),
|
||||||
|
bundleRegistry: bundleregistry.ProvideService(),
|
||||||
|
store: newStore(kvstore.NewFakeKVStore()),
|
||||||
|
encryptionPublicKeys: []string{testAgePublicKey},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
|
||||||
|
collector := basicCollector(cfg)
|
||||||
|
s.bundleRegistry.RegisterSupportItemCollector(collector)
|
||||||
|
|
||||||
|
createdBundle, err := s.store.Create(context.Background(), &user.SignedInUser{UserID: 1, Login: "bob"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.startBundleWork(context.Background(), []string{collector.UID}, createdBundle.UID)
|
||||||
|
|
||||||
|
bundle, err := s.get(context.Background(), createdBundle.UID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, createdBundle.UID, bundle.UID)
|
||||||
|
assert.Equal(t, supportbundles.StateComplete, bundle.State)
|
||||||
|
assert.Equal(t, "bob", bundle.Creator)
|
||||||
|
assert.NotZero(t, len(bundle.TarBytes))
|
||||||
|
|
||||||
|
tarBytes := decryptTar(t, bundle.TarBytes, testAgePrivateKey)
|
||||||
|
assert.NotZero(t, len(tarBytes))
|
||||||
|
|
||||||
|
confirmFilesInTar(t, tarBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_bundleEncryptDecryptMultipleRecipients(t *testing.T) {
|
||||||
|
s := &Service{
|
||||||
|
log: log.New("test"),
|
||||||
|
bundleRegistry: bundleregistry.ProvideService(),
|
||||||
|
store: newStore(kvstore.NewFakeKVStore()),
|
||||||
|
encryptionPublicKeys: []string{testAgePublicKey, testAgePublicKey2},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
|
||||||
|
collector := basicCollector(cfg)
|
||||||
|
s.bundleRegistry.RegisterSupportItemCollector(collector)
|
||||||
|
|
||||||
|
createdBundle, err := s.store.Create(context.Background(), &user.SignedInUser{UserID: 1, Login: "bob"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s.startBundleWork(context.Background(), []string{collector.UID}, createdBundle.UID)
|
||||||
|
|
||||||
|
bundle, err := s.get(context.Background(), createdBundle.UID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, createdBundle.UID, bundle.UID)
|
||||||
|
assert.Equal(t, supportbundles.StateComplete, bundle.State)
|
||||||
|
assert.Equal(t, "bob", bundle.Creator)
|
||||||
|
assert.NotZero(t, len(bundle.TarBytes))
|
||||||
|
|
||||||
|
tarBytes := decryptTar(t, bundle.TarBytes, testAgePrivateKey)
|
||||||
|
assert.NotZero(t, len(tarBytes))
|
||||||
|
|
||||||
|
confirmFilesInTar(t, tarBytes)
|
||||||
|
|
||||||
|
tarBytes2 := decryptTar(t, bundle.TarBytes, testAgePrivateKey2)
|
||||||
|
assert.NotZero(t, len(tarBytes2))
|
||||||
|
|
||||||
|
confirmFilesInTar(t, tarBytes2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptTar(t *testing.T, tarBytes []byte, privateKey string) []byte {
|
||||||
|
reader := bytes.NewReader(tarBytes)
|
||||||
|
t.Helper()
|
||||||
|
recipientPK, err := age.ParseX25519Identity(privateKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tarBytesReader, err := age.Decrypt(reader, recipientPK)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newTarBytes, err := io.ReadAll(tarBytesReader)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return newTarBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the tarball contains the expected files
|
||||||
|
func confirmFilesInTar(t *testing.T, tarBytes []byte) {
|
||||||
|
t.Helper()
|
||||||
|
r := bytes.NewReader(tarBytes)
|
||||||
|
gzipReader, err := gzip.NewReader(r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tr := tar.NewReader(gzipReader)
|
||||||
|
files := []string{}
|
||||||
|
for {
|
||||||
|
hdr, err := tr.Next()
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
files = append(files, hdr.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, []string{"/bundle/basic.json"}, files)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user