Skip to content

Commit

Permalink
pkg/cli: add debug encryption-at-rest decrypt command
Browse files Browse the repository at this point in the history
During storage-level L2 investigations, files from problematic stores
are often requested (e.g. the MANIFEST file(s), SSTables, etc.). In
cases where the store is using encryption-at-rest, the debug artifacts
are useless unless they have been decrypted.

Add a new debug command that can be used to decrypt a file in-situ,
given the encryption spec for the store, and a path to an encrypted file
in the store. For example:

```bash
$ cockroach debug encryption-at-rest \
  path=/path/to/store,key=/path/to/aes.key,old-key=plain \
  /path/to/encrypted/file \
  /path/to/decrypted/output/file
```

Touches cockroachdb#89095.

Release note (ops change): A new debug tool was added to allow for
decrypting files in a store using encryption-at-rest. This tool is
intended for use while debugging, or for providing debug artifacts to
Cockroach Labs to aid with support investigations. It is intended to be
run "in-situ" (i.e. on site), as it prevents having to move sensitive
key material.
  • Loading branch information
nicktrav committed Oct 10, 2022
1 parent 1db259f commit e8fc99e
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 7 deletions.
8 changes: 4 additions & 4 deletions pkg/ccl/baseccl/encryption_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ type StoreEncryptionSpec struct {
RotationPeriod time.Duration
}

// Convert to a serialized EncryptionOptions protobuf.
func (es StoreEncryptionSpec) toEncryptionOptions() ([]byte, error) {
// ToEncryptionOptions convert to a serialized EncryptionOptions protobuf.
func (es StoreEncryptionSpec) ToEncryptionOptions() ([]byte, error) {
opts := EncryptionOptions{
KeySource: EncryptionKeySource_KeyFiles,
KeyFiles: &EncryptionKeyFiles{
Expand Down Expand Up @@ -206,7 +206,7 @@ func PopulateStoreSpecWithEncryption(

// Tell the store we absolutely need the file registry.
storeSpecs.Specs[i].UseFileRegistry = true
opts, err := es.toEncryptionOptions()
opts, err := es.ToEncryptionOptions()
if err != nil {
return err
}
Expand Down Expand Up @@ -235,7 +235,7 @@ func EncryptionOptionsForStore(

for _, es := range encryptionSpecs.Specs {
if es.Path == path {
return es.toEncryptionOptions()
return es.ToEncryptionOptions()
}
}

Expand Down
6 changes: 3 additions & 3 deletions pkg/ccl/storageccl/engineccl/encrypted_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,16 +294,16 @@ func (e *encryptionStatsHandler) GetKeyIDFromSettings(settings []byte) (string,

// init initializes function hooks used in non-CCL code.
func init() {
storage.NewEncryptedEnvFunc = newEncryptedEnv
storage.NewEncryptedEnvFunc = NewEncryptedEnv
storage.CanRegistryElideFunc = canRegistryElide
}

// newEncryptedEnv creates an encrypted environment and returns the vfs.FS to use for reading and
// NewEncryptedEnv creates an encrypted environment and returns the vfs.FS to use for reading and
// writing data. The optionBytes is a binary serialized baseccl.EncryptionOptions, so that non-CCL
// code does not depend on CCL code.
//
// See the comment at the top of this file for the structure of this environment.
func newEncryptedEnv(
func NewEncryptedEnv(
fs vfs.FS, fr *storage.PebbleFileRegistry, dbDir string, readOnly bool, optionBytes []byte,
) (*storage.EncryptionEnv, error) {
options := &baseccl.EncryptionOptions{}
Expand Down
6 changes: 6 additions & 0 deletions pkg/cli/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ go_library(
"demo.go",
"demo_telemetry.go",
"doctor.go",
"ear.go",
"env.go",
"examples.go",
"flags.go",
Expand Down Expand Up @@ -102,8 +103,10 @@ go_library(
deps = [
"//pkg/base",
"//pkg/build",
"//pkg/ccl/baseccl",
"//pkg/ccl/sqlproxyccl",
"//pkg/ccl/sqlproxyccl/tenantdirsvr",
"//pkg/ccl/storageccl/engineccl",
"//pkg/cli/clicfg",
"//pkg/cli/clientflags",
"//pkg/cli/clienturl",
Expand Down Expand Up @@ -321,6 +324,7 @@ go_test(
"demo_locality_test.go",
"demo_test.go",
"doctor_test.go",
"ear_test.go",
"flags_test.go",
"gen_test.go",
"haproxy_test.go",
Expand Down Expand Up @@ -348,6 +352,7 @@ go_test(
deps = [
"//pkg/base",
"//pkg/build",
"//pkg/ccl/baseccl",
"//pkg/ccl/kvccl/kvtenantccl",
"//pkg/cli/clicfg",
"//pkg/cli/clierror",
Expand Down Expand Up @@ -402,6 +407,7 @@ go_test(
"//pkg/workload/examples",
"@com_github_cockroachdb_datadriven//:datadriven",
"@com_github_cockroachdb_errors//:errors",
"@com_github_cockroachdb_pebble//:pebble",
"@com_github_cockroachdb_pebble//vfs",
"@com_github_spf13_cobra//:cobra",
"@com_github_spf13_pflag//:pflag",
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,8 @@ func init() {

DebugCmd.AddCommand(debugJobTraceFromClusterCmd)

DebugCmd.AddCommand(earCmd)

f := debugSyncBenchCmd.Flags()
f.IntVarP(&syncBenchOpts.Concurrency, "concurrency", "c", syncBenchOpts.Concurrency,
"number of concurrent writers")
Expand Down
106 changes: 106 additions & 0 deletions pkg/cli/ear.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
"io"
"os"

"github.com/cockroachdb/cockroach/pkg/ccl/baseccl"
"github.com/cockroachdb/cockroach/pkg/ccl/storageccl/engineccl"
"github.com/cockroachdb/cockroach/pkg/cli/clierrorplus"
"github.com/cockroachdb/cockroach/pkg/storage"
"github.com/cockroachdb/errors"
"github.com/spf13/cobra"
)

// Sub-commands for encryption-at-rest command.
var earCmds = []*cobra.Command{
earDecryptCmd,
}

var earCmd = &cobra.Command{
Use: "encryption-at-rest [command]",
Short: "tools for working with encrypted stores",
}

func init() {
earCmd.AddCommand(earCmds...)
}

var earDecryptCmd = &cobra.Command{
Use: "decrypt <encryption-spec> <in-file> [out-file]",
Short: "decrypt a file from an encrypted store",
Long: `Decrypts an file from an encrypted store, and outputs it to the
specified path.
If out-file is not specified, the command will output the decrypted contents to
stdout.
`,
Args: cobra.MinimumNArgs(2),
RunE: clierrorplus.MaybeDecorateError(runDecrypt),
}

func runDecrypt(cmd *cobra.Command, args []string) (returnErr error) {
encSpecStr, inPath := args[0], args[1]
var outPath string
if len(args) > 2 {
outPath = args[2]
}

encSpec, err := baseccl.NewStoreEncryptionSpec(encSpecStr)
if err != nil {
return errors.Wrap(err, "could not parse encryption spec")
}
storePath := encSpec.Path
encOpts, err := encSpec.ToEncryptionOptions()
if err != nil {
return errors.Wrap(err, "could not generate encryption opts")
}

fs := &absoluteFS{pebbleToolFS}
fr := &storage.PebbleFileRegistry{
FS: fs,
DBDir: storePath,
ReadOnly: true,
}
if err := fr.Load(); err != nil {
return errors.Wrap(err, "could not load file registry")
}

env, err := engineccl.NewEncryptedEnv(fs, fr, storePath, true, encOpts)
if err != nil {
return errors.Wrap(err, "could not construct encryption env")
}

// Open the specified file through the FS, decrypting it.
f, err := env.FS.Open(inPath)
if err != nil {
return errors.Wrapf(err, "could not open input file %s", inPath)
}
defer f.Close()

// Copy the raw bytes into the destination file.
outFile := cmd.OutOrStdout()
if outPath != "" {
f, err := os.OpenFile(outPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return errors.Wrapf(err, "could not open output file %s", outPath)
}
defer f.Close()
outFile = f
}
if _, err = io.Copy(outFile, f); err != nil {
return errors.Wrap(err, "could not write to output file")
}

return nil
}
127 changes: 127 additions & 0 deletions pkg/cli/ear_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2022 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
"bytes"
"context"
"fmt"
"path/filepath"
"strings"
"testing"

"github.com/cockroachdb/cockroach/pkg/base"
"github.com/cockroachdb/cockroach/pkg/ccl/baseccl"
"github.com/cockroachdb/cockroach/pkg/storage"
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/pebble"
"github.com/cockroachdb/pebble/vfs"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
)

func TestDecrypt(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

ctx := context.Background()
dir := t.TempDir()

// Generate a new encryption key to use.
keyPath := filepath.Join(dir, "aes.key")
err := genEncryptionKeyCmd.RunE(nil, []string{keyPath})
require.NoError(t, err)

// Spin up a new encrypted store.
encSpecStr := fmt.Sprintf("path=%s,key=%s,old-key=plain", dir, keyPath)
encSpec, err := baseccl.NewStoreEncryptionSpec(encSpecStr)
require.NoError(t, err)
encOpts, err := encSpec.ToEncryptionOptions()
require.NoError(t, err)

p, err := storage.NewPebble(ctx, storage.PebbleConfig{
StorageConfig: base.StorageConfig{
Dir: dir,
UseFileRegistry: true,
EncryptionOptions: encOpts,
},
Opts: &pebble.Options{
FS: vfs.Default,
},
})
require.NoError(t, err)
defer p.Close()

// Write some data and flush the memtable.
err = p.PutUnversioned([]byte("foo"), []byte("bar"))
require.NoError(t, err)
require.NoError(t, p.Flush())

// Find a manifest file to check.
files, err := p.List(dir)
require.NoError(t, err)
var manifestPath string
for _, basename := range files {
if strings.HasPrefix(basename, "MANIFEST-") {
manifestPath = filepath.Join(dir, basename)
break
}
}

// Should have found a manifest file.
require.NotEmpty(t, manifestPath)

// Pluck the `pebble manifest dump` command out of the debug command.
dumpCmd := getPebbleTool(DebugPebbleCmd, []string{"pebble", "manifest", "dump"})
require.NotNil(t, dumpCmd)

// Check that the encrypted manifest file cannot be read.
dumpManifest := func(path string) string {
var b bytes.Buffer
dumpCmd.SetOut(&b)
dumpCmd.SetErr(&b)
dumpCmd.Run(dumpCmd, []string{path})
return b.String()
}
out := dumpManifest(manifestPath)
// Check for the addition of the table that was flushed.
require.NotContains(t, out, "added:")

// Decrypt the manifest file.
outPath := filepath.Join(dir, "manifest.plain")
err = earDecryptCmd.RunE(earDecryptCmd, []string{encSpecStr, manifestPath, outPath})
require.NoError(t, err)

// Check that the decrypted manifest file can now be read.
out = dumpManifest(outPath)
require.Contains(t, out, "added:")
}

// getPebbleTool traverses the given cobra.Command recursively, searching for a
// Pebble tool matching the given command.
func getPebbleTool(cmd *cobra.Command, want []string) *cobra.Command {
// Base cases.
if cmd.Name() != want[0] {
return nil
}
if len(want) == 1 {
return cmd
}
// Recursive case.
for _, subCmd := range cmd.Commands() {
found := getPebbleTool(subCmd, want[1:])
if found != nil {
return found
}
}
return nil
}

0 comments on commit e8fc99e

Please sign in to comment.