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

Catalog/S3: Allow custom trust and key stores #9012

Merged
merged 3 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void init() {
server = mockServer(mock -> {});

S3Config s3config = S3Config.builder().build();
httpClient = S3Clients.apacheHttpClient(s3config);
httpClient = S3Clients.apacheHttpClient(s3config, new SecretsProvider(names -> Map.of()));

S3Options<S3BucketOptions> s3options =
S3ProgrammaticOptions.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
Expand All @@ -41,6 +42,7 @@
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.projectnessie.catalog.secrets.SecretsProvider;
import org.projectnessie.objectstoragemock.ObjectStorageMock;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.regions.Region;
Expand Down Expand Up @@ -70,7 +72,7 @@ public void init() {
server = mockServer(mock -> {});

S3Config s3config = S3Config.builder().build();
httpClient = S3Clients.apacheHttpClient(s3config);
httpClient = S3Clients.apacheHttpClient(s3config, new SecretsProvider(names -> Map.of()));

S3Options<S3BucketOptions> s3options =
S3ProgrammaticOptions.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,40 @@
*/
package org.projectnessie.catalog.files.s3;

import static org.projectnessie.catalog.secrets.SecretType.KEY;
import static software.amazon.awssdk.http.SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.Optional;
import javax.net.ssl.KeyManager;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.projectnessie.catalog.secrets.BasicCredentials;
import org.projectnessie.catalog.secrets.KeySecret;
import org.projectnessie.catalog.secrets.SecretAttribute;
import org.projectnessie.catalog.secrets.SecretsProvider;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.TlsTrustManagersProvider;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.internal.http.AbstractFileStoreTlsKeyManagersProvider;
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.Validate;

public class S3Clients {

/** Builds an SDK Http client based on the Apache Http client. */
public static SdkHttpClient apacheHttpClient(S3Config s3Config) {
public static SdkHttpClient apacheHttpClient(S3Config s3Config, SecretsProvider secretsProvider) {
ApacheHttpClient.Builder httpClient = ApacheHttpClient.builder();
s3Config.maxHttpConnections().ifPresent(httpClient::maxConnections);
s3Config.readTimeout().ifPresent(httpClient::socketTimeout);
Expand All @@ -34,7 +57,65 @@ public static SdkHttpClient apacheHttpClient(S3Config s3Config) {
s3Config.connectionMaxIdleTime().ifPresent(httpClient::connectionMaxIdleTime);
s3Config.connectionTimeToLive().ifPresent(httpClient::connectionTimeToLive);
s3Config.expectContinueEnabled().ifPresent(httpClient::expectContinueEnabled);
return httpClient.build();
s3Config
.trustStorePath()
.ifPresent(
p -> {
S3Config withPassword =
secretsProvider
.applySecrets(
S3Config.builder().from(s3Config),
"s3.trust-store",
s3Config,
null,
null,
List.of(
SecretAttribute.secretAttribute(
"password",
KEY,
S3Config::trustStorePassword,
S3Config.Builder::trustStorePassword)))
.build();

httpClient.tlsTrustManagersProvider(
new FileStoreTlsTrustManagersProvider(
p,
withPassword
.trustStoreType()
.orElseThrow(() -> new IllegalArgumentException("No trust store type")),
withPassword.trustStorePassword().orElse(null)));
});
s3Config
.keyStorePath()
.ifPresent(
p -> {
S3Config withPassword =
secretsProvider
.applySecrets(
S3Config.builder().from(s3Config),
"s3.key-store",
s3Config,
null,
null,
List.of(
SecretAttribute.secretAttribute(
"password",
KEY,
S3Config::keyStorePassword,
S3Config.Builder::keyStorePassword)))
.build();

httpClient.tlsKeyManagersProvider(
new FileStoreTlsKeyManagersProvider(
p,
withPassword
.keyStoreType()
.orElseThrow(() -> new IllegalArgumentException("No key store type")),
withPassword.keyStorePassword().orElse(null)));
});
AttributeMap.Builder options = AttributeMap.builder();
s3Config.trustAllCertificates().ifPresent(v -> options.put(TRUST_ALL_CERTIFICATES, v));
return httpClient.buildWithDefaults(options.build());
}

public static AwsCredentialsProvider basicCredentialsProvider(
Expand Down Expand Up @@ -63,4 +144,60 @@ public static AwsCredentialsProvider awsCredentialsProvider(

return sessions.assumeRoleForServer(bucketOptions);
}

private static final class FileStoreTlsTrustManagersProvider implements TlsTrustManagersProvider {
private final Path path;
private final String type;
private final char[] password;

FileStoreTlsTrustManagersProvider(Path path, String type, KeySecret password) {
this.path = path;
this.type = type;
this.password = password != null ? password.key().toCharArray() : null;
}

@Override
public TrustManager[] trustManagers() {
try (InputStream storeInputStream = Files.newInputStream(path)) {
KeyStore keyStore = KeyStore.getInstance(type);
keyStore.load(storeInputStream, password);
TrustManagerFactory tmf =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
return tmf.getTrustManagers();
} catch (KeyStoreException
| CertificateException
| NoSuchAlgorithmException
| IOException e) {
throw new RuntimeException(e);
}
}
}

private static final class FileStoreTlsKeyManagersProvider
extends AbstractFileStoreTlsKeyManagersProvider {

private final Path storePath;
private final String storeType;
private final char[] password;

FileStoreTlsKeyManagersProvider(Path storePath, String storeType, KeySecret password) {
this.storePath = Validate.paramNotNull(storePath, "storePath");
this.storeType = Validate.paramNotBlank(storeType, "storeType");
this.password = password != null ? password.key().toCharArray() : null;
}

@Override
public KeyManager[] keyManagers() {
try {
return createKeyManagers(storePath, storeType, password);
} catch (CertificateException
| UnrecoverableKeyException
| IOException
| KeyStoreException
| NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package org.projectnessie.catalog.files.s3;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.file.Path;
import java.time.Duration;
import java.util.Optional;
import java.util.OptionalInt;
import org.immutables.value.Value;
import org.projectnessie.catalog.secrets.KeySecret;
import org.projectnessie.nessie.docgen.annotations.ConfigDocs.ConfigItem;

@Value.Immutable
Expand Down Expand Up @@ -55,6 +57,72 @@ public interface S3Config {
@ConfigItem(section = "transport")
Optional<Boolean> expectContinueEnabled();

/**
* Instruct the S3 HTTP client to accept all SSL certificates, if set to {@code true}. Enabling
* this option is dangerous, it is strongly recommended to leave this option unset or {@code
* false}.
*/
@ConfigItem(section = "transport")
Optional<Boolean> trustAllCertificates();

/**
* Override to set the file path to a custom SSL trust store. {@code
* nessie.catalog.service.s3.trust-store.type} and {@code
* nessie.catalog.service.s3.trust-store.password} must be supplied as well when providing a
* custom trust store.
*
* <p>When running in k8s or Docker, the path is local within the pod/container and must be
Copy link
Contributor

Choose a reason for hiding this comment

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

Should I go ahead an implement support for this in the Helm chart?

* explicitly mounted.
*/
@ConfigItem(section = "transport")
Optional<Path> trustStorePath();

/**
* Override to set the type of the custom SSL trust store specified in {@code
* nessie.catalog.service.s3.trust-store.path}.
*
* <p>Supported types include {@code JKS}, {@code PKCS12}, and all key store types supported by
* Java 17.
*/
@ConfigItem(section = "transport")
Optional<String> trustStoreType();

/**
* Override to set the password for the custom SSL trust store specified in {@code
* nessie.catalog.service.s3.trust-store.path}.
*/
@ConfigItem(section = "transport")
Optional<KeySecret> trustStorePassword();

/**
* Override to set the file path to a custom SSL key store. {@code
* nessie.catalog.service.s3.key-store.type} and {@code
* nessie.catalog.service.s3.key-store.password} must be supplied as well when providing a custom
* key store.
*
* <p>When running in k8s or Docker, the path is local within the pod/container and must be
* explicitly mounted.
*/
@ConfigItem(section = "transport")
Optional<Path> keyStorePath();

/**
* Override to set the type of the custom SSL key store specified in {@code
* nessie.catalog.service.s3.key-store.path}.
*
* <p>Supported types include {@code JKS}, {@code PKCS12}, and all key store types supported by
* Java 17.
*/
@ConfigItem(section = "transport")
Optional<String> keyStoreType();

/**
* Override to set the password for the custom SSL key store specified in {@code
* nessie.catalog.service.s3.key-store.path}.
*/
@ConfigItem(section = "transport")
Optional<KeySecret> keyStorePassword();

/**
* Interval after which a request is retried when S3 response with some "retry later" response.
*/
Expand All @@ -64,7 +132,11 @@ static Builder builder() {
return ImmutableS3Config.builder();
}

@SuppressWarnings("unused")
interface Builder {
@CanIgnoreReturnValue
Builder from(S3Config instance);

@CanIgnoreReturnValue
Builder maxHttpConnections(int maxHttpConnections);

Expand All @@ -86,6 +158,24 @@ interface Builder {
@CanIgnoreReturnValue
Builder expectContinueEnabled(boolean expectContinueEnabled);

@CanIgnoreReturnValue
Builder trustStorePath(Path trustStorePath);

@CanIgnoreReturnValue
Builder trustStoreType(String trustStoreType);

@CanIgnoreReturnValue
Builder trustStorePassword(KeySecret trustStorePassword);

@CanIgnoreReturnValue
Builder keyStorePath(Path keyStorePath);

@CanIgnoreReturnValue
Builder keyStoreType(String keyStoreType);

@CanIgnoreReturnValue
Builder keyStorePassword(KeySecret keyStorePassword);

@CanIgnoreReturnValue
Builder retryAfter(Duration retryAfter);

Expand Down
Loading