Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow user to locally obfuscate secret in a keystore #5687

Merged
merged 2 commits into from
Dec 22, 2017

Conversation

ph
Copy link
Contributor

@ph ph commented Nov 22, 2017

This PR allow users to define sensitive information into an obfuscated data store on disk instead of having them defined in plaintext in the yaml configuration.

This add a few users facing commands:

# create new keystore to disk 
beat keystore create

# add a new key to the store.
beat keystore add output.elasticsearch.password

# remove a key from the store
beat keystore remove output.elasticsearch.password

# list the configured keys without the sensitive information
beat keystore list

The current implementation doesn't allow user to configure the secret with a custom password, this will come in future improvements of this feature.

How to use it

You can reference keys from the keystore using the same syntax that we use for the environment variables.

password: "${output.elasticsearch.password}"

When we unpack the configuration we will resolve the variables using the following priority:

  • Keystore
  • Environment variable
  • variable expansion

TODO before review

  • Fix the checker that prevent the tests from passing on travis :D
  • Manual testing
  • Split the library update vs this PR, update of the library was needed for a few encryption and terminal improvement.
  • fix the suite, the make testsuite fails locally for me.
  • movee the CLI test into libbeat
  • [ ] raise an error if we try override a config defined in the yaml file. (Another PR)
  • add testing around the merging of configs
  • Add doc (Done in another PR)
  • implement templating

This is the initial version, I don't expect it to be pluggable with every system, but some of the things are in place to make it pluggable.(ie: interface/factory)

closes: #3982

@ph ph added in progress Pull request is currently in progress. libbeat labels Nov 22, 2017
@ph ph force-pushed the feature/keystore branch from d141137 to 85e2b36 Compare November 22, 2017 17:08
panic(err)
}
return fmt.Sprint(path, string(os.PathSeparator), "keystore")
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self note: Add test for bad or corrupt keystore.

@@ -8,6 +8,7 @@
//
// path.data - Contains things that are expected to change often during normal
// operations (“registry” files, UUID file, etc.)

//
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self note: remove this line, thank you vim.

@ph ph changed the title Allow user to locally obfuscate secret in a keystore [WIP] Allow user to locally obfuscate secret in a keystore Nov 22, 2017
@ph ph closed this Nov 22, 2017
@ph ph reopened this Nov 22, 2017
Copy link
Member

@andrewkroh andrewkroh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job figuring out the AES-GCM stuff.

return b.Keystore()
}

// GenKeystoreCmd initialize the Keystore command to manage the Keystore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/GenKeystoreCmd/genKeystoreCmd/ (probably you haven't linted yet since it's not marked for review and I'm jumping the gun)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the linter on as I code, It used to be called GenKeystoreCmd, but I've renamed it since it doesn't need to be exported in any way. Not sure why my linter didn't catch it.

I run:

  Enabled Linters: ['gofmt', 'golint', 'go vet']

flagStdin, _ := cmd.Flags().GetBool("stdin")

if len(args) == 0 {
fmt.Fprintln(os.Stderr, "Failed to create the secret no key provided")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about Failed to create the secret: no key provided?

// Version of the keystore format, will be added at the beginning of the file
var version = []byte("v1")

// FileKeystore Allows to store key / secrets pair securely into an encrypted local file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these comments are rendered in our godoc pages can you please add a period to the end of the sentence. https://github.com/golang/go/wiki/CodeReviewComments#comment-sentences

temporaryPath := fmt.Sprintf("%s.tmp", k.Path)
defer func() {
if _, err := os.Stat(temporaryPath); os.IsExist(err) {
os.Remove(temporaryPath)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would only register a defer like this after you have "os.Create()ed" or "os.OpenFile()ed". But based on the code you have I think you could add a single os.Remove() call to the error handling for the failed os.Rename().

}
}()

if _, err := os.Stat(temporaryPath); os.IsExist(err) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should do this check before adding the above defer or else the file you're asking them to delete will be cleaned up already.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the defer, so this will also fix this.

return s.value, nil
}

// String custom string implementation to make sure we don't bleed this struct into a string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about GoString()? Do we need to implement that too to cover %#v or does String() cover that case too? https://golang.org/pkg/fmt/#GoStringer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we also need to cover that, I will add a test case for it.


_, err := os.Stat(k.Path)

if os.IsNotExist(err) && !override {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.Stat is going to return a nil error if the file exists. So should the logic be if err == nil && !override?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've changed the logic, it should if the File exist and we don't want to override.

	if os.IsExist(err) && !override {
		return ErrAlreadyExists
	}

Copy link
Member

@andrewkroh andrewkroh Nov 23, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using os.IsExists() is for checking the error and a nil error will be returned if the file exists. See the implementation for a better understanding.

// Create Allow to create an empty keystore
Create(override bool) error

// Exists check if the current keystore is persisted
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on this description maybe the method should be IsPersisted(). And then the implementation would take into account the dirty flag.

}

if config.Path == "" {
config.Path = fmt.Sprint(dataPath, string(os.PathSeparator), "keystore")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use filepath.Join().

store, err := keystore.Factory(b.Config.Keystore, b.Config.Path.Data)

if err != nil {
return fmt.Errorf("Could not initialize the keystore: %v", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ph ph force-pushed the feature/keystore branch 2 times, most recently from 7662b76 to 515280d Compare November 23, 2017 19:24
NOTICE.txt Outdated
@@ -3581,7 +3581,7 @@ SOFTWARE.

--------------------------------------------------------------------
Dependency: golang.org/x/crypto
Revision: 9419663f5a44be8b34ca85f08abc5fe1be11f8a3
Revision: 9f005a07e0d31d45e6656d241bb5c0f2efd4bc94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you update the revision of the dependencies in an separate PR? This would make sure the version change does not break other things and it would make this PR much smaller.

Copy link
Contributor Author

@ph ph Nov 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, I've marked as a TODO in the PR description.

ph added a commit to ph/beats that referenced this pull request Nov 28, 2017
- Added: golang.org/x/crypto/pbkdf2 (Key derivation function)
- Added: golang.org/x/crypto/ssh/terminal (Request a password securely
on the terminal)
- Updated: golang.org/x/sys/unix (required by terminal)
- Updated: golang.org/x/sys/windows (required by terminal)

Required by: elastic#5687
@ph ph force-pushed the feature/keystore branch from 26cd53a to 5910ba9 Compare November 28, 2017 19:14
@ph
Copy link
Contributor Author

ph commented Nov 28, 2017

@ruflin @andrewkroh @urso I have updated this PR with the following:

If you review #5735 I will rebase this PR this will make it easier to review.

Thanks

@ph ph changed the title [WIP] Allow user to locally obfuscate secret in a keystore Allow user to locally obfuscate secret in a keystore Nov 28, 2017
@ph ph added review and removed in progress Pull request is currently in progress. labels Nov 28, 2017
@ph
Copy link
Contributor Author

ph commented Nov 28, 2017

@joshbressers This is the PR for the keystore implementation in filebeat, its closer to the kibana implementation than the LS version.

@ph
Copy link
Contributor Author

ph commented Nov 28, 2017

jenkins test this please

ruflin pushed a commit that referenced this pull request Nov 28, 2017
- Added: golang.org/x/crypto/pbkdf2 (Key derivation function)
- Added: golang.org/x/crypto/ssh/terminal (Request a password securely
on the terminal)
- Updated: golang.org/x/sys/unix (required by terminal)
- Updated: golang.org/x/sys/windows (required by terminal)

Required by: #5687
@ph ph force-pushed the feature/keystore branch from 5910ba9 to f59b4f3 Compare November 29, 2017 01:44
@ph
Copy link
Contributor Author

ph commented Nov 29, 2017

Rebased with master, the vendored files are now gone! :)

Copy link
Contributor

@ruflin ruflin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skimmed through the code more on a high level. Looks good to me so far, I think it will be also helpful to play around with this feature. And we should do some follow up PR's which also touch other projects but would be overkill to be added to this PR.

@@ -413,6 +421,15 @@ func (b *Beat) configure() error {

b.Beat.Config = &b.Config.BeatConfig

store, err := keystore.Factory(b.Config.Keystore, b.Config.Path.Data)
keystore.OverwriteConfigOpts(store)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest you check the error before overwriting the config?


if err != nil {
fmt.Fprintf(os.Stderr, "Error initializing beat: %s\n", err)
os.Exit(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I initially wanted to comment on why you exit here instead of returning an error and handling it up stream. But I saw that this kind of a common pattern now in cmd package. @exekias Do we need to exit here or could we also return and error? In the past we tried to reduce the places of Exit to 1 place to make testing easier.

Copy link
Contributor Author

@ph ph Nov 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of theses cmd are tested using the python framework, no? In this case exiting early with os.Exit(1) make sense? There is an [helper in cobra](https://github.com/spf13/cobra/blob/e5f66de850af3302fbe378c8acded2b0fa55472c/cobra/cmd/helpers.go#L63 to do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should some helpers like Fatalf and Fatal that log and do os.Exit(1).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw. the log.Fatal(f) methods do print the message + do os.Exit(1). It's common practice to use this one in main function on simple tools.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for returning errors and having top-level do the exit handling :)

As cobra requires a Run function one can wrap it like this:

func runWith(fn func (args []string) error) func(*cobra.Command, []string) {
  return func(_ *cobra.Command, args []string) {
    if err := fn(args); err != nil {
      fmt.Error(err)
      os.Exit(1)
    }
  }
}

...

  Run: runWith(func(args []string) error {
    return errors.New("TODO")
  })

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, and doing this through the logger has the benefit that the logger can flush and sync before exiting.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@urso @andrewkroh I can make the changes on the keystore cmd, we can create another issue to refactor the other command to have the same flow or execution, sound like plan?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, lets do a PR with the runWith change and slowly refactor commands, I think its better to split concerns in multiple PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed the notification here, but I totally agree with @urso, those run wrappers are +:100:

assert.NoError(t, err)

// unlikely to get 2 times the same results
assert.False(t, bytes.Equal(v1, v2))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using assert.NotEqual(t, string(v1[:]), string(v2[:])) would probably give a nicer error message.

"strings"
)

// ReadInput Capture user input for a question
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit scary that we now have interactive terminal support :-D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but all the command can be used without requiring interactive calls, so we are still friendly for orchestration.

reader := bufio.NewReader(os.Stdin)

input, err := reader.ReadString('\n')

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume @andrewkroh is not going to like these newlines even thought it's not after a { ;-) Often the err check is directly after without a newline.

// NewFileKeystore returns an new File based keystore or an error, currently users cannot set their
// own password on the keystore, the default password will be an empty string. When the keystore
// is initialied the secrets are automatically loaded into memory.
func NewFileKeystore(keystoreFile string) (Keystore, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there are potentially coming more key stores in the future, I'm thinking if we should put each in it's own package. keystore/file/... for this one, so this would be just New. BTW that is refactoring that could also be done in a second step.

Copy link
Contributor Author

@ph ph Nov 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with that, since the pluggable part of the keystore is not complete, I think we should do it when we add more.

return fmt.Errorf("cannot encrypt the keystore: %v", err)
}

f, err := os.OpenFile(temporaryPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, filePermission)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should really abstract out on how we write files from the filebeat registry and winlobeat registry: https://github.com/elastic/beats/blob/master/filebeat/registrar/registrar.go#L230 There are strange issues that can happen on shutdown and I remember @andrewkroh fixed on of those in winlogbeat. Having the logic on one place would make sure fixes are applied everywhere.

Also see the safeFileRotate helper: https://github.com/elastic/beats/blob/master/filebeat/registrar/registrar.go#L251

@ph Could we create a follow up meta issue to track such things we should cleanup after getting this PR in. Happy to help with abstracting out this logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ruflin I have created #5755 for a specific follow up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ruflin Also create this meta issue #5757 to do a bit more work.

@ph ph force-pushed the feature/keystore branch 4 times, most recently from b5445db to 4599714 Compare December 19, 2017 16:04
@ph
Copy link
Contributor Author

ph commented Dec 19, 2017

@tsg Ready for another round of testing!

// We have to initialize the keystore before any unpack or merging the cloud
// options.
keystoreCfg, _ := cfg.Child("keystore", -1)
pathData, _ := cfg.String("path.data", -1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if path.data (typically /usr/share/beatname/data is the right place for the keystore. Alternatively we could consider path.config (typically /etc/beatname)?

We should probably follow what ES/KB did, do you know?

Also, I guess we'd want to check the permissions on the file like we do with the YAML config files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, /etc/beatname make more sense, I haven't checked ES/KB I will verify that and make the change if needed.

We do check for permission https://github.com/elastic/beats/pull/5687/files#diff-33a534765f797732e272dc741db414f4R233 if StrictPerm(0600) is True.

@tsg
Copy link
Contributor

tsg commented Dec 20, 2017

@ph did you do any tests of the commands on Windows? Just very slightly worried about the terminal.Prompt calls and how they are implemented on Windows.

@ph
Copy link
Contributor Author

ph commented Dec 20, 2017

@tsg Concerning windows, I've tested it early and it was OK, but I will do another complete run just to be on the safe side.

@ph
Copy link
Contributor Author

ph commented Dec 20, 2017

@tsg small issue on windows powershell, will get a fix for that. Good point, I am still assuming more compatibility than I should coming from the jvm.

@ph ph force-pushed the feature/keystore branch from 44a99a3 to 5232ccf Compare December 20, 2017 02:31
@ph
Copy link
Contributor Author

ph commented Dec 20, 2017

@tsg Windows is all good and happy now with the exact same behavior as unix. ssh/terminal for readPassword handle windows gracefully

@ph ph force-pushed the feature/keystore branch 3 times, most recently from 8864815 to 569ee8b Compare December 20, 2017 15:49
@ph
Copy link
Contributor Author

ph commented Dec 20, 2017

@tsg I've addressed your concerns, adding log debug when successfully resolving keys from the keystore and where the keystore is loaded. Also I've changed the default path of the keystore to ${path.config}/${beatname}.keystore instead of ${path.data}/keystore to reflect Elasticsearch installation.

@ph
Copy link
Contributor Author

ph commented Dec 20, 2017

jenkins test this please

@tsg
Copy link
Contributor

tsg commented Dec 20, 2017

@ph The last Jenkins run is almost green, but there's a failure on Windows that looks related to the PR:

16:50:16 System testing filebeat
16:55:25 S..S.........S.............................................FSSSSSS..................................S............S......SS......S...............
16:55:25 ======================================================================
16:55:25 FAIL: Test that we correctly do string replacement with values from the keystore
16:55:25 ----------------------------------------------------------------------
16:55:25 Traceback (most recent call last):
16:55:25   File "C:\Users\jenkins\workspace\elastic+beats+pull-request+multijob-windows\beat\filebeat\label\windows\src\github.com\elastic\beats\filebeat\tests\system\test_keystore.py", line 36, in test_keystore_with_present_key
16:55:25     proc.check_kill_and_wait(1)
16:55:25   File "C:\Users\jenkins\workspace\elastic+beats+pull-request+multijob-windows\beat\filebeat\label\windows\src\github.com\elastic\beats\filebeat\tests\system\../../../libbeat/tests/system\beat\beat.py", line 91, in check_kill_and_wait
16:55:25     return self.check_wait(exit_code=exit_code)
16:55:25   File "C:\Users\jenkins\workspace\elastic+beats+pull-request+multijob-windows\beat\filebeat\label\windows\src\github.com\elastic\beats\filebeat\tests\system\../../../libbeat/tests/system\beat\beat.py", line 80, in check_wait
16:55:25     exit_code, actual_exit_code)
16:55:25 AssertionError: Expected exit code to be 1, but it was 2
16:55:25 

I triggered another Travis run on Filebeat as well.

Copy link
Contributor

@tsg tsg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, if the test failure is solved.

ph added 2 commits December 20, 2017 16:53
This PR allow users to define sensitive information into an obfuscated data store on disk instead of having them defined in plaintext in the yaml configuration.

This add a few users facing commands:

beat keystore create

beat keystore add output.elasticsearch.password

beat keystore remove output.elasticsearch.password

beat keystore list
The current implementation doesn't allow user to configure the secret with a custom password, this will come in future improvements of this feature.
@ph ph force-pushed the feature/keystore branch from 85e39bf to f34a28a Compare December 20, 2017 21:53
@ph
Copy link
Contributor Author

ph commented Dec 20, 2017

@tsg I've pushed a new commit for the test on windows, I will monitor the greeness of it. The Appveyor tests are also failing for winlogbeat, but this should not be related to anything this PR touch,.

@tsg tsg merged commit 0f09311 into elastic:master Dec 22, 2017

// Stretch the user provided key
password, _ := k.password.Get()
passwordBytes := pbkdf2.Key(password, salt, iterationsCount, keyLength, sha512.New)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider replacing pbkdf2 with a never key derivation function like scrypt or even Argon2. Argon2 was very recently merged into golang.org/x/crypto/ .
Some resources about the topic:
https://www.linkedin.com/pulse/top-password-hashing-schemes-employ-today-chintan-jain-mba-cissp
https://download.libsodium.org/doc/password_hashing/
https://core.trac.wordpress.org/ticket/39499
https://gitlab.com/cryptsetup/cryptsetup/issues/119

In addition I would recommend to store the key derivation function and the iteration along side with the VERSION|SALT|IV|PAYLOAD.
If you decide to change the iteration count or the key derivation function you don't need to increase the file format version.
This is kind of similar to the https://secure.php.net/manual/en/function.password-hash.php. In your case the the key would be left out.

With this implementation it's possible to increase the key derivation parameters to prevent faster CPU/GPU's. To achieve this the maintainer needs to alter the key derivation parameters from time to time and if a change is detected from the key derivation parameters stored in the storage file the encrypted payload needs to be decrypted and encrypted with the new parameters.
Similar to https://secure.php.net/manual/en/function.password-needs-rehash.php

To reduce code duplication I would extract the passwordBytes generation passwordBytes := pbkdf2.Key(password, salt, iterationsCount, keyLength, sha512.New) to it own private function.

Btw: The rest of the encryption part looks good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dol I've created the following issues:

#6015
#6016

I will do the change right away to add the function in the format and check for argon2.

@ph
Copy link
Contributor Author

ph commented Jan 7, 2018 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Store secrets in a keystore
8 participants