diff --git a/README.md b/README.md index a3a9e22d..ca7a81a0 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,12 @@ 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`. @@ -184,7 +189,7 @@ and how it advertises itself to a Cryostat server instance. Properties that requ - [ ] `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 diff --git a/src/main/java/io/cryostat/agent/ConfigModule.java b/src/main/java/io/cryostat/agent/ConfigModule.java index 9fc443ad..f7c9da6f 100644 --- a/src/main/java/io/cryostat/agent/ConfigModule.java +++ b/src/main/java/io/cryostat/agent/ConfigModule.java @@ -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; @@ -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 = @@ -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 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 provideCryostatAgentWebclientTlsTruststorePassFromFile( + Config config) { + Optional 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 = 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 provideCryostatAgentWebclientTlsTruststorePass( + Config config, + @Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_FILE) + Optional truststorePass, + @Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS_CHARSET) String passCharset) { + Optional opt = + config.getOptionalValue(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS, String.class); + 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 provideCryostatAgentWecblientTlsTruststoreCerts( - Config config) { + Config config, + @Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS) Optional truststorePass, + @Named(CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PATH) Optional truststorePath) { Map truststoreBuilders = new HashMap<>(); + List 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( @@ -302,7 +381,6 @@ public static List provideCryostatAgentWecblientTlsTruststoreC } }); - List truststoreConfigs = new ArrayList<>(); for (TruststoreConfig.Builder builder : truststoreBuilders.values()) { try { truststoreConfigs.add(builder.build()); @@ -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); + } + + 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); + } + } } diff --git a/src/main/java/io/cryostat/agent/MainModule.java b/src/main/java/io/cryostat/agent/MainModule.java index 0d867cf2..2094c6f0 100644 --- a/src/main/java/io/cryostat/agent/MainModule.java +++ b/src/main/java/io/cryostat/agent/MainModule.java @@ -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; @@ -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; @@ -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 truststorePath, + @Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_TLS_TRUSTSTORE_PASS) + Optional 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 truststores) { + List truststoreCerts) { try { if (trustAll) { SSLContext sslCtx = SSLContext.getInstance(clientTlsVersion); @@ -177,24 +188,48 @@ public X509Certificate[] getAcceptedIssuers() { } } - // initialize truststore - KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType()); + KeyStore ts = KeyStore.getInstance(truststoreType); ts.load(null, null); - 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 diff --git a/src/main/java/io/cryostat/agent/TruststoreConfig.java b/src/main/java/io/cryostat/agent/TruststoreConfig.java index f5f390ce..fe34df2a 100644 --- a/src/main/java/io/cryostat/agent/TruststoreConfig.java +++ b/src/main/java/io/cryostat/agent/TruststoreConfig.java @@ -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() { diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index b8af09e6..a77fcf49 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -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