-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Conversation
panic(err) | ||
} | ||
return fmt.Sprint(path, string(os.PathSeparator), "keystore") | ||
} |
There was a problem hiding this comment.
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.
libbeat/paths/paths.go
Outdated
@@ -8,6 +8,7 @@ | |||
// | |||
// path.data - Contains things that are expected to change often during normal | |||
// operations (“registry” files, UUID file, etc.) | |||
|
|||
// |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
libbeat/cmd/keystore.go
Outdated
return b.Keystore() | ||
} | ||
|
||
// GenKeystoreCmd initialize the Keystore command to manage the Keystore |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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']
libbeat/cmd/keystore.go
Outdated
flagStdin, _ := cmd.Flags().GetBool("stdin") | ||
|
||
if len(args) == 0 { | ||
fmt.Fprintln(os.Stderr, "Failed to create the secret no key provided") |
There was a problem hiding this comment.
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
?
libbeat/keystore/file_keystore.go
Outdated
// 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 |
There was a problem hiding this comment.
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
libbeat/keystore/file_keystore.go
Outdated
temporaryPath := fmt.Sprintf("%s.tmp", k.Path) | ||
defer func() { | ||
if _, err := os.Stat(temporaryPath); os.IsExist(err) { | ||
os.Remove(temporaryPath) |
There was a problem hiding this comment.
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()
.
libbeat/keystore/file_keystore.go
Outdated
} | ||
}() | ||
|
||
if _, err := os.Stat(temporaryPath); os.IsExist(err) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
libbeat/keystore/secure_string.go
Outdated
return s.value, nil | ||
} | ||
|
||
// String custom string implementation to make sure we don't bleed this struct into a string |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
libbeat/keystore/file_keystore.go
Outdated
|
||
_, err := os.Stat(k.Path) | ||
|
||
if os.IsNotExist(err) && !override { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.
libbeat/keystore/keystore.go
Outdated
// Create Allow to create an empty keystore | ||
Create(override bool) error | ||
|
||
// Exists check if the current keystore is persisted |
There was a problem hiding this comment.
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.
libbeat/keystore/keystore.go
Outdated
} | ||
|
||
if config.Path == "" { | ||
config.Path = fmt.Sprint(dataPath, string(os.PathSeparator), "keystore") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use filepath.Join()
.
libbeat/cmd/instance/beat.go
Outdated
store, err := keystore.Factory(b.Config.Keystore, b.Config.Path.Data) | ||
|
||
if err != nil { | ||
return fmt.Errorf("Could not initialize the keystore: %v", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a lower cased error message. https://github.com/golang/go/wiki/CodeReviewComments#error-strings
7662b76
to
515280d
Compare
NOTICE.txt
Outdated
@@ -3581,7 +3581,7 @@ SOFTWARE. | |||
|
|||
-------------------------------------------------------------------- | |||
Dependency: golang.org/x/crypto | |||
Revision: 9419663f5a44be8b34ca85f08abc5fe1be11f8a3 | |||
Revision: 9f005a07e0d31d45e6656d241bb5c0f2efd4bc94 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
- 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
@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 |
@joshbressers This is the PR for the keystore implementation in filebeat, its closer to the kibana implementation than the LS version. |
jenkins test this please |
- 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
Rebased with master, the vendored files are now gone! :) |
There was a problem hiding this 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.
libbeat/cmd/instance/beat.go
Outdated
@@ -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) |
There was a problem hiding this comment.
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?
libbeat/cmd/keystore.go
Outdated
|
||
if err != nil { | ||
fmt.Fprintf(os.Stderr, "Error initializing beat: %s\n", err) | ||
os.Exit(1) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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")
})
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
libbeat/common/terminal/terminal.go
Outdated
reader := bufio.NewReader(os.Stdin) | ||
|
||
input, err := reader.ReadString('\n') | ||
|
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
libbeat/keystore/file_keystore.go
Outdated
return fmt.Errorf("cannot encrypt the keystore: %v", err) | ||
} | ||
|
||
f, err := os.OpenFile(temporaryPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, filePermission) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
b5445db
to
4599714
Compare
@tsg Ready for another round of testing! |
libbeat/cmd/instance/beat.go
Outdated
// 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@ph did you do any tests of the commands on Windows? Just very slightly worried about the |
@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. |
@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. |
@tsg Windows is all good and happy now with the exact same behavior as unix. |
8864815
to
569ee8b
Compare
@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 |
jenkins test this please |
@ph The last Jenkins run is almost green, but there's a failure on Windows that looks related to the PR:
I triggered another Travis run on Filebeat as well. |
There was a problem hiding this 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.
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.
@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,. |
|
||
// Stretch the user provided key | ||
password, _ := k.password.Get() | ||
passwordBytes := pbkdf2.Key(password, salt, iterationsCount, keyLength, sha512.New) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion I will do a follow up on that.
On Fri, Jan 5, 2018 at 6:29 PM Dominic ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In libbeat/keystore/file_keystore.go
<#5687 (comment)>:
> + // randomly generate the salt and the initialization vector, this information will be saved
+ // on disk in the file as part of the header
+ iv, err := common.RandomBytes(iVLength)
+
+ if err != nil {
+ return nil, err
+ }
+
+ salt, err := common.RandomBytes(saltLength)
+ if err != nil {
+ return nil, err
+ }
+
+ // Stretch the user provided key
+ password, _ := k.password.Get()
+ passwordBytes := pbkdf2.Key(password, salt, iterationsCount, keyLength, sha512.New)
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.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#5687 (review)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAACgIZqPS5LnJi_-FzjyI2BdzmSYzG7ks5tHrBHgaJpZM4Qnuc3>
.
--
ph
|
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:
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.
When we unpack the configuration we will resolve the variables using the following priority:
TODO before review
[ ] raise an error if we try override a config defined in the yaml file. (Another PR)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