Skip to content

Commit

Permalink
[EDGCOMMON-78]. Enhance HTTP Endpoint Security with TLS and FIPS-140-…
Browse files Browse the repository at this point in the history
…2 Compliant Cryptography (#99)

* EDGCOMMON-79.Add ssl/tls support
  • Loading branch information
SerhiiNosko authored May 17, 2024
1 parent 519de70 commit e35fed0
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 43 deletions.
31 changes: 19 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,18 +152,25 @@ Configuration information is specified in two forms:

### System Properties

Property | Default | Description
------------------------ | ----------- | -------------
`port` | `8081` | Server port to listen on
`okapi_url` | *required* | Where to find Okapi (URL)
`secure_store` | `Ephemeral` | Type of secure store to use. Valid: `Ephemeral`, `AwsSsm`, `Vault`
`secure_store_props` | `NA` | Path to a properties file specifying secure store configuration
`token_cache_ttl_ms` | `3600000` | How long to cache JWTs, in milliseconds (ms)
`null_token_cache_ttl_ms`| `30000` | How long to cache login failure (null JWTs), in milliseconds (ms)
`token_cache_capacity` | `100` | Max token cache size
`log_level` | `INFO` | Log4j Log Level
`request_timeout_ms` | `30000` | Request Timeout
`api_key_sources` | `PARAM,HEADER,PATH` | Defines the sources (order of precendence) of the API key.
| Property | Default | Description |
|---------------------------|---------------------|---------------------------------------------------------------------------|
| `port` | `8081` | Server port to listen on |
| `okapi_url` | *required* | Where to find Okapi (URL) |
| `secure_store` | `Ephemeral` | Type of secure store to use. Valid: `Ephemeral`, `AwsSsm`, `Vault` |
| `secure_store_props` | `NA` | Path to a properties file specifying secure store configuration |
| `token_cache_ttl_ms` | `3600000` | How long to cache JWTs, in milliseconds (ms) |
| `null_token_cache_ttl_ms` | `30000` | How long to cache login failure (null JWTs), in milliseconds (ms) |
| `token_cache_capacity` | `100` | Max token cache size |
| `log_level` | `INFO` | Log4j Log Level |
| `request_timeout_ms` | `30000` | Request Timeout |
| `ssl_enabled` | `false` | Set whether SSL/TLS is enabled for Vertx Http Server |
| `keystore_type` | `NA` | Set the key store type |
| `keystore_provider` | `NA` | Set the provider name of the key store |
| `keystore_path` | `NA` | Set the path to the key store file |
| `keystore_password` | `NA` | Set the password for the key store |
| `key_alias` | `NA` | Optional param that points to a specific key within the key store |
| `key_alias_password` | `NA` | Optional param that points to a password of `key_alias` if it protected |
| `api_key_sources` | `PARAM,HEADER,PATH` | Defines the sources (order of precendence) of the API key. |

## Additional information

Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/folio/edge/core/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ private Constants() {
public static final String SYS_REQUEST_TIMEOUT_MS = "request_timeout_ms";
public static final String SYS_API_KEY_SOURCES = "api_key_sources";
public static final String SYS_RESPONSE_COMPRESSION = "response_compression";
public static final String SYS_SSL_ENABLED = "ssl_enabled";
public static final String SYS_KEYSTORE_PATH = "keystore_path";
public static final String SYS_KEYSTORE_PASSWORD = "keystore_password";
public static final String SYS_KEYSTORE_TYPE = "keystore_type";
public static final String SYS_KEYSTORE_PROVIDER = "keystore_provider";
public static final String SYS_KEY_ALIAS = "key_alias";
public static final String SYS_KEY_ALIAS_PASSWORD = "key_alias_password";

// Property names
public static final String PROP_SECURE_STORE_TYPE = "secureStore.type";
Expand All @@ -38,6 +45,7 @@ private Constants() {
public static final int DEFAULT_TOKEN_CACHE_CAPACITY = 100;
public static final String DEFAULT_API_KEY_SOURCES = "PARAM,HEADER,PATH";
public static final boolean DEFAULT_RESPONSE_COMPRESSION = false;
public static final boolean DEFAULT_SSL_ENABLED = false;

// Headers
public static final String X_OKAPI_TENANT = "x-okapi-tenant";
Expand Down Expand Up @@ -97,6 +105,9 @@ private Constants() {
defaultMap.put(SYS_RESPONSE_COMPRESSION,
Boolean.parseBoolean(System.getProperty(SYS_RESPONSE_COMPRESSION,
Boolean.toString(DEFAULT_RESPONSE_COMPRESSION))));
defaultMap.put(SYS_SSL_ENABLED,
Boolean.parseBoolean(System.getProperty(SYS_SSL_ENABLED,
Boolean.toString(DEFAULT_SSL_ENABLED))));
defaultMap.put(SYS_SECURE_STORE_PROP_FILE,
System.getProperty(SYS_SECURE_STORE_PROP_FILE));
defaultMap.put(SYS_OKAPI_URL,
Expand Down
72 changes: 59 additions & 13 deletions src/main/java/org/folio/edge/core/EdgeVerticleHttp.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package org.folio.edge.core;

import static org.folio.edge.core.Constants.SYS_KEYSTORE_PASSWORD;
import static org.folio.edge.core.Constants.SYS_KEYSTORE_PATH;
import static org.folio.edge.core.Constants.SYS_KEYSTORE_PROVIDER;
import static org.folio.edge.core.Constants.SYS_KEYSTORE_TYPE;
import static org.folio.edge.core.Constants.SYS_KEY_ALIAS;
import static org.folio.edge.core.Constants.SYS_KEY_ALIAS_PASSWORD;
import static org.folio.edge.core.Constants.SYS_PORT;
import static org.folio.edge.core.Constants.SYS_RESPONSE_COMPRESSION;
import static org.folio.edge.core.Constants.SYS_SSL_ENABLED;
import static org.folio.edge.core.Constants.TEXT_PLAIN;

import com.amazonaws.util.StringUtils;
import io.vertx.core.Future;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpHeaders;
import io.vertx.core.http.HttpServer;
import io.vertx.core.net.KeyStoreOptions;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;

Expand All @@ -25,23 +34,27 @@ public abstract class EdgeVerticleHttp extends EdgeVerticleCore {
@Override
public void start(Promise<Void> promise) {
Future.<Void>future(p -> super.start(p)).<Void>compose(res -> {
final int port = config().getInteger(SYS_PORT);
logger.info("Using port: {}", port);
final int port = config().getInteger(SYS_PORT);
logger.info("Using port: {}", port);

// initialize response compression
final boolean isCompressionSupported = config().getBoolean(SYS_RESPONSE_COMPRESSION);
logger.info("Response compression enabled: {}", isCompressionSupported);
final HttpServerOptions serverOptions = new HttpServerOptions();
serverOptions.setCompressionSupported(isCompressionSupported);
final HttpServerOptions serverOptions = new HttpServerOptions();

final HttpServer server = getVertx().createHttpServer(serverOptions);
// initialize response compression
final boolean isCompressionSupported = config().getBoolean(SYS_RESPONSE_COMPRESSION);
logger.info("Response compression enabled: {}", isCompressionSupported);
serverOptions.setCompressionSupported(isCompressionSupported);

final Router router = defineRoutes();
// initialize tls/ssl configuration for web server
configureSslIfEnabled(serverOptions);

return server.requestHandler(router)
.listen(port)
.mapEmpty();
}).onComplete(promise);
final HttpServer server = getVertx().createHttpServer(serverOptions);

final Router router = defineRoutes();

return server.requestHandler(router)
.listen(port)
.mapEmpty();
}).onComplete(promise);
}

public abstract Router defineRoutes();
Expand All @@ -52,4 +65,37 @@ protected void handleHealthCheck(RoutingContext ctx) {
.putHeader(HttpHeaders.CONTENT_TYPE, TEXT_PLAIN)
.end("\"OK\"");
}

private void configureSslIfEnabled(HttpServerOptions serverOptions) {
final boolean isSslEnabled = config().getBoolean(SYS_SSL_ENABLED);
if (isSslEnabled) {
logger.info("Enabling Vertx Http Server with TLS/SSL configuration...");
serverOptions.setSsl(true);
String keystoreType = config().getString(SYS_KEYSTORE_TYPE);
if (StringUtils.isNullOrEmpty(keystoreType)) {
throw new IllegalStateException("'keystore_type' system param must be specified when ssl_enabled = true");
}
logger.info("Using {} keystore type for SSL/TLS", keystoreType);
String keystoreProvider = config().getString(SYS_KEYSTORE_PROVIDER);
logger.info("Using {} keystore provider for SSL/TLS", keystoreProvider);
String keystorePath = config().getString(SYS_KEYSTORE_PATH);
if (StringUtils.isNullOrEmpty(keystorePath)) {
throw new IllegalStateException("'keystore_path' system param must be specified when ssl_enabled = true");
}
String keystorePassword = config().getString(SYS_KEYSTORE_PASSWORD);
if (StringUtils.isNullOrEmpty(keystorePassword)) {
throw new IllegalStateException("'keystore_password' system param must be specified when ssl_enabled = true");
}
String keyAlias = config().getString(SYS_KEY_ALIAS);
String keyAliasPassword = config().getString(SYS_KEY_ALIAS_PASSWORD);

serverOptions.setKeyCertOptions(new KeyStoreOptions()
.setType(keystoreType)
.setProvider(keystoreProvider)
.setPath(keystorePath)
.setPassword(keystorePassword)
.setAlias(keyAlias)
.setAliasPassword(keyAliasPassword));
}
}
}
22 changes: 19 additions & 3 deletions src/main/java/org/folio/edge/core/utils/OkapiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.amazonaws.util.StringUtils;
import io.vertx.core.Future;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.KeyCertOptions;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
Expand Down Expand Up @@ -67,9 +68,18 @@ protected OkapiClient(Vertx vertx, String okapiURL, String tenant, int timeout)
this.reqTimeout = timeout;
this.okapiURL = okapiURL;
this.tenant = tenant;
WebClientOptions options = new WebClientOptions().setTryUseCompression(true)
.setIdleTimeoutUnit(TimeUnit.MILLISECONDS).setIdleTimeout(timeout)
.setConnectTimeout(timeout);
WebClientOptions options = initDefaultWebClientOptions(timeout);
client = WebClientFactory.getWebClient(vertx, options);
initDefaultHeaders();
}

protected OkapiClient(Vertx vertx, String okapiURL, String tenant, int timeout, KeyCertOptions keyCertOptions) {
this.vertx = vertx;
this.reqTimeout = timeout;
this.okapiURL = okapiURL;
this.tenant = tenant;
WebClientOptions options = initDefaultWebClientOptions(timeout)
.setKeyCertOptions(keyCertOptions);
client = WebClientFactory.getWebClient(vertx, options);
initDefaultHeaders();
}
Expand All @@ -89,6 +99,12 @@ protected void initDefaultHeaders() {
defaultHeaders.add(X_OKAPI_TENANT, tenant);
}

protected WebClientOptions initDefaultWebClientOptions(int timeout) {
return new WebClientOptions().setTryUseCompression(true)
.setIdleTimeoutUnit(TimeUnit.MILLISECONDS).setIdleTimeout(timeout)
.setConnectTimeout(timeout);
}

public CompletableFuture<String> login(String username, String password) {
return doLogin(username, password, null).toCompletionStage().toCompletableFuture();
}
Expand Down
28 changes: 27 additions & 1 deletion src/main/java/org/folio/edge/core/utils/OkapiClientFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.concurrent.ConcurrentHashMap;

import io.vertx.core.Vertx;
import io.vertx.core.net.KeyCertOptions;
import io.vertx.core.net.KeyStoreOptions;

public class OkapiClientFactory {

Expand All @@ -12,14 +14,38 @@ public class OkapiClientFactory {
public final String okapiURL;
public final Vertx vertx;
public final int reqTimeoutMs;
private KeyCertOptions keyCertOptions;

public OkapiClientFactory(Vertx vertx, String okapiURL, int reqTimeoutMs) {
this.vertx = vertx;
this.okapiURL = okapiURL;
this.reqTimeoutMs = reqTimeoutMs;
}

public OkapiClientFactory(Vertx vertx,
String okapiURL,
int reqTimeoutMs,
String keystoreType,
String keystoreProvider,
String keystorePath,
String keystorePassword,
String keyAlias,
String keyAliasPassword) {
this(vertx, okapiURL, reqTimeoutMs);
this.keyCertOptions = new KeyStoreOptions()
.setType(keystoreType)
.setProvider(keystoreProvider)
.setPath(keystorePath)
.setPassword(keystorePassword)
.setAlias(keyAlias)
.setAliasPassword(keyAliasPassword);
}

public OkapiClient getOkapiClient(String tenant) {
return cache.computeIfAbsent(tenant, t -> new OkapiClient(vertx, okapiURL, t, reqTimeoutMs));
if (keyCertOptions == null) {
return cache.computeIfAbsent(tenant, t -> new OkapiClient(vertx, okapiURL, t, reqTimeoutMs));
} else {
return cache.computeIfAbsent(tenant, t -> new OkapiClient(vertx, okapiURL, t, reqTimeoutMs, keyCertOptions));
}
}
}
4 changes: 3 additions & 1 deletion src/test/java/org/folio/edge/core/EdgeVerticleCoreTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import io.vertx.core.json.JsonObject;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;

import java.io.File;
import java.io.FileNotFoundException;
import java.net.ConnectException;
import org.folio.edge.core.utils.test.TestUtils;
Expand Down Expand Up @@ -71,7 +73,7 @@ public void testGetPropertiesFailure1(TestContext context) {
.put(SYS_SECURE_STORE_PROP_FILE, "sx://foo.com");
final DeploymentOptions opt = new DeploymentOptions().setConfig(jo);
vertx.deployVerticle(new EdgeVerticleCore(), opt).onComplete(context.asyncAssertFailure(e -> {
assertThat(e.getMessage(), startsWith("Failed to load secure store properties: sx:/foo.com"));
assertThat(e.getMessage(), startsWith(String.format("Failed to load secure store properties: sx:%sfoo.com", File.separator)));
assertThat(e.getCause(), instanceOf(FileNotFoundException.class));
}));
}
Expand Down
Loading

0 comments on commit e35fed0

Please sign in to comment.