Skip to content

Commit c22002a

Browse files
Add support for certificates hot reload (#4880)
Signed-off-by: Andrey Pleskach <ples@aiven.io>
1 parent a8447cc commit c22002a

File tree

8 files changed

+362
-11
lines changed

8 files changed

+362
-11
lines changed

src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,10 @@
223223
import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting;
224224
import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX;
225225
import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE;
226+
import static org.opensearch.security.support.ConfigConstants.SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED;
227+
import static org.opensearch.security.support.ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED;
226228
import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION;
229+
227230
// CS-ENFORCE-SINGLE
228231

229232
public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
@@ -313,7 +316,11 @@ private static boolean useClusterStateToInitSecurityConfig(final Settings settin
313316
* @return true if ssl cert reload is enabled else false
314317
*/
315318
private static boolean isSslCertReloadEnabled(final Settings settings) {
316-
return settings.getAsBoolean(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, false);
319+
return settings.getAsBoolean(SECURITY_SSL_CERT_RELOAD_ENABLED, false);
320+
}
321+
322+
private boolean sslCertificatesHotReloadEnabled(final Settings settings) {
323+
return settings.getAsBoolean(SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED, false);
317324
}
318325

319326
@SuppressWarnings("removal")
@@ -1203,6 +1210,19 @@ public Collection<Object> createComponents(
12031210
components.add(passwordHasher);
12041211

12051212
components.add(sslSettingsManager);
1213+
if (isSslCertReloadEnabled(settings) && sslCertificatesHotReloadEnabled(settings)) {
1214+
throw new OpenSearchException(
1215+
"Either "
1216+
+ SECURITY_SSL_CERT_RELOAD_ENABLED
1217+
+ " or "
1218+
+ SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED
1219+
+ " can be set to true, but not both."
1220+
);
1221+
}
1222+
1223+
if (sslCertificatesHotReloadEnabled(settings) && !isSslCertReloadEnabled(settings)) {
1224+
sslSettingsManager.addSslConfigurationsChangeListener(resourceWatcherService);
1225+
}
12061226

12071227
final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false);
12081228
final var useClusterState = useClusterStateToInitSecurityConfig(settings);

src/main/java/org/opensearch/security/ssl/SslContextHandler.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import java.util.stream.Stream;
2222
import javax.net.ssl.SSLEngine;
2323

24+
import org.apache.logging.log4j.LogManager;
25+
import org.apache.logging.log4j.Logger;
26+
2427
import org.opensearch.security.ssl.config.Certificate;
2528
import org.opensearch.transport.NettyAllocator;
2629

@@ -30,6 +33,8 @@
3033

3134
public class SslContextHandler {
3235

36+
private final static Logger LOGGER = LogManager.getLogger(SslContextHandler.class);
37+
3338
private SslContext sslContext;
3439

3540
private final SslConfiguration sslConfiguration;
@@ -78,7 +83,7 @@ Stream<Certificate> keyMaterialCertificates(final List<Certificate> certificates
7883
return certificates.stream().filter(Certificate::hasKey);
7984
}
8085

81-
void reloadSslContext() throws CertificateException {
86+
boolean reloadSslContext() throws CertificateException {
8287
final var newCertificates = sslConfiguration.certificates();
8388

8489
boolean hasChanges = false;
@@ -89,11 +94,13 @@ void reloadSslContext() throws CertificateException {
8994
final var newKeyMaterialCertificates = keyMaterialCertificates(newCertificates).collect(Collectors.toList());
9095

9196
if (notSameCertificates(loadedAuthorityCertificates, newAuthorityCertificates)) {
97+
LOGGER.debug("Certification authority has changed");
9298
hasChanges = true;
9399
validateDates(newAuthorityCertificates);
94100
}
95101

96102
if (notSameCertificates(loadedKeyMaterialCertificates, newKeyMaterialCertificates)) {
103+
LOGGER.debug("Key material and access certificate has changed");
97104
hasChanges = true;
98105
validateNewKeyMaterialCertificates(
99106
loadedKeyMaterialCertificates,
@@ -111,6 +118,7 @@ void reloadSslContext() throws CertificateException {
111118
loadedCertificates.clear();
112119
loadedCertificates.addAll(newCertificates);
113120
}
121+
return hasChanges;
114122
}
115123

116124
private boolean notSameCertificates(final List<Certificate> loadedCertificates, final List<Certificate> newCertificates) {

src/main/java/org/opensearch/security/ssl/SslSettingsManager.java

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@
1111

1212
package org.opensearch.security.ssl;
1313

14+
import java.io.IOException;
15+
import java.nio.file.Path;
1416
import java.security.NoSuchAlgorithmException;
1517
import java.security.cert.CertificateException;
1618
import java.util.Locale;
1719
import java.util.Map;
1820
import java.util.Optional;
21+
import java.util.Set;
22+
import java.util.stream.Collectors;
1923
import javax.crypto.Cipher;
2024

2125
import com.google.common.collect.ImmutableMap;
@@ -29,6 +33,9 @@
2933
import org.opensearch.security.ssl.config.CertType;
3034
import org.opensearch.security.ssl.config.SslCertificatesLoader;
3135
import org.opensearch.security.ssl.config.SslParameters;
36+
import org.opensearch.watcher.FileChangesListener;
37+
import org.opensearch.watcher.FileWatcher;
38+
import org.opensearch.watcher.ResourceWatcherService;
3239

3340
import io.netty.handler.ssl.ClientAuth;
3441
import io.netty.handler.ssl.OpenSsl;
@@ -108,13 +115,13 @@ private Map<CertType, SslContextHandler> buildSslContexts(final Environment envi
108115

109116
public synchronized void reloadSslContext(final CertType certType) {
110117
sslContextHandler(certType).ifPresentOrElse(sscContextHandler -> {
111-
LOGGER.info("Reloading {} SSL context", certType.name());
112118
try {
113-
sscContextHandler.reloadSslContext();
119+
if (sscContextHandler.reloadSslContext()) {
120+
LOGGER.info("{} SSL context reloaded", certType.name());
121+
}
114122
} catch (CertificateException e) {
115123
throw new OpenSearchException(e);
116124
}
117-
LOGGER.info("{} SSL context reloaded", certType.name());
118125
}, () -> LOGGER.error("Missing SSL Context for {}", certType.name()));
119126
}
120127

@@ -180,6 +187,50 @@ private Map<CertType, SslConfiguration> loadConfigurations(final Environment env
180187
return configurationBuilder.build();
181188
}
182189

190+
public void addSslConfigurationsChangeListener(final ResourceWatcherService resourceWatcherService) {
191+
for (final var directoryToMonitor : directoriesToMonitor()) {
192+
final var fileWatcher = new FileWatcher(directoryToMonitor);
193+
fileWatcher.addListener(new FileChangesListener() {
194+
@Override
195+
public void onFileCreated(final Path file) {
196+
onFileChanged(file);
197+
}
198+
199+
@Override
200+
public void onFileDeleted(final Path file) {
201+
onFileChanged(file);
202+
}
203+
204+
@Override
205+
public void onFileChanged(final Path file) {
206+
for (final var e : sslSettingsContexts.entrySet()) {
207+
final var certType = e.getKey();
208+
final var sslConfiguration = e.getValue().sslConfiguration();
209+
if (sslConfiguration.dependentFiles().contains(file)) {
210+
SslSettingsManager.this.reloadSslContext(certType);
211+
}
212+
}
213+
}
214+
});
215+
try {
216+
resourceWatcherService.add(fileWatcher, ResourceWatcherService.Frequency.HIGH);
217+
LOGGER.info("Added SSL configuration change listener for: {}", directoryToMonitor);
218+
} catch (IOException e) {
219+
// TODO: should we fail here, or are error logs sufficient?
220+
throw new OpenSearchException("Couldn't add SSL configurations change listener", e);
221+
}
222+
}
223+
}
224+
225+
private Set<Path> directoriesToMonitor() {
226+
return sslSettingsContexts.values()
227+
.stream()
228+
.map(SslContextHandler::sslConfiguration)
229+
.flatMap(c -> c.dependentFiles().stream())
230+
.map(Path::getParent)
231+
.collect(Collectors.toSet());
232+
}
233+
183234
private boolean clientNode(final Settings settings) {
184235
return !"node".equals(settings.get(OpenSearchSecuritySSLPlugin.CLIENT_TYPE));
185236
}

src/main/java/org/opensearch/security/support/ConfigConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ public class ConfigConstants {
287287
public static final String SECURITY_SSL_DUAL_MODE_SKIP_SECURITY = OPENDISTRO_SECURITY_CONFIG_PREFIX + "passive_security";
288288
public static final String LEGACY_OPENDISTRO_SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED = "opendistro_security_config.ssl_dual_mode_enabled";
289289
public static final String SECURITY_SSL_CERT_RELOAD_ENABLED = "plugins.security.ssl_cert_reload_enabled";
290+
public static final String SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED = "plugins.security.ssl.certificates_hot_reload.enabled";
290291
public static final String SECURITY_DISABLE_ENVVAR_REPLACEMENT = "plugins.security.disable_envvar_replacement";
291292
public static final String SECURITY_DFM_EMPTY_OVERRIDES_ALL = "plugins.security.dfm_empty_overrides_all";
292293

src/test/java/org/opensearch/security/ssl/CertificatesRule.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,28 @@ public class CertificatesRule extends ExternalResource {
6969

7070
private PrivateKey accessCertificatePrivateKey;
7171

72+
private final boolean generateDefaultCertificates;
73+
74+
public CertificatesRule() {
75+
this(true);
76+
}
77+
78+
public CertificatesRule(final boolean generateDefaultCertificates) {
79+
this.generateDefaultCertificates = generateDefaultCertificates;
80+
}
81+
7282
@Override
7383
protected void before() throws Throwable {
7484
super.before();
7585
temporaryFolder.create();
7686
configRootFolder = temporaryFolder.newFolder("esHome").toPath();
77-
final var keyPair = generateKeyPair();
78-
caCertificateHolder = generateCaCertificate(keyPair);
79-
final var keyAndCertificate = generateAccessCertificate(keyPair);
80-
accessCertificatePrivateKey = keyAndCertificate.v1();
81-
accessCertificateHolder = keyAndCertificate.v2();
87+
if (generateDefaultCertificates) {
88+
final var keyPair = generateKeyPair();
89+
caCertificateHolder = generateCaCertificate(keyPair);
90+
final var keyAndCertificate = generateAccessCertificate(keyPair);
91+
accessCertificatePrivateKey = keyAndCertificate.v1();
92+
accessCertificateHolder = keyAndCertificate.v2();
93+
}
8294
}
8395

8496
@Override

0 commit comments

Comments
 (0)