Skip to content

Commit

Permalink
Merge pull request #44 from bancolombia/feature/vault-backend
Browse files Browse the repository at this point in the history
feat: added vault client for secrets
  • Loading branch information
juancgalvis authored Jan 2, 2024
2 parents 693b48c + e8d2787 commit 74479df
Show file tree
Hide file tree
Showing 24 changed files with 1,402 additions and 3 deletions.
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This library will help you to decouple your application of your secrets provider
- AWS Secrets Manager Async (Non blocking flows)
- AWS Parameter Store Sync
- AWS Parameter Store Async (Non blocking flows)
- Vault Async (Non blocking flows)
- File Secrets (E.g Kubernetes Secrets )
- Environment System Secrets (E.g Kubernetes Secrets )

Expand Down Expand Up @@ -127,6 +128,67 @@ connector.getSecret("pruebaLibreria", DefineYourModel.class)
})
```

## Vault Async (Compatible with Reactor)

```java
dependencies {
// Reactor Core is required!
implementation group: 'io.projectreactor', name: 'reactor-core', version: '3.4.17'
// vault-async dependency
implementation 'com.github.bancolombia:vault-async:<version-here>'
}
```

Define your configuration:
```java
// Simplified Config
VaultSecretManagerConfigurator configurator = VaultSecretManagerConfigurator.builder()
.withProperties(VaultSecretsManagerProperties.builder()
.host("localhost")
.port(8200)
.ssl(false)
.roleId("65903d42-6dd4-2aa3-6a61-xxxxxxxxxx") // for authentication with vault
.secretId("0cce6d0b-e756-c12e-9729-xxxxxxxxx") // for authentication with vault
.build())
.build();
```



##### Configurations

You can pass the following variables to VaultSecretsManagerProperties:

- **host**: host name or ip address of the vault server. The default value is localhost.
- **port**: port number of the vault server. The default value is 8200.
The next requests to the same secret will be resolved from the cache. The default value is 0 (no cache).
- **ssl**: Defines if the connection to the vault server is secure or not. The default value is false.
- **roleId** and **secretId**: credentials for authenticate with vault and obtain a Token.
- **token**: If you already have a token, you can pass it here. If you pass a token, the roleId and secretId
will be ignored.

For other configurations, you can use the `VaultSecretsManagerProperties` class.

Create the connector:
```java
GenericManagerAsync connector = configurator.getVaultClient();
```

Get the secret in String:
```java
connector.getSecret("my/secret/path")
.doOnNext(System.out::println);
// ... develop your async flow
```
Get the secret deserialized:
```java
connector.getSecret("my/database/credentials", DBCredentials.class)
.doOnNext(secret -> {
//... develop your async flow
})
```


## Parameter Store Sync
```java
dependencies {
Expand Down Expand Up @@ -224,6 +286,5 @@ Great !!:
### To Do

- New connectors for other services.
- Vault
- Key Vault Azure
- Improve our tests
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ private static class GsonUtilsHolder {
public <T> T stringToModel(String data, Class<T> model) {
return gson.fromJson(data, model);
}

public String modelToString(Object model) {
return gson.toJson(model);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package co.com.bancolombia.secretsmanager.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CacheProperties {

@Builder.Default
private int expireAfter = 600; //in seconds

@Builder.Default
private int maxSize = 100;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package co.com.bancolombia.secretsmanager.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpProperties {

@Builder.Default
private int connectionTimeout = 5; //in seconds
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package co.com.bancolombia.secretsmanager.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.io.File;

/**
* If you would like to use Vault's TLS Certificate auth backend for client side auth, then you need to provide
* either:
* 1) A JKS keystore containing your client-side certificate and private key, and optionally a password.
* 2) PEM files containing your client-side certificate and private key
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VaultKeyStoreProperties {

// JKS file containing a client certificate and private key.
private File keyStoreFile;
// Password for the JKS file or resource (optional).
private String keyStorePassword;

// Path to an X.509 certificate in unencrypted PEM format, using UTF-8 encoding.
private File clientPem;
// path to an RSA private key in unencrypted PEM format, using UTF-8 encoding.
private File clientKeyPem;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package co.com.bancolombia.secretsmanager.config;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder(toBuilder = true)
public class VaultSecretsManagerProperties {

@Builder.Default
private String host = "localhost";

@Builder.Default
private int port = 8200;

@Builder.Default
private boolean ssl = false;

@Builder.Default
private boolean tlsAuth = false;

@Builder.Default
private String baseApi = "/v1";

@Builder.Default
private String baseSecrets = "/kv/data/";

private String token;

private String roleId;

private String secretId;

@Builder.Default
private int engineVersion = 2;

private VaultTrustStoreProperties trustStoreProperties;

private VaultKeyStoreProperties keyStoreProperties;

@Builder.Default
private HttpProperties httpProperties = HttpProperties.builder().connectionTimeout(5).build();

@Builder.Default
private CacheProperties authCacheProperties = CacheProperties.builder().expireAfter(600).maxSize(10).build();

@Builder.Default
private CacheProperties secretsCacheProperties= CacheProperties.builder().expireAfter(600).maxSize(100).build();

public String buildUrl() {
return String.format("%s://%s:%d%s", ssl ? "https" : "http", host, port, baseApi);
}

public boolean roleCredentialsProvided() {
return roleId != null && !roleId.isEmpty() && secretId != null && !secretId.isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package co.com.bancolombia.secretsmanager.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.io.File;

/**
* Provide information about a JKS truststore, containing Vault's server-side certificate for basic SSL, using one of
* the following two options:
* 1) A JKS file with trustStoreJksFile attribute or
* 2) Pem files using pemFile attribute
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class VaultTrustStoreProperties {

/**
* JKS file containing Vault server cert(s) that can be trusted.
*/
private File trustStoreJksFile;

/**
* Supply the path to an X.509 certificate in unencrypted PEM format, using UTF-8 encoding
* (defaults to "VAULT_SSL_CERT" environment variable)
*/
private File pemFile;


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package co.com.bancolombia.secretsmanager.connector;

import co.com.bancolombia.secretsmanager.api.exceptions.SecretException;
import co.com.bancolombia.secretsmanager.commons.utils.GsonUtils;
import co.com.bancolombia.secretsmanager.config.VaultSecretsManagerProperties;
import co.com.bancolombia.secretsmanager.connector.auth.AuthResponse;
import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.gson.JsonObject;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

public class VaultAuthenticator {
private final Logger logger = Logger.getLogger("connector.VaultAuthenticator2");

private static final String BASE_AUTH_PATH = "/auth/approle/login";
private static final String CONTENT_TYPE_HEADER = "Content-Type";

private final HttpClient httpClient;
private final VaultSecretsManagerProperties properties;
private final AsyncCache<String, AuthResponse> cache;

public VaultAuthenticator(HttpClient httpClient, VaultSecretsManagerProperties properties) {
this.httpClient = httpClient;
this.properties = properties;
this.cache = initCache();
}

public Mono<AuthResponse> loginByAppRole() {
if (!properties.roleCredentialsProvided()) {
return Mono.defer(() ->
Mono.error(new SecretException("Could not perform action loginByAppRole. Role id or secret id is null, please check your configuration")));
} else {
return Mono.fromFuture(cache.get(properties.getRoleId(),
(s, executor) -> performLoginByAppRole().toFuture().toCompletableFuture()));
}
}

private Mono<AuthResponse> performLoginByAppRole() {
return Mono.fromSupplier(() ->
HttpRequest.newBuilder()
.uri(URI.create(this.properties.buildUrl() + BASE_AUTH_PATH))
.timeout(Duration.ofSeconds(5))
.header(CONTENT_TYPE_HEADER, "application/json")
.POST(HttpRequest.BodyPublishers.ofString(buildLoginBody()))
.build()
)
.flatMap(request -> Mono.fromFuture(httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())))
.flatMap(response -> response.statusCode() != 200 ?
Mono.error(() -> new SecretException("Error performing authentication with vault: " + response.body())) :
Mono.just(response))
.map(HttpResponse::body)
.map(body -> GsonUtils.getInstance().stringToModel(body, JsonObject.class))
.map(map -> map.getAsJsonObject("auth"))
.map(auth -> AuthResponse.builder()
.clientToken(auth.get("client_token").getAsString())
.accessor(auth.get("accessor").getAsString())
.leaseDuration(auth.get("lease_duration").getAsLong())
.renewable(auth.get("renewable").getAsBoolean())
.build())
.doOnSuccess(authResponse -> logger.info("Successfully authenticated by app-role with vault"))
.doOnError(err -> logger.severe("Error performing authentication with vault: " + err.getMessage()));
}

private String buildLoginBody() {
return "{\"role_id\":\"" + properties.getRoleId() + "\",\"secret_id\":\"" + properties.getSecretId() + "\"}";
}

private AsyncCache<String, AuthResponse> initCache() {
return Caffeine.newBuilder()
.maximumSize(properties.getAuthCacheProperties().getMaxSize())
.expireAfterWrite(properties.getAuthCacheProperties().getExpireAfter(), TimeUnit.SECONDS)
.buildAsync();
}

}
Loading

0 comments on commit 74479df

Please sign in to comment.