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

Keystore improvements. #17404

Closed
wants to merge 1 commit into from

Conversation

mashhurs
Copy link
Contributor

@mashhurs mashhurs commented Mar 25, 2025

Release notes

[rn:skip]

What does this PR do?

  • Require keystore.file and keystore.classname when generating a valid secure config (SecureConfig in SecretStoreExt).
  • Safely resolve keystore file and class name from the settings if available. If they both not available (explicitly set null), it might be that user intentionally turned off the keystore otherwise require both.
  • Send non-null file and class name that SecretStoreFactory#exists validates file existency and loads.

Why is it important/What is the impact to the user?

Improves user experience

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • [ ] I have made corresponding changes to the documentation
  • [ ] I have made corresponding change to the default configuration files (and/or docker env variables)
  • I have added tests that prove my fix is effective or that my feature works

Author's Checklist

  • Add unit tests

How to test this PR locally

  • CLI: always requires both keystore.file and keystore.classname
    • run bin/logstash-keystore create/add/list
  • LS run:
    • intentionally turn off keystore (set keystore.file and keystore.classname in logstash.yml)
    • setting keystore.file requires keystore.classname if keystore.classname is null, or vise-versa
    • add MY_VAR to keystore is available in pipeline configs with "${MY_VAR}"

Related issues

Use cases

Screenshots

Logs

…nfig, safely resolve keystore file and classname from the settings if available. If they both not available, it might be that user intentionally turned off the keystore otherwise require both.
Copy link

mergify bot commented Mar 25, 2025

This pull request does not have a backport label. Could you fix it @mashhurs? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-8./d is the label to automatically backport to the 8./d branch. /d is the digit.
  • backport-8.x is the label to automatically backport to the 8.x branch.
  • If no backport is necessary, please add the backport-skip label

@mashhurs mashhurs added backport-active-9 Automated backport with mergify to all the active 9.[0-9]+ branches backport-active-8 Automated backport with mergify to all the active 8.[0-9]+ branches labels Mar 25, 2025
Copy link

Quality Gate passed Quality Gate passed

Issues
0 New issues
0 Fixed issues
0 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
No data about Duplication

See analysis details on SonarQube

@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

Copy link
Contributor

@kaisecheng kaisecheng left a comment

Choose a reason for hiding this comment

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

Could you share more context on the purpose of this change? What specific problem does it address from the user's perspective?

keystore.file and keystore.classname are not user facing setting and are not listed in document. Both have assigned with default values. I am not sure how user will end up setting these value to empty string.

I am trying to understand how users interact with the setting and what other value can be set to keystore.classname other than the default. In order to load a custom class, the class needs to implement SecretStore. This is pretty advance use case. Does any user config this setting?

@mashhurs
Copy link
Contributor Author

mashhurs commented Apr 8, 2025

Could you share more context on the purpose of this change? What specific problem does it address from the user's perspective?

That is the reason why this PR is draft! :)
It requires discussion (the below point) if valid.

keystore.file and keystore.classname are not user facing setting and are not listed in document. Both have assigned with default values. I am not sure how user will end up setting these value to empty string.

If settings are not documented, does it mean LS is not expected to accept them through configs?
LS allows setting both keystore.file and keystore.classname in logstash.yml. Several questions,

  • in the fact they accept empty/null, and at the end ends with an exception;
  • or LS doesn't validate/ignore these setting to align internal expectations;
  • bigger than that, LS internally creates keystore.file in the config by default. Does this default file creating brings any security or any other concerns? This can be turned off with nullifying keystore.file and keystore.classname settings but doesn't get succeeded without this change.

I myself also not sure about the expectation. Let's get some input from @jsvd!

I am trying to understand how users interact with the setting and what other value can be set to keystore.classname other than the default. In order to load a custom class, the class needs to implement SecretStore. This is pretty advance use case. Does any user config this setting?

Copy link
Member

@yaauie yaauie left a comment

Choose a reason for hiding this comment

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

I'm unclear on the value in allowing a user to provide null for either keystore.file or keystore.classname; if a user doesn't want to use the keystore, they can simply invoke Logstash without having created a keystore.


In Logstash 8.17.4, setting either or both of these to null results in a TypeError that prevents pipelines from starting:

  • only keystore.file is null:

    printf "%s\n" 'keystore.file: null' > config/logstash.yml
    bin/logstash -e 'input { generator { count => 1} }'
    
    Click me
    [2025-04-08T19:53:25,171][ERROR][logstash.agent           ] Failed to execute action {:action=>LogStash::PipelineAction::Create/pipeline_id:main, :exception=>"TypeError", :message=>"nil is not a string", :backtrace=>["org/logstash/execution/AbstractPipelineExt.java:293:in `initialize'", "org/logstash/execution/AbstractPipelineExt.java:227:in `initialize'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/java_pipeline.rb:47:in `initialize'", "org/jruby/RubyClass.java:949:in `new'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/pipeline_action/create.rb:50:in `execute'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/agent.rb:420:in `block in converge_state'"]}
    
  • when only keystore.classname is null:

    printf "%s\n" 'keystore.classname: null' > config/logstash.yml
    bin/logstash -e 'input { generator { count => 1} }'
    
    Click me
    [2025-04-08T19:54:06,183][ERROR][logstash.agent           ] Failed to execute action {:action=>LogStash::PipelineAction::Create/pipeline_id:main, :exception=>"TypeError", :message=>"nil is not a string", :backtrace=>["org/logstash/execution/AbstractPipelineExt.java:293:in `initialize'", "org/logstash/execution/AbstractPipelineExt.java:227:in `initialize'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/java_pipeline.rb:47:in `initialize'", "org/jruby/RubyClass.java:949:in `new'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/pipeline_action/create.rb:50:in `execute'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/agent.rb:420:in `block in converge_state'"]}
    
  • when both are null:

    printf "%s\n" 'keystore.file: null' 'keystore.classname: null' > config/logstash.yml
    bin/logstash -e 'input { generator { count => 1} }'
    
    Click me
    [2025-04-08T19:55:20,126][ERROR][logstash.agent           ] Failed to execute action {:action=>LogStash::PipelineAction::Create/pipeline_id:main, :exception=>"TypeError", :message=>"nil is not a string", :backtrace=>["org/logstash/execution/AbstractPipelineExt.java:293:in `initialize'", "org/logstash/execution/AbstractPipelineExt.java:227:in `initialize'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/java_pipeline.rb:47:in `initialize'", "org/jruby/RubyClass.java:949:in `new'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/pipeline_action/create.rb:50:in `execute'", "/Users/rye/src/elastic/sdh-logstash/1634-elasticsearch-filter-missing/logstash-8.17.4/logstash-core/lib/logstash/agent.rb:420:in `block in converge_state'"]}
    

I believe this can be addressed more simply by just erroring helpfully if either setting is null. It appears that there was a regression in a recent settings refactor in which the supposedly-non-nullable SettingString incorrectly accepted null values. Fixing this in #17522 makes this PR a non-issue.

Comment on lines +44 to +46
if (keystoreFile == null || keystoreClassname == null) {
throw new IllegalArgumentException("`keystore.file` and `keystore.classname` cannot be null");
}
Copy link
Member

Choose a reason for hiding this comment

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

nit: while we know that the current path to these variables are the settings keystore.file and keystore.classname, I'm wary of making the exception reference those settings here -- we don't know the future uses of the secretstore.

Suggested change
if (keystoreFile == null || keystoreClassname == null) {
throw new IllegalArgumentException("`keystore.file` and `keystore.classname` cannot be null");
}
Objects.requireNonNull(keystoreFile, "keystoreFile");
Objects.requireNonNull(keystoreClassname, "keystoreClassname");

Comment on lines +848 to +855
if (keystoreFile == null && keystoreClassname == null) {
// explicitly set keystore and classname null
return null;
}

if (keystoreFile == null | keystoreClassname == null) {
throw new IllegalStateException("Setting `keystore.file` requires `keystore.classname`, or vice versa");
}
Copy link
Member

Choose a reason for hiding this comment

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

nit: the mix of bitwise logic and conditional logic works, but is surprising to me.

I would also rather have an else-if chain to provide specific error messages instead of grouping them up.

Suggested change
if (keystoreFile == null && keystoreClassname == null) {
// explicitly set keystore and classname null
return null;
}
if (keystoreFile == null | keystoreClassname == null) {
throw new IllegalStateException("Setting `keystore.file` requires `keystore.classname`, or vice versa");
}
if (keystoreFile == null && keystoreClassname == null) {
// explicitly set keystore and classname null
return null;
} else if (keystoreFile == null) {
throw new IllegalStateException("Setting `keystore.file` is required when `keystore.classname` is provided");
} else if (keystoreClassname == null) {
throw new IllegalStateException("Setting `keystore.classname` is required when `keystore.file` is provided");
}

But separately and possibly more importantly, both keystore.file and keystore.classname are registered settings, so #hasSetting() will always return true for either of them, and the existing code will have already thrown a TypeError if either of them are null.

@@ -842,15 +843,28 @@ protected final boolean hasSetting(final ThreadContext context, final String nam
}

protected SecretStore getSecretStore(final ThreadContext context) {
String keystoreFile = hasSetting(context, "keystore.file")
Copy link
Member

Choose a reason for hiding this comment

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

It would be an illegal state for hasSetting(context, "keystore.file") to return false -- it would mean that the setting hasn't been registered (not simply that a value was not provided by the user).

@@ -842,15 +843,28 @@ protected final boolean hasSetting(final ThreadContext context, final String nam
}

protected SecretStore getSecretStore(final ThreadContext context) {
String keystoreFile = hasSetting(context, "keystore.file")
? getSetting(context, "keystore.file").asJavaString()
Copy link
Member

Choose a reason for hiding this comment

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

When we send IRubyObject#asJavaString() to nil, we get a TypeError: nil is not a string. This effectively already safeguards against a nil/null value (although it could be done in a more clear way).

String keystoreClassname = hasSetting(context, "keystore.classname")
? getSetting(context, "keystore.classname").asJavaString()
: null;
return (keystoreFile != null && keystoreClassname != null)
Copy link
Member

Choose a reason for hiding this comment

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

Neither keystoreFile nor keystoreClassname can actually be null here.

@mashhurs
Copy link
Contributor Author

Addressed by #17522

@mashhurs mashhurs closed this Apr 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport-active-8 Automated backport with mergify to all the active 8.[0-9]+ branches backport-active-9 Automated backport with mergify to all the active 9.[0-9]+ branches enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants