Skip to content

Conversation

@jfreden
Copy link
Contributor

@jfreden jfreden commented Sep 17, 2025

This PR adds trust configuration for cross-cluster API keys.

The actual verification of certificate dn to cross cluster api keys will be added in subsequent PRs.

The new configurations are:

cluster.remote.signing.truststore.path
cluster.remote.signing.truststore.secure_password
cluster.remote.signing.truststore.algorithm
cluster.remote.signing.truststore.type
cluster.remote.signing.certificate_authorities
  • All new cluster settings can be dynamically updated through the cluster settings API
  • All new secure settings can be reloaded through the reload_secure_settings API
  • All files pointed to by the new settings are hot reloadable and the trust config will automatically be updated when the files change

@jfreden jfreden force-pushed the rcs/conf_trust_anchors branch from 4615266 to c8388c4 Compare September 19, 2025 07:24
@jfreden jfreden added >enhancement :Security/Security Security issues without another label test-fips Trigger CI checks for FIPS labels Sep 19, 2025
@jfreden jfreden marked this pull request as ready for review September 19, 2025 07:32
@elasticsearchmachine elasticsearchmachine added the Team:Security Meta label for security team label Sep 19, 2025
@elasticsearchmachine
Copy link
Collaborator

Pinging @elastic/es-security (Team:Security)

@elasticsearchmachine
Copy link
Collaborator

Hi @jfreden, I've created a changelog YAML for you.

logger.trace("Loading trust config with settings [{}]", settings);
try {
// Only load a trust manager if trust is configured to avoid using key store as trust store
if (settingsHaveTrustConfig(settings)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would move this check into SslConfiguration - it seems like something useful to push up.

That is,

var sslConfig = loadSslConfig(environment, settings);
if (sslConfig.hasExplicitTrustConfig()) {
 ....
}

It like that this PR reduces the amount of custom certificate handling we have. If there are opportunities to push more SSL/certificate related things up that's generally going to be good.

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 moved the hasExplicitConfig check into SslTrustConfig. While doing that I realized that we might want to allow the default jdk trust store as a valid config. Do you agree with that? So if you configure nothing, that's what you'll get.

The thing I was trying to protect against with that check was that the customer doesn't accidentally trust the key store setup for signing, since it doesn't feel like I would expect that to happen.

var trustConfig = sslConfig.trustConfig();
final X509ExtendedTrustManager newTrustManager = trustConfig.createTrustManager();
if (newTrustManager.getAcceptedIssuers().length == 0) {
logger.warn("Cross cluster API Key trust configuration [{}] has no accepted certificate issuers", this, trustConfig);
Copy link
Contributor

Choose a reason for hiding this comment

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

This has one {} but two args.

Suggested change
logger.warn("Cross cluster API Key trust configuration [{}] has no accepted certificate issuers", this, trustConfig);
logger.warn("Cross cluster API Key trust configuration [{}] has no accepted certificate issuers", trustConfig);

}
}

public Collection<Path> getDependentFiles() {
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be worth being explicit about which method handles trust and which handles signing.

Suggested change
public Collection<Path> getDependentFiles() {
public Collection<Path> getDependentTrustFiles() {

or some other name of your choosing.

return sslConfig == null ? Collections.emptyList() : sslConfig.getDependentFiles();
}

public Collection<Path> getDependentFiles(String clusterAlias) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public Collection<Path> getDependentFiles(String clusterAlias) {
public Collection<Path> getDependentSigningFiles(String clusterAlias) {

var authTrustManager = trustManager.get();
if (authTrustManager == null) {
logger.warn("No trust manager found");
throw new IllegalStateException("No trust manager found");
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a plausible scenario if someone updates the cluster settings to remove the trust config, right?

In that case I think these messages need to be a bit more explicit about what's happened.

Something like

Cannot verify signed cross-cluster headers because [cluster.remote.signing] has not trust configuration


try {
// Make sure the provided certificate chain is trusted
authTrustManager.checkClientTrusted(signature.certificates(), signature.certificates()[0].getPublicKey().getAlgorithm());
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's worth a trace log here

Suggested change
authTrustManager.checkClientTrusted(signature.certificates(), signature.certificates()[0].getPublicKey().getAlgorithm());
var leaf = signature.certificates()[0];
if (logger.isTraceEnabled()) {
logger.trace("checking signing chain (len={}) [{}] with leaf subject [{}] using algorithm [{}]",
signature.certificates().length,
Arrays.stream(signature.certificates()).map(CrossClusterApiKeySignatureManager::calculateFingerprint).collect(Collectors.joining(",")),
leaf.getSubjectX500Principal().getName(X500Principal.RFC2253),
leaf.getPublicKey().getAlgorithm
);
}
authTrustManager.checkClientTrusted(signature.certificates(),left.getPublicKey().getAlgorithm());

// Make sure the provided certificate chain is trusted
authTrustManager.checkClientTrusted(signature.certificates(), signature.certificates()[0].getPublicKey().getAlgorithm());
// TODO Make sure the signing certificate belongs to the correct DN (the configured api key cert identity)
// TODO Make sure the signing certificate is valid
Copy link
Contributor

Choose a reason for hiding this comment

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

I can never remember how much the trust manager does for you here. I think it does more than you'd expect from the name, but maybe not everything we need.

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 left the todo very broad to make sure I verify everything when we do the "certificate belongs to the correct DN" check.

CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
final Certificate cert = certFactory.generateCertificate(bais);
if (cert instanceof X509Certificate x509) {
certificate = x509;
Copy link
Contributor

Choose a reason for hiding this comment

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

This could just be a return (rather than needing a local variable).

+ fingerprint()
+ "), "
+ "certificates="
+ Arrays.toString(Arrays.stream(certificateChain).map(this::certificateToString).toArray(String[]::new))
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it better to use Collectors.joining(",") rather than converting to an array here?


var manager = new CrossClusterApiKeySignatureManager(TestEnvironment.newEnvironment(builder.build()));
var signature = manager.signerForClusterAlias("my_remote").sign("a_header");
assertFalse(manager.verifier().verify(signature, "another_header"));
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we also have tests for:

  1. Signed with a different key
  2. Manipulate signature string

?

@jfreden jfreden merged commit 778bab9 into elastic:main Sep 26, 2025
40 checks passed
szybia added a commit to szybia/elasticsearch that referenced this pull request Sep 26, 2025
…-dls

* upstream/main: (55 commits)
  Mute org.elasticsearch.upgrades.MatchOnlyTextRollingUpgradeIT testIndexing {upgradedNodes=1} elastic#135525
  Address es819 tsdb doc values format performance bug (elastic#135505)
  Remove obsolete --add-opens from JDK API extractor tool (elastic#135445)
  [CI] Fix MergeWithFailureIT (elastic#135447)
  Increase wait time in AdaptiveAllocationsScalerServiceTests (elastic#135510)
  ES|QL: Handle multi values in FUSE (elastic#135448)
  Mute org.elasticsearch.upgrades.SyntheticSourceRollingUpgradeIT testIndexing {upgradedNodes=1} elastic#135512
  Add trust configuration for cross cluster api keys (elastic#134893)
  ESQL: Fix flakiness in SessionUtilsTests (elastic#135375)
  Mute org.elasticsearch.upgrades.LogsdbIndexingRollingUpgradeIT testIndexing {upgradedNodes=1} elastic#135511
  Mute org.elasticsearch.upgrades.MatchOnlyTextRollingUpgradeIT testIndexing {upgradedNodes=2} elastic#135325
  Require all functions to provide examples (elastic#135094)
  Mute org.elasticsearch.upgrades.SyntheticSourceRollingUpgradeIT testIndexing {upgradedNodes=2} elastic#135344
  Mute org.elasticsearch.upgrades.TextRollingUpgradeIT testIndexing {upgradedNodes=1} elastic#135236
  Mute org.elasticsearch.upgrades.TextRollingUpgradeIT testIndexing {upgradedNodes=2} elastic#135238
  Mute org.elasticsearch.upgrades.LogsdbIndexingRollingUpgradeIT testIndexing {upgradedNodes=2} elastic#135327
  Mute org.elasticsearch.upgrades.MatchOnlyTextRollingUpgradeIT testIndexing {upgradedNodes=3} elastic#135324
  Mute org.elasticsearch.upgrades.StandardToLogsDbIndexModeRollingUpgradeIT testLogsIndexing {upgradedNodes=3} elastic#135315
  ESQL: Handle right hand side of Inline Stats coming optimized with LocalRelation shortcut (elastic#135011)
  Mute org.elasticsearch.upgrades.TextRollingUpgradeIT testIndexing {upgradedNodes=3} elastic#135237
  ...
elasticsearchmachine pushed a commit that referenced this pull request Oct 10, 2025
This PR is a follow up to
#134137 and
#134893. It adds
serialization and verification of the new header: 

- The signing configurations are now used to generate a signature that's passed as a header for cross cluster api keys.
- The signature headers are deserializaed and validated on the server side and auth fails if the validation fails. 

This PR does not use the certificate identity that was added in
#134604 to verify that the
identity in the passed leaf certificate belongs to the signed cross
cluster API key by matching it against the API key certificate identity
pattern. That will be done in a follow up PR to keep the scope of this
PR manageable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

>enhancement :Security/Security Security issues without another label Team:Security Meta label for security team test-fips Trigger CI checks for FIPS v9.2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants