Skip to content

Commit 07c6358

Browse files
iigoninbennygoerzigKarstenSchnitterKai Sternad
committed
for GCS plugin prioritize custom truststore. With fallback to default google.p12 in non-FIPS mode and an exception in FIPS mode when no truststore is set up.
Signed-off-by: Igonin <[email protected]> Co-authored-by: Benny Goerzig <[email protected]> Co-authored-by: Karsten Schnitter <[email protected]> Co-authored-by: Kai Sternad <[email protected]>
1 parent f8cd57b commit 07c6358

19 files changed

+537
-47
lines changed

plugins/repository-gcs/build.gradle

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import org.opensearch.gradle.MavenFilteringHack
1414
import org.opensearch.gradle.info.BuildParams
15+
import org.opensearch.gradle.info.FipsBuildParams
1516
import org.opensearch.gradle.test.InternalClusterTestPlugin
1617
import org.opensearch.gradle.test.RestIntegTestTask
1718
import org.opensearch.gradle.test.rest.YamlRestTestPlugin
@@ -351,6 +352,14 @@ processYamlRestTestResources {
351352
internalClusterTest {
352353
// this is tested explicitly in a separate test task
353354
exclude '**/GoogleCloudStorageThirdPartyTests.class'
355+
exclude '**/GoogleCloudStorageThirdPartyFipsTests.class'
356+
357+
// Conditionally exclude based on FIPS mode
358+
if (FipsBuildParams.isInFipsMode()) {
359+
exclude '**/GoogleCloudStorageBlobStoreRepositoryTests.class'
360+
} else {
361+
exclude '**/GoogleCloudStorageBlobStoreRepositoryFipsTests.class'
362+
}
354363
}
355364

356365
final Closure testClustersConfiguration = {
@@ -413,7 +422,11 @@ task gcsThirdPartyTest(type: Test) {
413422
SourceSet internalTestSourceSet = sourceSets.getByName(InternalClusterTestPlugin.SOURCE_SET_NAME)
414423
setTestClassesDirs(internalTestSourceSet.getOutput().getClassesDirs())
415424
setClasspath(internalTestSourceSet.getRuntimeClasspath())
416-
include '**/GoogleCloudStorageThirdPartyTests.class'
425+
if (FipsBuildParams.isInFipsMode()) {
426+
include '**/GoogleCloudStorageThirdPartyFipsTests.class'
427+
} else {
428+
include '**/GoogleCloudStorageThirdPartyTests.class'
429+
}
417430
systemProperty 'tests.security.manager', false
418431
systemProperty 'test.google.bucket', gcsBucket
419432
nonInputProperties.systemProperty 'test.google.base', gcsBasePath + "_third_party_tests_" + BuildParams.testSeed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.repositories.gcs;
10+
11+
import org.opensearch.common.settings.MockSecureSettings;
12+
import org.opensearch.common.settings.Settings;
13+
14+
import java.nio.file.Path;
15+
16+
import static org.opensearch.repositories.gcs.GoogleCloudStorageClientSettings.TRUSTSTORE_PASSWORD_SETTING;
17+
import static org.opensearch.repositories.gcs.GoogleCloudStorageClientSettings.TRUSTSTORE_PATH_SETTING;
18+
import static org.opensearch.repositories.gcs.GoogleCloudStorageClientSettings.TRUSTSTORE_TYPE_SETTING;
19+
20+
public class GoogleCloudStorageBlobStoreRepositoryFipsTests extends GoogleCloudStorageBlobStoreRepositoryTests {
21+
22+
private final Path truststorePath = getDataPath("/google.bcfks");
23+
24+
@Override
25+
protected void configureClientSettings(Settings.Builder settings) {
26+
settings.put(TRUSTSTORE_PATH_SETTING.getConcreteSettingForNamespace("test").getKey(), truststorePath.toString());
27+
settings.put(TRUSTSTORE_TYPE_SETTING.getConcreteSettingForNamespace("test").getKey(), "BCFKS");
28+
}
29+
30+
@Override
31+
protected void configureSecureSettings(MockSecureSettings secureSettings) {
32+
secureSettings.setString(TRUSTSTORE_PASSWORD_SETTING.getConcreteSettingForNamespace("test").getKey(), "notasecret");
33+
}
34+
}

plugins/repository-gcs/src/internalClusterTest/java/org/opensearch/repositories/gcs/GoogleCloudStorageBlobStoreRepositoryTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,34 @@ protected Settings nodeSettings(int nodeOrdinal) {
121121
settings.put(super.nodeSettings(nodeOrdinal));
122122
settings.put(ENDPOINT_SETTING.getConcreteSettingForNamespace("test").getKey(), httpServerUrl());
123123
settings.put(TOKEN_URI_SETTING.getConcreteSettingForNamespace("test").getKey(), httpServerUrl() + "/token");
124+
configureClientSettings(settings);
124125

125126
final MockSecureSettings secureSettings = new MockSecureSettings();
126127
final byte[] serviceAccount = TestUtils.createServiceAccount(random());
127128
secureSettings.setFile(CREDENTIALS_FILE_SETTING.getConcreteSettingForNamespace("test").getKey(), serviceAccount);
129+
configureSecureSettings(secureSettings);
128130
settings.setSecureSettings(secureSettings);
129131
return settings.build();
130132
}
131133

134+
/**
135+
* Hook method for subclasses to add additional client settings.
136+
*
137+
* @param settings the settings builder to add settings to
138+
*/
139+
protected void configureClientSettings(Settings.Builder settings) {
140+
// Default implementation: no additional settings
141+
}
142+
143+
/**
144+
* Hook method for subclasses to add additional secure settings.
145+
*
146+
* @param secureSettings the secure settings to add settings to
147+
*/
148+
protected void configureSecureSettings(MockSecureSettings secureSettings) {
149+
// Default implementation: no additional secure settings
150+
}
151+
132152
public void testDeleteSingleItem() {
133153
final String repoName = createRepository(randomName());
134154
final RepositoriesService repositoriesService = internalCluster().getClusterManagerNodeInstance(RepositoriesService.class);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.repositories.gcs;
10+
11+
import org.opensearch.common.settings.MockSecureSettings;
12+
import org.opensearch.common.settings.SecureSettings;
13+
import org.opensearch.common.settings.Settings;
14+
15+
import java.nio.file.Path;
16+
17+
public class GoogleCloudStorageThirdPartyFipsTests extends GoogleCloudStorageThirdPartyTests {
18+
19+
private final Path truststorePath = getDataPath("/google.bcfks");
20+
21+
@Override
22+
protected Settings nodeSettings() {
23+
Settings.Builder builder = Settings.builder().put(super.nodeSettings());
24+
builder.put("gcs.client.default.truststore.path", truststorePath.toString());
25+
builder.put("gcs.client.default.truststore.type", "BCFKS");
26+
return builder.build();
27+
}
28+
29+
@Override
30+
protected SecureSettings credentials() {
31+
MockSecureSettings secureSettings = (MockSecureSettings) super.credentials();
32+
secureSettings.setString("gcs.client.default.truststore.password", "notasecret");
33+
return secureSettings;
34+
}
35+
}

plugins/repository-gcs/src/main/java/org/opensearch/repositories/gcs/GoogleCloudStorageClientSettings.java

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,27 @@ public class GoogleCloudStorageClientSettings {
167167
() -> PROXY_USERNAME_SETTING
168168
);
169169

170+
/** The path to the truststore file for SSL/TLS connections */
171+
static final Setting.AffixSetting<String> TRUSTSTORE_PATH_SETTING = Setting.affixKeySetting(
172+
PREFIX,
173+
"truststore.path",
174+
key -> Setting.simpleString(key, Setting.Property.NodeScope)
175+
);
176+
177+
/** The secure password for the truststore */
178+
static final Setting.AffixSetting<SecureString> TRUSTSTORE_PASSWORD_SETTING = Setting.affixKeySetting(
179+
PREFIX,
180+
"truststore.password",
181+
key -> SecureSetting.secureString(key, null)
182+
);
183+
184+
/** The type of the truststore (e.g., BCFKS, PKCS11, PKCS12, JKS) */
185+
static final Setting.AffixSetting<String> TRUSTSTORE_TYPE_SETTING = Setting.affixKeySetting(
186+
PREFIX,
187+
"truststore.type",
188+
key -> Setting.simpleString(key, Setting.Property.NodeScope)
189+
);
190+
170191
/** The credentials used by the client to connect to the Storage endpoint. */
171192
private final ServiceAccountCredentials credential;
172193

@@ -191,6 +212,9 @@ public class GoogleCloudStorageClientSettings {
191212
/** The GCS SDK Proxy settings. */
192213
private final ProxySettings proxySettings;
193214

215+
/** The GCS SDK Truststore settings. */
216+
private final TruststoreSettings truststoreSettings;
217+
194218
GoogleCloudStorageClientSettings(
195219
final ServiceAccountCredentials credential,
196220
final String endpoint,
@@ -199,7 +223,8 @@ public class GoogleCloudStorageClientSettings {
199223
final TimeValue readTimeout,
200224
final String applicationName,
201225
final URI tokenUri,
202-
final ProxySettings proxySettings
226+
final ProxySettings proxySettings,
227+
final TruststoreSettings truststoreSettings
203228
) {
204229
this.credential = credential;
205230
this.endpoint = endpoint;
@@ -209,6 +234,7 @@ public class GoogleCloudStorageClientSettings {
209234
this.applicationName = applicationName;
210235
this.tokenUri = tokenUri;
211236
this.proxySettings = proxySettings;
237+
this.truststoreSettings = truststoreSettings;
212238
}
213239

214240
public ServiceAccountCredentials getCredential() {
@@ -243,6 +269,10 @@ public ProxySettings getProxySettings() {
243269
return proxySettings;
244270
}
245271

272+
public TruststoreSettings getTruststoreSettings() {
273+
return truststoreSettings;
274+
}
275+
246276
public static Map<String, GoogleCloudStorageClientSettings> load(final Settings settings) {
247277
final Map<String, GoogleCloudStorageClientSettings> clients = new HashMap<>();
248278
for (final String clientName : settings.getGroups(PREFIX).keySet()) {
@@ -265,7 +295,8 @@ static GoogleCloudStorageClientSettings getClientSettings(final Settings setting
265295
getConfigValue(settings, clientName, READ_TIMEOUT_SETTING),
266296
getConfigValue(settings, clientName, APPLICATION_NAME_SETTING),
267297
getConfigValue(settings, clientName, TOKEN_URI_SETTING),
268-
validateAndCreateProxySettings(settings, clientName)
298+
validateAndCreateProxySettings(settings, clientName),
299+
validateAndCreateTruststoreSettings(settings, clientName)
269300
);
270301
}
271302

@@ -297,6 +328,36 @@ static ProxySettings validateAndCreateProxySettings(final Settings settings, fin
297328
}
298329
}
299330

331+
static TruststoreSettings validateAndCreateTruststoreSettings(final Settings settings, final String clientName) {
332+
final String truststorePathString = getConfigValue(settings, clientName, TRUSTSTORE_PATH_SETTING);
333+
final String truststoreType = getConfigValue(settings, clientName, TRUSTSTORE_TYPE_SETTING);
334+
final SecureString truststorePassword = getConfigValue(settings, clientName, TRUSTSTORE_PASSWORD_SETTING);
335+
final SecureString password = (truststorePassword != null) ? truststorePassword : new SecureString(new char[0]);
336+
final boolean hasPath = Strings.hasText(truststorePathString);
337+
final boolean hasType = Strings.hasText(truststoreType);
338+
final boolean hasPassword = Strings.hasText(password);
339+
340+
if (!hasPath && !hasType && !hasPassword) {
341+
return TruststoreSettings.NO_TRUSTSTORE_SETTINGS;
342+
}
343+
if (!hasType) {
344+
throw new SettingsException(
345+
"Google Cloud Storage truststore type is missing. Use '"
346+
+ TRUSTSTORE_TYPE_SETTING.getConcreteSettingForNamespace(clientName).getKey()
347+
+ "' setting to configure the truststore type."
348+
);
349+
}
350+
if (!hasPath) {
351+
throw new SettingsException(
352+
"Google Cloud Storage truststore path is missing. Use '"
353+
+ TRUSTSTORE_PATH_SETTING.getConcreteSettingForNamespace(clientName).getKey()
354+
+ "' setting to configure the truststore path."
355+
);
356+
}
357+
358+
return new TruststoreSettings(java.nio.file.Path.of(truststorePathString), truststoreType, password);
359+
}
360+
300361
/**
301362
* Loads the service account file corresponding to a given client name. If no
302363
* file is defined for the client, a {@code null} credential is returned.

plugins/repository-gcs/src/main/java/org/opensearch/repositories/gcs/GoogleCloudStoragePlugin.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ public List<Setting<?>> getSettings() {
9797
GoogleCloudStorageClientSettings.PROXY_HOST_SETTING,
9898
GoogleCloudStorageClientSettings.PROXY_PORT_SETTING,
9999
GoogleCloudStorageClientSettings.PROXY_USERNAME_SETTING,
100-
GoogleCloudStorageClientSettings.PROXY_PASSWORD_SETTING
100+
GoogleCloudStorageClientSettings.PROXY_PASSWORD_SETTING,
101+
GoogleCloudStorageClientSettings.TRUSTSTORE_PATH_SETTING,
102+
GoogleCloudStorageClientSettings.TRUSTSTORE_PASSWORD_SETTING,
103+
GoogleCloudStorageClientSettings.TRUSTSTORE_TYPE_SETTING
101104
);
102105
}
103106

plugins/repository-gcs/src/main/java/org/opensearch/repositories/gcs/GoogleCloudStorageService.java

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@
4949
import org.opensearch.common.collect.MapBuilder;
5050
import org.opensearch.common.unit.TimeValue;
5151
import org.opensearch.core.common.Strings;
52+
import org.opensearch.core.common.settings.SecureString;
5253

5354
import java.io.IOException;
54-
import java.io.InputStream;
5555
import java.net.Authenticator;
5656
import java.net.PasswordAuthentication;
5757
import java.net.Proxy;
5858
import java.net.URI;
59+
import java.nio.file.Files;
60+
import java.security.GeneralSecurityException;
5961
import java.security.KeyStore;
6062
import java.security.Security;
6163
import java.util.Map;
@@ -189,18 +191,12 @@ public HttpRequestInitializer getHttpRequestInitializer(ServiceOptions<?, ?> ser
189191
private HttpTransport createHttpTransport(final GoogleCloudStorageClientSettings clientSettings) throws IOException {
190192
return SocketAccess.doPrivilegedIOException(() -> {
191193
final NetHttpTransport.Builder builder = new NetHttpTransport.Builder();
192-
KeyStore certTrustStore;
193-
if (Security.getProvider("BCFIPS") != null) {
194-
certTrustStore = KeyStore.getInstance("BCFKS");
195-
InputStream keyStoreStream = getClass().getResourceAsStream("/google.bcfks");
196-
SecurityUtils.loadKeyStore(certTrustStore, keyStoreStream, "notasecret");
197-
} else {
198-
// requires java.lang.RuntimePermission "setFactory"
199-
// Pin the TLS trust certificates.
200-
certTrustStore = GoogleUtils.getCertificateTrustStore();
194+
final TruststoreSettings truststoreSettings = clientSettings.getTruststoreSettings();
195+
try {
196+
builder.trustCertificates(loadTrustStore(truststoreSettings));
197+
} catch (GeneralSecurityException e) {
198+
throw new IllegalStateException("Failed to load truststore", e);
201199
}
202-
203-
builder.trustCertificates(certTrustStore);
204200
final ProxySettings proxySettings = clientSettings.getProxySettings();
205201
if (proxySettings != ProxySettings.NO_PROXY_SETTINGS) {
206202
if (proxySettings.isAuthenticated()) {
@@ -217,6 +213,32 @@ protected PasswordAuthentication getPasswordAuthentication() {
217213
});
218214
}
219215

216+
private KeyStore loadTrustStore(TruststoreSettings truststoreSettings) throws GeneralSecurityException, IOException {
217+
KeyStore certTrustStore;
218+
if (truststoreSettings.isConfigured()) {
219+
final var truststorePath = truststoreSettings.path();
220+
final var truststoreType = truststoreSettings.type();
221+
final SecureString truststorePassword = truststoreSettings.password();
222+
certTrustStore = KeyStore.getInstance(truststoreType);
223+
try (var trustStoreStream = Files.newInputStream(truststorePath)) {
224+
SecurityUtils.loadKeyStore(certTrustStore, trustStoreStream, truststorePassword.toString());
225+
}
226+
logger.debug("Loaded custom truststore from path: {} with type: {}", truststorePath, truststoreType);
227+
} else if (Security.getProvider("BCFIPS") != null) {
228+
throw new IllegalStateException(
229+
"FIPS mode is active but no custom truststore is configured. "
230+
+ "Please configure gcs.client.<client-name>.truststore.path and "
231+
+ "gcs.client.<client-name>.truststore.secure_password settings."
232+
);
233+
} else {
234+
// requires java.lang.RuntimePermission "setFactory"
235+
// Pin the TLS trust certificates.
236+
certTrustStore = GoogleUtils.getCertificateTrustStore();
237+
logger.debug("Using Google default certificate trust store");
238+
}
239+
return certTrustStore;
240+
}
241+
220242
StorageOptions createStorageOptions(
221243
final GoogleCloudStorageClientSettings clientSettings,
222244
final HttpTransportOptions httpTransportOptions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.repositories.gcs;
10+
11+
import org.opensearch.core.common.Strings;
12+
import org.opensearch.core.common.settings.SecureString;
13+
14+
import java.nio.file.Path;
15+
16+
/**
17+
* Encapsulates truststore configuration for Google Cloud Storage client.
18+
* Provides validation and convenient access to truststore settings.
19+
*/
20+
public record TruststoreSettings(Path path, String type, SecureString password) {
21+
22+
public static final TruststoreSettings NO_TRUSTSTORE_SETTINGS = new TruststoreSettings(null, null, null);
23+
24+
public boolean isConfigured() {
25+
return path != null && Strings.hasText(type);
26+
}
27+
28+
}

plugins/repository-gcs/src/main/resources/README.md

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)