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

feat(TLS): allow users to import whole truststore + pass #461

Merged
merged 9 commits into from
Aug 27, 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
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,20 @@ and how it advertises itself to a Cryostat server instance. Properties that requ
- [ ] `cryostat.agent.webclient.tls.version` [`String`]: the version of TLS used for the Agent's client SSL context. Default `TLSv1.2`.
- [ ] `cryostat.agent.webclient.tls.trust-all` [`boolean`]: control whether the agent trusts all certificates presented by the Cryostat server. Default `false`. This should only be overridden for development and testing purposes, never in production.
- [ ] `cryostat.agent.webclient.tls.verify-hostname` [`boolean`]: control whether the agent verifies hostnames on certificates presented by the Cryostat server. Default `true`. This should only be overridden for development and testing purposes, never in production.
- [ ] `cryostat.agent.webclient.tls.trustore.certs` [`list`]: the list of truststoreConfig objects with alias, path, and type properties for certificates to be stored in the agent's truststore. For example, 'cryostat.agent.webclient.tls.truststore.certs[0].type' would be the type of the first certificate in this list. A truststoreConfig object must contain all three properties to be a valid certificate entry.
- [ ] `cryostat.agent.webclient.tls.trustore.certs` [`list`]: the list of truststoreConfig objects with alias, path, and type properties for certificates to be stored in the agent's truststore. For example, 'cryostat.agent.webclient.tls.truststore.certs[0].type' would be the type of the first certificate in this list. A truststoreConfig object must contain all three properties to be a valid certificate entry.
- [ ] `cryostat.agent.webclient.tls.truststore.type` [`String`]: the type of truststore used for the agent's client truststore. Default `JKS`.
- [ ] `cryostat.agent.webclient.tls.truststore.path` [`String`]: the filepath to the agent's webclient truststore. This takes precedent over `cryostat.agent.webclient.tls.truststore.certs` and must be configured with the truststore's pass with `cryostat.agent.webclient.tls.truststore.pass.file` or `cryostat.agent.webclient.tls.truststore.pass`.
- [ ] `cryostat.agent.webclient.tls.truststore.pass.file` [`String`]: the filepath to the agent's client truststore's password
- [ ] `cryostat.agent.webclient.tls.truststore.pass.charset` [`String`]: the character set used by the agent's client truststore's password. Default `utf-8`.
- [ ] `cryostat.agent.webclient.tls.truststore.pass` [`String`]: the String format of the agent's client truststore's pass
- [ ] `cryostat.agent.webclient.connect.timeout-ms` [`long`]: the duration in milliseconds to wait for HTTP requests to the Cryostat server to connect. Default `1000`.
- [ ] `cryostat.agent.webclient.response.timeout-ms` [`long`]: the duration in milliseconds to wait for HTTP requests to the Cryostat server to respond. Default `1000`.
- [ ] `cryostat.agent.webserver.host` [`String`]: the internal hostname or IP address for the embedded webserver to bind to. Default `0.0.0.0`.
- [ ] `cryostat.agent.webserver.port` [`int`]: the internal port number for the embedded webserver to bind to. Default `9977`.
- [ ] `cryostat.agent.webserver.tls.version` [`String`]: the version of TLS used for the Agent's server SSL context. Default `TLSv1.2`.
- [ ] `cryostat.agent.webserver.tls.keystore.pass` [`String`]: the filepath to the HTTPS server keystore's password
- [ ] `cryostat.agent.webserver.tls.keystore.pass.charset` [`String`]: the character set used by the HTTPS server keystore's password. Default `utf-8`.
- [ ] `cryostat.agent.webserver.tls.keystore.file` [`String`]: the file path to the HTTPS server keystore
- [ ] `cryostat.agent.webserver.tls.keystore.file` [`String`]: the filepath to the HTTPS server keystore
- [ ] `cryostat.agent.webserver.tls.keystore.type` [`String`]: the type of keystore used for the Agent's HTTPS server. Default `PKCS12`.
- [ ] `cryostat.agent.webserver.tls.cert.alias` [`String`]: the alias for the certificate stored in the HTTPS server keystore. Default `serverCert`.
- [ ] `cryostat.agent.webserver.tls.cert.file` [`String`]: the filepath to the certificate to be stored by the HTTPS server keystore
Expand Down
106 changes: 104 additions & 2 deletions src/main/java/io/cryostat/agent/ConfigModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
*/
package io.cryostat.agent;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -77,6 +80,16 @@ public abstract class ConfigModule {
"cryostat.agent.webclient.connect.timeout-ms";
public static final String CRYOSTAT_AGENT_WEBCLIENT_RESPONSE_TIMEOUT_MS =
"cryostat.agent.webclient.response.timeout-ms";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH =
"cryostat.agent.webclient.tls.truststore.path";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE =
"cryostat.agent.webclient.tls.truststore.pass.file";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET =
"cryostat.agent.webclient.tls.truststore.pass-charset";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS =
"cryostat.agent.webclient.tls.truststore.pass";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_TYPE =
"cryostat.agent.webclient.tls.truststore.type";
public static final String CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_CERTS =
"cryostat.agent.webclient.tls.truststore.cert";
public static final Pattern CRYOSTAT_AGENT_TRUSTSTORE_PATTERN =
Expand Down Expand Up @@ -254,12 +267,78 @@ public static int provideCryostatAgentWebclientResponseTimeoutMs(Config config)
return config.getValue(CRYOSTAT_AGENT_WEBCLIENT_RESPONSE_TIMEOUT_MS, int.class);
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH)
public static Optional<String> provideCryostatAgentWebclientTlsTruststorePath(Config config) {
return config.getOptionalValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH, String.class);
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE)
public static Optional<BytePass> provideCryostatAgentWebclientTlsTruststorePassFromFile(
Config config) {
Optional<String> truststorePassFile =
config.getOptionalValue(
CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE, String.class);
if (truststorePassFile.isEmpty()) {
return Optional.empty();
}
try (FileInputStream passFile = new FileInputStream(truststorePassFile.get())) {
byte[] pass = passFile.readAllBytes();
Optional<BytePass> bytePass = Optional.of(new BytePass(pass));
Arrays.fill(pass, (byte) 0);
return bytePass;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET)
public static String provideCryostatAgentWebclientTlsTruststorePassCharset(Config config) {
return config.getValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET, String.class);
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS)
public static Optional<BytePass> provideCryostatAgentWebclientTlsTruststorePass(
Config config,
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE)
Optional<BytePass> truststorePass,
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET) String passCharset) {
Optional<String> opt =
config.getOptionalValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS, String.class);
andrewazores marked this conversation as resolved.
Show resolved Hide resolved
if (opt.isEmpty()) {
return truststorePass;
}
return Optional.of(new BytePass(opt.get(), passCharset));
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_TYPE)
public static String provideCryostatAgentWebclientTlsTruststoreType(Config config) {
return config.getValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_TYPE, String.class);
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_CERTS)
public static List<TruststoreConfig> provideCryostatAgentWecblientTlsTruststoreCerts(
Config config) {
Config config,
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS) Optional<BytePass> truststorePass,
@Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH) Optional<String> truststorePath) {
Map<Integer, TruststoreConfig.Builder> truststoreBuilders = new HashMap<>();
List<TruststoreConfig> truststoreConfigs = new ArrayList<>();

if (!truststorePass.isEmpty() || !truststorePath.isEmpty()) {
return truststoreConfigs;
}

StreamSupport.stream(config.getPropertyNames().spliterator(), false)
.filter(e -> e.startsWith(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_CERTS))
.forEach(
Expand Down Expand Up @@ -302,7 +381,6 @@ public static List<TruststoreConfig> provideCryostatAgentWecblientTlsTruststoreC
}
});

List<TruststoreConfig> truststoreConfigs = new ArrayList<>();
for (TruststoreConfig.Builder builder : truststoreBuilders.values()) {
try {
truststoreConfigs.add(builder.build());
Expand Down Expand Up @@ -642,4 +720,28 @@ public static URIRange fromString(String s) {
return SITE_LOCAL;
}
}

public static class BytePass {
private final byte[] buf;

public BytePass(int len) {
this.buf = new byte[len];
}

public BytePass(byte[] s) {
this.buf = Arrays.copyOf(s, s.length);
andrewazores marked this conversation as resolved.
Show resolved Hide resolved
}

public BytePass(String s, String charset) {
this.buf = Arrays.copyOf(s.getBytes(Charset.forName(charset)), s.length());
}

public byte[] get() {
return Arrays.copyOf(this.buf, this.buf.length);
}

public void clear() {
Arrays.fill(this.buf, (byte) 0);
}
}
}
65 changes: 50 additions & 15 deletions src/main/java/io/cryostat/agent/MainModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
Expand Down Expand Up @@ -48,6 +51,7 @@
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import io.cryostat.agent.ConfigModule.BytePass;
import io.cryostat.agent.harvest.HarvestModule;
import io.cryostat.agent.remote.RemoteContext;
import io.cryostat.agent.remote.RemoteModule;
Expand Down Expand Up @@ -133,8 +137,15 @@ public static WebServer provideWebServer(
public static SSLContext provideClientSslContext(
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_VERSION) String clientTlsVersion,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUST_ALL) boolean trustAll,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH)
Optional<String> truststorePath,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS)
Optional<BytePass> truststorePass,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET)
String passCharset,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_TYPE) String truststoreType,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_CERTS)
List<TruststoreConfig> truststores) {
List<TruststoreConfig> truststoreCerts) {
try {
if (trustAll) {
SSLContext sslCtx = SSLContext.getInstance(clientTlsVersion);
Expand Down Expand Up @@ -177,24 +188,48 @@ public X509Certificate[] getAcceptedIssuers() {
}
}

// initialize truststore
KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType());
KeyStore ts = KeyStore.getInstance(truststoreType);
ts.load(null, null);
mwangggg marked this conversation as resolved.
Show resolved Hide resolved

for (TruststoreConfig truststore : truststores) {
// initialize truststore with user provided path and pass
if (!truststorePath.isEmpty() && !truststorePass.isEmpty()) {
Charset charset = Charset.forName(passCharset);
CharsetDecoder decoder = charset.newDecoder();
ByteBuffer byteBuffer = ByteBuffer.wrap(truststorePass.get().get());
CharBuffer charBuffer = decoder.decode(byteBuffer);
try (InputStream truststore = new FileInputStream(truststorePath.get())) {
ts.load(truststore, charBuffer.array());
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
byteBuffer.clear();
charBuffer.clear();
truststorePass.get().clear();
}
} else if (!truststorePath.isEmpty() || !truststorePass.isEmpty()) {
throw new IllegalArgumentException(
String.format(
"To import a truststore, provide both the path to the truststore"
+ " and the pass, or a path to a file containing the pass"));
}

// initialize truststore with user provided certs
for (TruststoreConfig truststore : truststoreCerts) {
// load truststore with certificatesCertificate
InputStream certFile = new FileInputStream(truststore.getPath());
CertificateFactory cf = CertificateFactory.getInstance(truststore.getType());
Certificate cert = cf.generateCertificate(certFile);
if (ts.containsAlias(truststore.getType())) {
throw new IllegalStateException(
String.format(
"truststore already contains a certificate with alias"
+ " \"%s\"",
truststore.getAlias()));
try (InputStream certFile = new FileInputStream(truststore.getPath())) {
CertificateFactory cf = CertificateFactory.getInstance(truststore.getType());
Certificate cert = cf.generateCertificate(certFile);
if (ts.containsAlias(truststore.getType())) {
throw new IllegalStateException(
String.format(
"truststore already contains a certificate with alias"
+ " \"%s\"",
truststore.getAlias()));
}
ts.setCertificateEntry(truststore.getAlias(), cert);
} catch (CertificateException e) {
throw new RuntimeException(e);
}
ts.setCertificateEntry(truststore.getAlias(), cert);
certFile.close();
}

// set up trust manager factory
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/io/cryostat/agent/TruststoreConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ private TruststoreConfig(Builder builder) {
this.alias =
Objects.requireNonNull(
builder.alias,
"Truststore config properties must include a certificate alias");
"Imported certs for the agent's truststore must include a certificate"
+ " alias");
this.path =
Objects.requireNonNull(
builder.path,
"Truststore config properties must include a certificate path");
"Imported certs for the agent's truststore must include a certificate"
+ " path");
this.type =
Objects.requireNonNull(
builder.type,
"Truststore config properties must include a certificate type");
"Imported certs for the agent's truststore must include a certificate"
+ " type");
}

public String getAlias() {
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/META-INF/microprofile-config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ cryostat.agent.api.writes-enabled=false
cryostat.agent.webclient.tls.version=TLSv1.2
cryostat.agent.webclient.tls.trust-all=false
cryostat.agent.webclient.tls.verify-hostname=true
cryostat.agent.webclient.tls.truststore.type=JKS
cryostat.agent.webclient.tls.truststore.pass-charset=utf-8
cryostat.agent.webclient.connect.timeout-ms=1000
cryostat.agent.webclient.response.timeout-ms=1000
cryostat.agent.webclient.response.retry-count=3
Expand Down
Loading