Skip to content

Commit

Permalink
Allow SSL certificate to be passed as a file path (#1517)
Browse files Browse the repository at this point in the history
Co-authored-by: Guillaume Grossetie <[email protected]>
  • Loading branch information
felixvanoost and ggrossetie committed Apr 21, 2023
1 parent 28f4250 commit fd5b64f
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 20 deletions.
21 changes: 18 additions & 3 deletions docs/modules/setup/pages/configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,23 @@ We recommend to use a maximum length that's not greater than 8192 and not greate

By default, SSL/TLS is not enabled on the server but you can enable it by setting `KROKI_SSL` environment variable to `true`.

When SSL is enabled, you must provide the certificate and the private key values as PEM format using `KROKI_SSL_KEY` and `KROKI_SSL_CERT` environment variables.
When SSL is enabled, you must provide the certificate and the private key in one of two ways:

- As strings in PEM format using the `KROKI_SSL_KEY` and `KROKI_SSL_CERT` environment variables, e.g.,:
+
[source,bash]
----
KROKI_SSL_KEY="-----BEGIN RSA PRIVATE KEY-----<PRIVATE_KEY>-----END RSA PRIVATE KEY-----"
----

- As PEM file paths using the `KROKI_SSL_KEY_PATH` and `KROKI_SSL_CERT_PATH` environment variables.
+
[source,bash]
----
KROKI_SSL_KEY_PATH="/etc/ssl/certs/mydomain/privatekey.pem"
----

If both methods are used, the values in `KROKI_SSL_KEY` and `KROKI_SSL_CERT` are given priority.

[NOTE]
====
Expand Down Expand Up @@ -255,5 +271,4 @@ podman run -p8000:8000 -e KROKI_SSL=true --env-file=.env yuzutech/kroki
====

If SSL is enabled, both `KROKI_SSL_KEY` and `KROKI_SSL_CERT` must be configured.

If SSL is enabled, both `KROKI_SSL_KEY` (or `KROKI_SSL_KEY_PATH`) and `KROKI_SSL_CERT` (or `KROKI_SSL_CERT_PATH`) must be configured.
32 changes: 25 additions & 7 deletions server/src/main/java/io/kroki/server/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,34 @@ static void start(Vertx vertx, JsonObject config, Handler<AsyncResult<HttpServer

private static void setPemKeyCertOptions(JsonObject config, HttpServerOptions serverOptions, boolean enableSSL) {
if (enableSSL) {
PemKeyCertOptions certOptions = new PemKeyCertOptions();
Optional<String> sslKeyValue = Optional.ofNullable(config.getString("KROKI_SSL_KEY"));
Optional<String> sslKeyPath = Optional.ofNullable(config.getString("KROKI_SSL_KEY_PATH"));
Optional<String> sslCertValue = Optional.ofNullable(config.getString("KROKI_SSL_CERT"));
if (sslKeyValue.isEmpty() || sslCertValue.isEmpty()) {
throw new IllegalArgumentException("KROKI_SSL_KEY and KROKI_SSL_CERT must be configured when SSL is enabled.");
Optional<String> sslCertPath = Optional.ofNullable(config.getString("KROKI_SSL_CERT_PATH"));

if (sslKeyValue.isEmpty() && sslKeyPath.isEmpty()) {
throw new IllegalArgumentException("KROKI_SSL_KEY or KROKI_SSL_KEY_PATH must be configured when SSL is enabled.");
}
serverOptions.setPemKeyCertOptions(
new PemKeyCertOptions()
.addKeyValue(Buffer.buffer(sslKeyValue.get()))
.addCertValue(Buffer.buffer(sslCertValue.get()))
);
if (sslCertValue.isEmpty() && sslCertPath.isEmpty()) {
throw new IllegalArgumentException("KROKI_SSL_CERT or KROKI_SSL_CERT_PATH must be configured when SSL is enabled.");
}

if (sslKeyValue.isPresent()) {
certOptions.addKeyValue(Buffer.buffer(config.getString("KROKI_SSL_KEY")));
}
else {
certOptions.addKeyPath(config.getString("KROKI_SSL_KEY_PATH"));
}

if (sslCertValue.isPresent()) {
certOptions.addCertValue(Buffer.buffer(config.getString("KROKI_SSL_CERT")));
}
else {
certOptions.addCertPath(config.getString("KROKI_SSL_CERT_PATH"));
}

serverOptions.setPemKeyCertOptions(certOptions);
}
}

Expand Down
148 changes: 138 additions & 10 deletions server/src/test/java/io/kroki/server/ServerSSLTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ void with_ssl_disabled(Vertx vertx, VertxTestContext testContext) {
}

@Test
void with_ssl_enabled_and_secure_http_web_client(Vertx vertx, VertxTestContext testContext) {
void with_ssl_enabled_by_string_and_secure_http_web_client(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// successful deployment
testContext.verify(() -> assertThat(deployVerticleResult.failed()).isFalse());
Expand All @@ -72,11 +72,47 @@ void with_ssl_enabled_and_secure_http_web_client(Vertx vertx, VertxTestContext t
testContext.completeNow();
})));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabled(vertx)), handle);
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledByString(vertx)), handle);
}

@Test
void with_ssl_enabled_and_insecure_http_web_client(Vertx vertx, VertxTestContext testContext) {
void with_ssl_enabled_by_path_and_secure_http_web_client(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// successful deployment
testContext.verify(() -> assertThat(deployVerticleResult.failed()).isFalse());
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setTrustAll(true);
WebClient client = WebClient.create(vertx, options);
// successful request
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.succeeding(response -> testContext.verify(() -> {
assertThat(response.body()).contains("https://kroki.io");
testContext.completeNow();
})));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledByPath(vertx)), handle);
}

@Test
void with_ssl_enabled_by_string_and_insecure_http_web_client(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// successful deployment
testContext.verify(() -> assertThat(deployVerticleResult.failed()).isFalse());
WebClient client = WebClient.create(vertx);
// failed request, client must send HTTPS request
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.failing(response -> testContext.verify(testContext::completeNow)));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledByString(vertx)), handle);
}

@Test
void with_ssl_enabled_by_path_and_insecure_http_web_client(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// successful deployment
testContext.verify(() -> assertThat(deployVerticleResult.failed()).isFalse());
Expand All @@ -87,17 +123,39 @@ void with_ssl_enabled_and_insecure_http_web_client(Vertx vertx, VertxTestContext
.as(BodyCodec.string())
.send(testContext.failing(response -> testContext.verify(testContext::completeNow)));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabled(vertx)), handle);
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledByPath(vertx)), handle);
}

@Test
void with_ssl_enabled_by_string_and_missing_ssl_key_config(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// failed deployment
testContext.verify(() -> {
assertThat(deployVerticleResult.failed()).isTrue();
assertThat(deployVerticleResult.cause()).isInstanceOf(IllegalArgumentException.class);
assertThat(deployVerticleResult.cause()).hasMessage("KROKI_SSL_KEY or KROKI_SSL_KEY_PATH must be configured when SSL is enabled.");
});
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setTrustAll(true);
WebClient client = WebClient.create(vertx, options);
// failed request, server has not started
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.failing(response -> testContext.verify(testContext::completeNow)));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledByStringMissingKey(vertx)), handle);
}

@Test
void with_ssl_enabled_and_missing_ssl_key_config(Vertx vertx, VertxTestContext testContext) {
void with_ssl_enabled_by_path_and_missing_ssl_key_config(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// failed deployment
testContext.verify(() -> {
assertThat(deployVerticleResult.failed()).isTrue();
assertThat(deployVerticleResult.cause()).isInstanceOf(IllegalArgumentException.class);
assertThat(deployVerticleResult.cause()).hasMessage("KROKI_SSL_KEY and KROKI_SSL_CERT must be configured when SSL is enabled.");
assertThat(deployVerticleResult.cause()).hasMessage("KROKI_SSL_KEY or KROKI_SSL_KEY_PATH must be configured when SSL is enabled.");
});
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
Expand All @@ -109,7 +167,51 @@ void with_ssl_enabled_and_missing_ssl_key_config(Vertx vertx, VertxTestContext t
.as(BodyCodec.string())
.send(testContext.failing(response -> testContext.verify(testContext::completeNow)));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledMissingKey(vertx)), handle);
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledByPathMissingKey(vertx)), handle);
}

@Test
void with_ssl_enabled_by_string_and_missing_ssl_cert_config(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// failed deployment
testContext.verify(() -> {
assertThat(deployVerticleResult.failed()).isTrue();
assertThat(deployVerticleResult.cause()).isInstanceOf(IllegalArgumentException.class);
assertThat(deployVerticleResult.cause()).hasMessage("KROKI_SSL_CERT or KROKI_SSL_CERT_PATH must be configured when SSL is enabled.");
});
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setTrustAll(true);
WebClient client = WebClient.create(vertx, options);
// failed request, server has not started
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.failing(response -> testContext.verify(testContext::completeNow)));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledByStringMissingCert(vertx)), handle);
}

@Test
void with_ssl_enabled_by_path_and_missing_ssl_cert_config(Vertx vertx, VertxTestContext testContext) {
Handler<AsyncResult<String>> handle = deployVerticleResult -> {
// failed deployment
testContext.verify(() -> {
assertThat(deployVerticleResult.failed()).isTrue();
assertThat(deployVerticleResult.cause()).isInstanceOf(IllegalArgumentException.class);
assertThat(deployVerticleResult.cause()).hasMessage("KROKI_SSL_CERT or KROKI_SSL_CERT_PATH must be configured when SSL is enabled.");
});
WebClientOptions options = new WebClientOptions();
options.setSsl(true);
options.setTrustAll(true);
WebClient client = WebClient.create(vertx, options);
// failed request, server has not started
client
.get(port, "localhost", "/")
.as(BodyCodec.string())
.send(testContext.failing(response -> testContext.verify(testContext::completeNow)));
};
vertx.deployVerticle(new Server(), new DeploymentOptions().setConfig(configWithSslEnabledByPathMissingCert(vertx)), handle);
}

private JsonObject configWithSslDisabled() {
Expand All @@ -118,17 +220,43 @@ private JsonObject configWithSslDisabled() {
.put("KROKI_SSL", false);
}

private JsonObject configWithSslEnabledMissingKey(Vertx vertx) {
JsonObject config = configWithSslEnabled(vertx);
private JsonObject configWithSslEnabledByStringMissingKey(Vertx vertx) {
JsonObject config = configWithSslEnabledByString(vertx);
config.remove("KROKI_SSL_KEY");
return config;
}

private JsonObject configWithSslEnabled(Vertx vertx) {
private JsonObject configWithSslEnabledByStringMissingCert(Vertx vertx) {
JsonObject config = configWithSslEnabledByString(vertx);
config.remove("KROKI_SSL_CERT");
return config;
}

private JsonObject configWithSslEnabledByString(Vertx vertx) {
return new JsonObject()
.put("KROKI_PORT", port)
.put("KROKI_SSL", true)
.put("KROKI_SSL_KEY", vertx.fileSystem().readFileBlocking(pemKeyCertOptions.getKeyPath()).toString())
.put("KROKI_SSL_CERT", vertx.fileSystem().readFileBlocking(pemKeyCertOptions.getCertPath()).toString());
}

private JsonObject configWithSslEnabledByPathMissingKey(Vertx vertx) {
JsonObject config = configWithSslEnabledByPath(vertx);
config.remove("KROKI_SSL_KEY_PATH");
return config;
}

private JsonObject configWithSslEnabledByPathMissingCert(Vertx vertx) {
JsonObject config = configWithSslEnabledByPath(vertx);
config.remove("KROKI_SSL_CERT_PATH");
return config;
}

private JsonObject configWithSslEnabledByPath(Vertx vertx) {
return new JsonObject()
.put("KROKI_PORT", port)
.put("KROKI_SSL", true)
.put("KROKI_SSL_KEY_PATH", pemKeyCertOptions.getKeyPath())
.put("KROKI_SSL_CERT_PATH", pemKeyCertOptions.getCertPath());
}
}

0 comments on commit fd5b64f

Please sign in to comment.