-
Notifications
You must be signed in to change notification settings - Fork 25.2k
Add infrastructure for elasticsearch keystore #22335
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
Conversation
This change is the first towards providing the ability to store sensitive settings in elasticsearch. It adds the `elasticsearch-keystore` tool, which allows managing a java keystore. The keystore is loaded upon node startup in Elasticsearch, and used by the Setting infrastructure when a setting is configured as secure. There are a lot of caveats to this PR. The most important is it only provides the tool and setting infrastructure for secure strings. It does not yet provide for keystore passwords, keypairs, certificates, or even convert any existing string settings to secure string settings. Those will all come in follow up PRs. But this PR was already too big, so this at least gets a basic version of the infrastructure in. The two main things to look at. The first is the `SecureSetting` class, which extends `Setting`, but removes the assumption for the raw value of the setting to be a string. SecureSetting provides, for now, a single helper, `stringSetting()` to create a SecureSetting which will return a SecureString (which is like String, but is closeable, so that the underlying character array can be cleared). The second is the `KeyStoreWrapper` class, which wraps the java `KeyStore` to provide a simpler api (we do not need the entire keystore api) and also extend the serialized format to add metadata needed for loading the keystore with no assumptions about keystore type (so that we can change this in the future) as well as whether the keystore has a password (so that we can know whether prompting is necessary when we add support for keystore passwords).
For those just wanting to know what the interaction with the keystore tool looks like, here is the help:
|
One final note for reviewers: The shell scripts are copies of |
@rjernst Something which is unclear to me. Is the keystore operation something that people will have to do on every single node? Or do it on a first node, then copy with scp the keytore files to the other nodes? I'm trying to understand how complex it will be for the end user. Probably something we will have to document in details. |
@dadoonet Users could do either. I don't see it different than the node settings in elasticsearch.yml. They have to get to each node somehow. Either the editing work is done on each node, or a single file is copied to each node. |
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 left some comments, but I think this is awesome to finally have
@Override | ||
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception { | ||
KeyStoreWrapper keystore = KeyStoreWrapper.loadMetadata(env.configFile()); | ||
if (keystore == null) { |
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.
More of a user friendliness / usability aspect, but it would be great if we invoked the CreateKeyStoreCommand here. It is pretty minor since the create command would ideally only be run once.
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 don't think we should. I would rather it be an error and explicit rather than magic/leniency.
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.
That's also fine with me. It's not a big deal since it's probably a one time only operation
String setting = arguments.value(options); | ||
if (keystore.getSettings().contains(setting) && options.has(forceOption) == false) { | ||
String answer = terminal.readText("Setting " + setting + " already exists. Overwrite? [y/N]"); | ||
if (answer.equals("y") == false) { |
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.
Another minor thing from a usability standpoint, can we check the input and make sure it was either y
or N
and re-prompt if it was a typo? It would apply in other places as well where we do the same thing
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.
Sure I can add that.
private static final SecretKeyFactory passwordFactory; | ||
static { | ||
try { | ||
passwordFactory = SecretKeyFactory.getInstance("PBE"); |
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.
The Standard Algorithm Names docs don't list PBE as a algorithm for the SecretKeyFactory. I'm thinking we should be more explicit and use something like PBEWithHmacSHA256AndAES_128
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.
Ok, I made this explicit, and added the algorithm name to the metadata we store before the keystore, so that we can change this in the future.
terminal.println(entry); | ||
} | ||
|
||
// 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.
can we drop this TODO or move it to a more appropriate place?
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.
Removed. These were leftover notes to myself which were already addressed.
} | ||
|
||
/** | ||
* Convert to a {@link String}. This only be used with APIs that do not take {@link CharSequence}. |
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.
missing should
I think
import java.util.Objects; | ||
|
||
/** | ||
* A String implementations which allows clearing the underlying char array. |
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.
can we add something about how this is not a thread-safe object (one closing while the other is reading)
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.
lets synchronized all methods here and we are good
@@ -613,6 +636,13 @@ public String get(String key) { | |||
return map.get(key); | |||
} | |||
|
|||
/** Sets the secret store for these settings. */ | |||
public void setKeyStore(KeyStoreWrapper keystore) { | |||
assert this.keystore == null; // only set once! |
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 think we should use a SetOnce
here, especially with a public method
/** Sets the secret store for these settings. */ | ||
public void setKeyStore(KeyStoreWrapper keystore) { | ||
assert this.keystore == null; // only set once! | ||
assert keystore.isLoaded(); |
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.
can this be a hard check that throws a IAE if the keystore is not loaded?
@jaymode I believe I addressed all your feedback in 4e4a40d. The only outstanding issue is using the explicit PBE algo. Unfortunately for some reason PKCS12 does not work with those explicit algos (it works with some others, but not of the hmac based ones, getting an unrecognized algorithm name when adding a key). But I did make the algorithm changeable, so when we figure it out, we can simply change the algo used and any new/updated keystore will use the new algo. |
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.
Left a minor comment but other than that LGTM
@@ -613,6 +637,14 @@ public String get(String key) { | |||
return map.get(key); | |||
} | |||
|
|||
/** Sets the secret store for these settings. */ | |||
public void setKeyStore(KeyStoreWrapper keystore) { | |||
if (keystore.isLoaded()) { |
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.
Probably want to move the requireNonNull here
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 can just remove it it will fail with NPE either way
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 left a bunch of comments. This looks awesome thanks ryan
/** | ||
* A subcommand for the keystore cli which adds a string setting. | ||
*/ | ||
class AddStringKeyStoreCommand extends EnvironmentAwareCommand { |
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.
final?
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.
These can't be final because they are subclassed in tests in order to manipulate eg the environment the command runs with.
/** | ||
* A subcommand for the keystore cli to create a new keystore. | ||
*/ | ||
class CreateKeyStoreCommand extends EnvironmentAwareCommand { |
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.
final?
} | ||
|
||
/** Retrieve a string setting. The {@link SecureString} should be closed once it is used. */ | ||
SecureString getStringSetting(String setting) throws GeneralSecurityException { |
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 we should think about adding a special security permission that we have to grant to all plugins that want to access the keystore. That way we can prevent unintended access to it and users will be prompted when they install the plugin. Only certain plugins should be able to access this stuff right?
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 don't think certain plugins should be able to access, but I have thought about allowing plugins to only read the secure settings they have registered, and also only allowing reading a setting from the keystore wrapper a single time. But this should be another followup investigation.
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.
sure maybe add a todo to the code so we don't forget
if (Files.exists(keystoreFile) == false) { | ||
return null; | ||
} | ||
DataInputStream inputStream = new DataInputStream(Files.newInputStream(keystoreFile)); |
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.
can we make use of CodecUtil and write a head as well as a footer. I really want this to be checksummed given that we embed this keystore in our own file. We already have InputStreamIndexInput
and OutputStreamIndexOutput
so I think it should be doable?
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.
The keystore itself already has its own integrity checks, so I'm not sure this is needed.
/** Loads the keystore this metadata wraps. This may only be called once. */ | ||
public void loadKeystore(char[] password) throws GeneralSecurityException, IOException { | ||
this.keystore.set(KeyStore.getInstance(type)); | ||
try (InputStream in = input) { |
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 think we should not hold on to this InputStream... rather go an open it again, skip the metadata, check the checksum and load all values. That way we never leak any resources and everything is contained. it's also less complex since you don't have that input
member that could be closed at some point...
} | ||
|
||
/** Write the keystore to the given config directory. */ | ||
void save(Path configDir) throws Exception { |
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 think just to be safe you should synchronized this method so it won't be invoked concurrently
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.
same is true for load
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 also wonder if the mutators here like save, remove etc. should require some security permission that we only give to the key tool in it's main method? - we run without sec manager on that anyway so I think we are safe but can prevent unintended access
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.
Regarding permissions, the only way that could work is if we make the tool a separate jar. We would then need to still have the main ES jar (and probably lucene jars if we start using indexinput/output stuff) with permissions, so I'm not sure what exactly it buys us. I have the write methods as package private already. A more immediate change we could make which could help would be to seal the settings package within the core jar (although we would need to think about how testing works then). But I think this should be a follow up investigation, after many other things are achieved (password protection, existing settings migrated).
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 am confused, afaik this entire thing is only for plugins. Where in core do we need a secret/password?
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 don't. My point was code outside of core should not be calling save (hence it is package private). But it is moot since we do not give permissions to write to the config dir, so only the keystore tool (which doesn't run under SM) can write it.
* Convert to a {@link String}. This should only be used with APIs that do not take {@link CharSequence}. | ||
*/ | ||
@Override | ||
public String toString() { |
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.
won't this render this entire thing useless? I mean we are copying the secure setting here?
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 unfortunately the external things we use these secure settings with mostly take String, eg aws credentials. To allow passing SecureString, we would need to get them to change to accepting CharSequence.
import java.util.Objects; | ||
|
||
/** | ||
* A String implementations which allows clearing the underlying char array. |
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.
lets synchronized all methods here and we are good
@@ -613,6 +637,14 @@ public String get(String key) { | |||
return map.get(key); | |||
} | |||
|
|||
/** Sets the secret store for these settings. */ | |||
public void setKeyStore(KeyStoreWrapper keystore) { | |||
if (keystore.isLoaded()) { |
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 can just remove it it will fail with NPE either way
@@ -57,9 +57,9 @@ protected Environment createEnv(Terminal terminal, Map<String, String> settings) | |||
return new Environment(realSettings); | |||
} | |||
@Override | |||
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Settings initialSettings) { | |||
void init(final boolean daemonize, final Path pidFile, final boolean quiet, Environment env0) { |
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.
can't we just name it env
instead?
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 called it env0 because it is not the "real" env, it is an initial environment that will be recreated. In the bootstrap method, we have env which is created using settings from env0. But I'm happy to change it to env here in this intermediate layer if you feel strongly.
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.
nah I was just curious maybe a better name would be initialEnv
?
I dug into the JDK source for this and I have a workaround. There is no OID for those PBE with HMAC sha2 algorithms, so it fails there. However, there is special handling elsewhere in the PKCS12KeyStore class for these algorithms when they are used for the actual encryption and decryption of the secret value. My workaround is to use --- a/core/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java
+++ b/core/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java
@@ -100,11 +100,12 @@ public class KeyStoreWrapper implements Closeable {
/** Constructs a new keystore with the given password. */
static KeyStoreWrapper create(char[] password) throws Exception {
- KeyStoreWrapper wrapper = new KeyStoreWrapper(password.length != 0, NEW_KEYSTORE_TYPE, NEW_KEYSTORE_SECRET_KEY_ALGO, null);
+ // PBE is ok here... we just use it to generate a secret key
+ KeyStoreWrapper wrapper = new KeyStoreWrapper(password.length != 0, NEW_KEYSTORE_TYPE, "PBE", null);
KeyStore keyStore = KeyStore.getInstance(NEW_KEYSTORE_TYPE);
keyStore.load(null, null);
wrapper.keystore.set(keyStore);
- wrapper.keystorePassword.set(new KeyStore.PasswordProtection(password));
+ wrapper.keystorePassword.set(new KeyStore.PasswordProtection(password, NEW_KEYSTORE_SECRET_KEY_ALGO, null));
return wrapper;
}
@@ -147,7 +148,7 @@ public class KeyStoreWrapper implements Closeable {
keystore.get().load(in, password);
}
- this.keystorePassword.set(new KeyStore.PasswordProtection(password));
+ this.keystorePassword.set(new KeyStore.PasswordProtection(password, NEW_KEYSTORE_SECRET_KEY_ALGO, null));
Arrays.fill(password, '\0');
// convert keystore aliases enum into a set for easy lookup Digging into the jdk pkcs12 code didn't leave me with a good feeling, but it is the best option we have for a keystore out of the box... |
@jaymode I'm not sure what advantage setting the algo on PasswordProtection provides, given that it is only in memory? The issue with pkcs12 and explicit pbe is supposed to be fixed in java 9: |
The password protection algorithm is used in conjunction with the password to encrypt the bytes of the secretkey both in memory and in the bytes stored on disk. |
@rjernst there is another issue with the use of a PBE key factory. The use of non ascii characters results in:
I'd expect that we should have the ability to store UTF-8 characters; my test used a character with an umlaut. |
@s1monw @jaymode I pushed more changes. The wrapper now uses IndexInput/Output with a header and footer. It also reads the entire wrapped keystore into a byte array, which is then loaded later (I also renamed the methods to make a little more sense, Jay, I tried setting the algorithm as you described, on PasswordProtection, but it did not work for me (all of the unit tests which add a string value failed, with |
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.
left some minor suggestions
if (format != FORMAT_VERSION) { | ||
throw new IllegalStateException("Unknown keystore metadata format [" + format + "]"); | ||
|
||
NIOFSDirectory directory = new NIOFSDirectory(configDir); |
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.
maybe just use SimpleFsDir here?
keystore.get().store(outputStream, password); | ||
char[] password = this.keystorePassword.get().getPassword(); | ||
|
||
NIOFSDirectory directory = new NIOFSDirectory(configDir); |
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.
same here just use simple fs?
} | ||
|
||
Path keystoreFile = keystorePath(configDir); | ||
Files.move(configDir.resolve(tmpFile), keystoreFile, StandardCopyOption.REPLACE_EXISTING); |
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.
do you want to use atomic move here too?
I agree that we should iterate on this outside of this PR and everything else still LGTM |
This change is the first towards providing the ability to store sensitive settings in elasticsearch. It adds the elasticsearch-keystore tool, which allows managing a java keystore. The keystore is loaded upon node startup in Elasticsearch, and used by the Setting infrastructure when a setting is configured as secure. There are a lot of caveats to this PR. The most important is it only provides the tool and setting infrastructure for secure strings. It does not yet provide for keystore passwords, keypairs, certificates, or even convert any existing string settings to secure string settings. Those will all come in follow up PRs. But this PR was already too big, so this at least gets a basic version of the infrastructure in. The two main things to look at. The first is the SecureSetting class, which extends Setting, but removes the assumption for the raw value of the setting to be a string. SecureSetting provides, for now, a single helper, stringSetting() to create a SecureSetting which will return a SecureString (which is like String, but is closeable, so that the underlying character array can be cleared). The second is the KeyStoreWrapper class, which wraps the java KeyStore to provide a simpler api (we do not need the entire keystore api) and also extend the serialized format to add metadata needed for loading the keystore with no assumptions about keystore type (so that we can change this in the future) as well as whether the keystore has a password (so that we can know whether prompting is necessary when we add support for keystore passwords).
PosixFileAttributeView attrs = Files.getFileAttributeView(keystoreFile, PosixFileAttributeView.class); | ||
if (attrs != null) { | ||
// don't rely on umask: ensure the keystore has minimal permissions | ||
attrs.setPermissions(PosixFilePermissions.fromString("rw-------")); |
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
This change is the first towards providing the ability to store
sensitive settings in elasticsearch. It adds the
elasticsearch-keystore
tool, which allows managing a java keystore.The keystore is loaded upon node startup in Elasticsearch, and used by
the Setting infrastructure when a setting is configured as secure.
There are a lot of caveats to this PR. The most important is it only
provides the tool and setting infrastructure for secure strings. It does
not yet provide for keystore passwords, keypairs, certificates, or even
convert any existing string settings to secure string settings. Those
will all come in follow up PRs. But this PR was already too big, so this
at least gets a basic version of the infrastructure in.
The two main things to look at. The first is the
SecureSetting
class,which extends
Setting
, but removes the assumption for the raw value of thesetting to be a string. SecureSetting provides, for now, a single
helper,
stringSetting()
to create a SecureSetting which will return aSecureString (which is like String, but is closeable, so that the
underlying character array can be cleared). The second is the
KeyStoreWrapper
class, which wraps the javaKeyStore
to provide asimpler api (we do not need the entire keystore api) and also extend
the serialized format to add metadata needed for loading the keystore
with no assumptions about keystore type (so that we can change this in
the future) as well as whether the keystore has a password (so that we
can know whether prompting is necessary when we add support for keystore
passwords).