Skip to content

Commit

Permalink
Add new settings plugins.security.ssl.http.enforce_cert_reload_dn_ver…
Browse files Browse the repository at this point in the history
…ification and plugins.security.ssl.transport.enforce_cert_reload_dn_verification, to control whether DN validation should be performed when hot reloading certificates

Signed-off-by: Paris Larkins <[email protected]>
  • Loading branch information
parislarkins committed Sep 23, 2024
1 parent 4f2e689 commit 9f90129
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ private void printJCEWarnings() {
public final SslProvider sslTransportServerProvider;
public final SslProvider sslTransportClientProvider;
private final boolean httpSSLEnabled;
private final boolean httpSSLEnforceCertReloadDnVerification;
private final boolean transportSSLEnabled;
private final boolean transportSSLEnforceCertReloadDnVerification;

private ArrayList<String> enabledHttpCiphersJDKProvider;
private ArrayList<String> enabledHttpCiphersOpenSSLProvider;
Expand Down Expand Up @@ -165,10 +167,18 @@ public DefaultSecurityKeyStore(final Settings settings, final Path configPath) {
SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED,
SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_DEFAULT
);
httpSSLEnforceCertReloadDnVerification = settings.getAsBoolean(
SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION,
SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION_DEFAULT
);
transportSSLEnabled = settings.getAsBoolean(
SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED,
SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_DEFAULT
);
transportSSLEnforceCertReloadDnVerification = settings.getAsBoolean(
SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION,
SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION_DEFAULT
);
final boolean useOpenSSLForHttpIfAvailable = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED
&& settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLE_OPENSSL_IF_AVAILABLE, true);
final boolean useOpenSSLForTransportIfAvailable = OpenSearchSecuritySSLPlugin.OPENSSL_SUPPORTED
Expand Down Expand Up @@ -421,7 +431,7 @@ public void initTransportSSLConfig() {
certFromTruststore = new CertFromTruststore(truststoreProps, truststoreAlias);
}

validateNewCerts(transportCerts, certFromKeystore.getCerts());
validateNewCerts(transportCerts, certFromKeystore.getCerts(), transportSSLEnforceCertReloadDnVerification);
transportServerSslContext = buildSSLServerContext(
certFromKeystore.getServerKey(),
certFromKeystore.getServerCert(),
Expand Down Expand Up @@ -472,7 +482,7 @@ public void initTransportSSLConfig() {
certFromFile = new CertFromFile(certProps);
}

validateNewCerts(transportCerts, certFromFile.getCerts());
validateNewCerts(transportCerts, certFromFile.getCerts(), transportSSLEnforceCertReloadDnVerification);
transportServerSslContext = buildSSLServerContext(
certFromFile.getServerPemKey(),
certFromFile.getServerPemCert(),
Expand Down Expand Up @@ -570,7 +580,7 @@ public void initHttpSSLConfig() {
certFromTruststore = new CertFromTruststore(truststoreProps, truststoreAlias);
}

validateNewCerts(httpCerts, certFromKeystore.getCerts());
validateNewCerts(httpCerts, certFromKeystore.getCerts(), httpSSLEnforceCertReloadDnVerification);
httpSslContext = buildSSLServerContext(
certFromKeystore.getServerKey(),
certFromKeystore.getServerCert(),
Expand Down Expand Up @@ -601,7 +611,7 @@ public void initHttpSSLConfig() {
);
CertFromFile certFromFile = new CertFromFile(certFileProps);

validateNewCerts(httpCerts, certFromFile.getCerts());
validateNewCerts(httpCerts, certFromFile.getCerts(), httpSSLEnforceCertReloadDnVerification);
httpSslContext = buildSSLServerContext(
certFromFile.getServerPemKey(),
certFromFile.getServerPemCert(),
Expand Down Expand Up @@ -632,11 +642,13 @@ public void initHttpSSLConfig() {
* If the current and new certificates are same, skip remaining checks.
* For new X509 cert to be valid Issuer, Subject DN must be the same and
* new certificates should expire after current ones.
* @param currentX509Certs Array of current x509 certificates
* @param newX509Certs Array of x509 certificates which will replace our current cert
*
* @param currentX509Certs Array of current x509 certificates
* @param newX509Certs Array of x509 certificates which will replace our current cert
* @param verifyValidDNs Whether to verify that new certs have valid IssuerDN, SubjectDN and SAN
* @throws Exception if certificate is invalid
*/
private void validateNewCerts(final X509Certificate[] currentX509Certs, final X509Certificate[] newX509Certs) throws Exception {
private void validateNewCerts(final X509Certificate[] currentX509Certs, final X509Certificate[] newX509Certs, final boolean verifyValidDNs) throws Exception {

// First time we init certs ignore validity check
if (currentX509Certs == null) {
Expand All @@ -653,7 +665,7 @@ private void validateNewCerts(final X509Certificate[] currentX509Certs, final X5
}

// Check if new X509 certs have valid IssuerDN, SubjectDN or SAN
if (!hasValidDNs(currentX509Certs, newX509Certs)) {
if (verifyValidDNs && !hasValidDNs(currentX509Certs, newX509Certs)) {
throw new Exception("New Certs do not have valid Issuer DN, Subject DN or SAN.");
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,23 @@ public List<Setting<?>> getSettings() {
Setting.longSetting(SSLConfigConstants.SECURITY_SSL_HTTP_CRL_VALIDATION_DATE, -1, -1, Property.NodeScope, Property.Filtered)
);

settings.add(
Setting.boolSetting(
SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION,
SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION_DEFAULT,
Property.NodeScope,
Property.Filtered
)
);
settings.add(
Setting.boolSetting(
SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION,
SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION_DEFAULT,
Property.NodeScope,
Property.Filtered
)
);

return settings;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public final class SSLConfigConstants {
public static final String SECURITY_SSL_HTTP_TRUSTSTORE_ALIAS = "plugins.security.ssl.http.truststore_alias";
public static final String SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH = "plugins.security.ssl.http.truststore_filepath";
public static final String SECURITY_SSL_HTTP_TRUSTSTORE_TYPE = "plugins.security.ssl.http.truststore_type";
public static final String SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION = "plugins.security.ssl.http.enforce_cert_reload_dn_verification";
public static final Boolean SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION_DEFAULT = true;
public static final String SECURITY_SSL_TRANSPORT_ENABLE_OPENSSL_IF_AVAILABLE =
"plugins.security.ssl.transport.enable_openssl_if_available";
public static final String SECURITY_SSL_TRANSPORT_ENABLED = "plugins.security.ssl.transport.enabled";
Expand All @@ -46,7 +48,8 @@ public final class SSLConfigConstants {
"plugins.security.ssl.transport.enforce_hostname_verification";
public static final String SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME =
"plugins.security.ssl.transport.resolve_hostname";

public static final String SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION = "plugins.security.ssl.transport.enforce_cert_reload_dn_verification";
public static final Boolean SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION_DEFAULT = true;
public static final String SECURITY_SSL_TRANSPORT_KEYSTORE_ALIAS = "plugins.security.ssl.transport.keystore_alias";
public static final String SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_ALIAS = "plugins.security.ssl.transport.server.keystore_alias";
public static final String SECURITY_SSL_TRANSPORT_CLIENT_KEYSTORE_ALIAS = "plugins.security.ssl.transport.client.keystore_alias";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,21 @@ public class SecuritySSLReloadCertsActionTests extends SingleClusterTest {
)
);

private final List<Map<String, String>> NEW_CA_NODE_CERT_DETAILS = List.of(
Map.of(
"issuer_dn",
"CN=Example Com Inc. Secondary Signing CA,OU=Example Com Inc. Secondary Signing CA,O=Example Com Inc.,DC=example,DC=com",
"subject_dn",
"CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE",
"san",
"[[2, localhost], [2, node-1.example.com], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]",
"not_before",
"2024-09-17T00:15:48Z",
"not_after",
"2034-09-15T00:15:48Z"
)
);

private String pemCertFilePath;
private String pemKeyFilePath;
private final String defaultCertFilePath = "ssl/reload/node.crt.pem";
Expand Down Expand Up @@ -186,6 +201,93 @@ public void testReloadHttpSSLSameCertsPass() throws Exception {
assertReloadCertificateSuccess(rh, "http", getInitCertDetailsExpectedResponse());
}


@Test
public void testReloadHttpCertDifferentTrustChain_skipDnValidationPass() throws Exception {
updateFiles(defaultCertFilePath, pemCertFilePath);
updateFiles(defaultKeyFilePath, pemKeyFilePath);
initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, false, true);

RestHelper rh = getRestHelperAdminUser();
// Change http certs to one signed by a different CA than the previous one
updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath);
updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath);

RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_HTTP_CERTS_ENDPOINT, null);

assertThat(reloadCertsResponse.getStatusCode(), is(200));
final var expectedJsonResponse = DefaultObjectMapper.objectMapper.createObjectNode();
expectedJsonResponse.put("message", "updated http certs");
assertThat(reloadCertsResponse.getBody(), is(expectedJsonResponse.toString()));

String certDetailsResponse = rh.executeSimpleRequest(GET_CERT_DETAILS_ENDPOINT);
assertThat(DefaultObjectMapper.readTree(certDetailsResponse), is(getNewCertAuthorityUpdatedCertDetailsExpectedResponse("http")));
}

@Test
public void testReloadHttpCertDifferentTrustChain_noSkipDnValidationFail() throws Exception {
updateFiles(defaultCertFilePath, pemCertFilePath);
updateFiles(defaultKeyFilePath, pemKeyFilePath);
initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true);

RestHelper rh = getRestHelperAdminUser();
// Change http certs to one signed by a different CA than the previous one
updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath);
updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath);

RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_HTTP_CERTS_ENDPOINT, null);

assertThat(reloadCertsResponse.getStatusCode(), is(500));
assertThat(
DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(),
is("OpenSearchSecurityException[Error while initializing http SSL layer from PEM: java.lang.Exception: "
+ "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];")
);
}

@Test
public void testReloadTransportCertDifferentTrustChain_skipDnValidationPass() throws Exception {
updateFiles(defaultCertFilePath, pemCertFilePath);
updateFiles(defaultKeyFilePath, pemKeyFilePath);
initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, false);

RestHelper rh = getRestHelperAdminUser();
// Change transport certs to one signed by a different CA than the previous one
updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath);
updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath);

RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null);

assertThat(reloadCertsResponse.getStatusCode(), is(200));
final var expectedJsonResponse = DefaultObjectMapper.objectMapper.createObjectNode();
expectedJsonResponse.put("message", "updated transport certs");
assertThat(reloadCertsResponse.getBody(), is(expectedJsonResponse.toString()));

String certDetailsResponse = rh.executeSimpleRequest(GET_CERT_DETAILS_ENDPOINT);
assertThat(DefaultObjectMapper.readTree(certDetailsResponse), is(getNewCertAuthorityUpdatedCertDetailsExpectedResponse("transport")));
}

@Test
public void testReloadTransportCertDifferentTrustChain_noSkipDnValidationFail() throws Exception {
updateFiles(defaultCertFilePath, pemCertFilePath);
updateFiles(defaultKeyFilePath, pemKeyFilePath);
initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true);

RestHelper rh = getRestHelperAdminUser();
// Change transport certs to one signed by a different CA than the previous one
updateFiles("ssl/reload/node-new-ca.crt.pem", pemCertFilePath);
updateFiles("ssl/reload/node-new-ca.key.pem", pemKeyFilePath);

RestHelper.HttpResponse reloadCertsResponse = rh.executePutRequest(RELOAD_TRANSPORT_CERTS_ENDPOINT, null);

assertThat(reloadCertsResponse.getStatusCode(), is(500));
assertThat(
DefaultObjectMapper.readTree(reloadCertsResponse.getBody()).get("error").get("root_cause").get(0).get("reason").asText(),
is("OpenSearchSecurityException[Error while initializing transport SSL layer from PEM: java.lang.Exception: "
+ "New Certs do not have valid Issuer DN, Subject DN or SAN.]; nested: Exception[New Certs do not have valid Issuer DN, Subject DN or SAN.];")
);
}

/**
*
* @param rh RestHelper to perform rest actions on the cluster
Expand All @@ -211,6 +313,15 @@ private void updateFiles(String srcFile, String dstFile) {
FileHelper.copyFileContents(FileHelper.getAbsoluteFilePathFromClassPath(srcFile).toString(), dstFile);
}

private JsonNode getNewCertAuthorityUpdatedCertDetailsExpectedResponse(String updateChannel) {
String updateKey = (Objects.equals(updateChannel, "http")) ? HTTP_CERTIFICATES_LIST_KEY : TRANSPORT_CERTIFICATES_LIST_KEY;
String oldKey = (Objects.equals(updateChannel, "http")) ? TRANSPORT_CERTIFICATES_LIST_KEY : HTTP_CERTIFICATES_LIST_KEY;
final var updatedCertDetailsResponse = DefaultObjectMapper.objectMapper.createObjectNode();
updatedCertDetailsResponse.set(updateKey, buildCertsInfoNode(NEW_CA_NODE_CERT_DETAILS));
updatedCertDetailsResponse.set(oldKey, buildCertsInfoNode(NODE_CERT_DETAILS));
return updatedCertDetailsResponse;
}

private JsonNode getUpdatedCertDetailsExpectedResponse(String updateChannel) {
String updateKey = (Objects.equals(updateChannel, "http")) ? HTTP_CERTIFICATES_LIST_KEY : TRANSPORT_CERTIFICATES_LIST_KEY;
String oldKey = (Objects.equals(updateChannel, "http")) ? TRANSPORT_CERTIFICATES_LIST_KEY : HTTP_CERTIFICATES_LIST_KEY;
Expand Down Expand Up @@ -270,31 +381,37 @@ private RestHelper getRestHelperNonAdminUser() {
private void initClusterWithTestCerts() throws Exception {
updateFiles(defaultCertFilePath, pemCertFilePath);
updateFiles(defaultKeyFilePath, pemKeyFilePath);
initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true);
initTestCluster(pemCertFilePath, pemKeyFilePath, pemCertFilePath, pemKeyFilePath, true, true, true);
}

/**
* Helper method to initialize test cluster for SSL Certificate Reload Tests
* @param transportPemCertFilePath Absolute Path to transport pem cert file
* @param transportPemKeyFilePath Absolute Path to transport pem key file
* @param httpPemCertFilePath Absolute Path to transport pem cert file
* @param httpPemKeyFilePath Absolute Path to transport pem key file
* @param sslCertReload Sets the ssl cert reload flag
*
* @param transportPemCertFilePath Absolute Path to transport pem cert file
* @param transportPemKeyFilePath Absolute Path to transport pem key file
* @param httpPemCertFilePath Absolute Path to transport pem cert file
* @param httpPemKeyFilePath Absolute Path to transport pem key file
* @param sslCertReload Sets the ssl cert reload flag
* @param httpEnforceReloadDnVerification
*/
private void initTestCluster(
final String transportPemCertFilePath,
final String transportPemKeyFilePath,
final String httpPemCertFilePath,
final String httpPemKeyFilePath,
final boolean sslCertReload
final String transportPemCertFilePath,
final String transportPemKeyFilePath,
final String httpPemCertFilePath,
final String httpPemKeyFilePath,
final boolean sslCertReload,
final boolean httpEnforceReloadDnVerification,
final boolean transportEnforceReloadDnVerification
) throws Exception {
final Settings settings = Settings.builder()
.putList(ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, "CN=kirk,OU=client,O=client,L=Test,C=DE")
.putList(ConfigConstants.SECURITY_NODES_DN, "CN=node-1.example.com,OU=SSL,O=Test,L=Test,C=DE")
.put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, true)
.put(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true)
.put(SSLConfigConstants.SECURITY_SSL_HTTP_ENFORCE_CERT_RELOAD_DN_VERIFICATION, httpEnforceReloadDnVerification)
.put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, false)
.put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, false)
.put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_CERT_RELOAD_DN_VERIFICATION, transportEnforceReloadDnVerification)
.put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, transportPemCertFilePath)
.put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, transportPemKeyFilePath)
.put(
Expand Down
Loading

0 comments on commit 9f90129

Please sign in to comment.