Skip to content

Commit

Permalink
Merge pull request #45 from bancolombia/feature/vault-secrets-k8s
Browse files Browse the repository at this point in the history
faet: added support for k8s auth method
  • Loading branch information
gabheadz authored Jan 9, 2024
2 parents 74479df + d383c14 commit 3b41c1d
Show file tree
Hide file tree
Showing 15 changed files with 407 additions and 91 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ dependencies {

Define your configuration:
```java
// Simplified Config
// Example Config
VaultSecretManagerConfigurator configurator = VaultSecretManagerConfigurator.builder()
.withProperties(VaultSecretsManagerProperties.builder()
.host("localhost")
Expand All @@ -163,9 +163,16 @@ You can pass the following variables to VaultSecretsManagerProperties:
- **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.
- **token**: If you already have a token to interact with Vault API, you can pass it to the configurator.
No auth is performed.

Authentication with vault can be done in two ways with this library:

- **roleId** and **secretId**: If AppRole auth is enabled, you can pass the roleId and secretId to the configurator. The library will authenticate
with vault and obtain a token.
- **vaultRoleForK8sAuth**: If Kubernetes auth is enabled, you can pass here the vault role for which you would like to
receive a token in the namespace for your app. For more information please refer to
[Kubernetes Auth Method](https://developer.hashicorp.com/vault/docs/auth/kubernetes) documentation.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class VaultSecretsManagerProperties {

private String secretId;

private String vaultRoleForK8sAuth;

@Builder.Default
private int engineVersion = 2;

Expand All @@ -49,11 +51,25 @@ public class VaultSecretsManagerProperties {
@Builder.Default
private CacheProperties secretsCacheProperties= CacheProperties.builder().expireAfter(600).maxSize(100).build();

@Builder.Default
private String appRoleAuthPath = "/auth/approle/login";

@Builder.Default
private String k8sAuthPath = "/auth/kubernetes/login";

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

public boolean roleCredentialsProvided() {
public boolean isRoleCredentialsProvided() {
return roleId != null && !roleId.isEmpty() && secretId != null && !secretId.isEmpty();
}

public boolean isRoleNameForK8sProvided() {
return vaultRoleForK8sAuth != null && !vaultRoleForK8sAuth.isEmpty();
}

public boolean isTokenProvided() {
return token != null && !token.isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,155 @@
import co.com.bancolombia.secretsmanager.commons.utils.GsonUtils;
import co.com.bancolombia.secretsmanager.config.VaultSecretsManagerProperties;
import co.com.bancolombia.secretsmanager.connector.auth.AuthResponse;
import co.com.bancolombia.secretsmanager.connector.auth.K8sAuth;
import co.com.bancolombia.secretsmanager.connector.auth.K8sTokenReader;
import co.com.bancolombia.secretsmanager.connector.auth.RoleAuth;
import com.github.benmanes.caffeine.cache.AsyncCache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import reactor.core.publisher.Mono;

import java.lang.reflect.Type;
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.Map;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/**
* Class in charge of authenticating with vault
*/
public class VaultAuthenticator {
private final Logger logger = Logger.getLogger("connector.VaultAuthenticator2");

private static final String BASE_AUTH_PATH = "/auth/approle/login";
private final Logger logger = Logger.getLogger("connector.VaultAuthenticator");
private static final String CONTENT_TYPE_HEADER = "Content-Type";

private final HttpClient httpClient;
private final VaultSecretsManagerProperties properties;
private final AsyncCache<String, AuthResponse> cache;
private final K8sTokenReader k8sTokenReader;
private final Gson gson = new Gson();
private final Type mapType = new TypeToken<Map<String, String>>() {}.getType();

public VaultAuthenticator(HttpClient httpClient, VaultSecretsManagerProperties properties) {
public VaultAuthenticator(HttpClient httpClient,
VaultSecretsManagerProperties properties,
K8sTokenReader k8sTokenReader) {
this.httpClient = httpClient;
this.properties = properties;
this.k8sTokenReader = k8sTokenReader;
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()));
}
/**
* Performs the login process with vault. If a token is provided, it will be used. If not, it will try to log in
* with the role_id and secret_id. If not, it will try to log in with k8s.
* @return the authentication response with the client token.
*/
public Mono<AuthResponse> login() {
return useTokenIfProvided()
.switchIfEmpty(loginWithRoleId())
.switchIfEmpty(loginK8s())
.switchIfEmpty(Mono.defer(() ->
Mono.error(new SecretException("Could not perform login with vault. Please check your configuration"))))
.doOnSuccess(this::checkLeaseDurationAgainstCacheExpTime);
}

private Mono<AuthResponse> useTokenIfProvided() {
return Mono.just(properties.isTokenProvided())
.filter(c -> c)
.map(c -> AuthResponse.builder()
.clientToken(properties.getToken())
.build());
}

private Mono<AuthResponse> loginWithRoleId() {
return Mono.just(properties.isRoleCredentialsProvided())
.filter(c -> c)
.flatMap(c -> Mono.fromFuture(cache.get(properties.getRoleId(),
(s, executor) -> performLoginByRoleId().toFuture().toCompletableFuture())));
}

private Mono<AuthResponse> performLoginByAppRole() {
private Mono<AuthResponse> loginK8s() {
return Mono.just(properties.isRoleNameForK8sProvided())
.filter(c -> c)
.flatMap(c -> Mono.fromFuture(cache.get(properties.getVaultRoleForK8sAuth(),
(s, executor) -> performLoginWithK8s().toFuture().toCompletableFuture())));
}

private Mono<AuthResponse> performLoginByRoleId() {
return Mono.fromSupplier(() ->
HttpRequest.newBuilder()
.uri(URI.create(this.properties.buildUrl() + BASE_AUTH_PATH))
.uri(URI.create(this.properties.buildUrl() + properties.getAppRoleAuthPath()))
.timeout(Duration.ofSeconds(5))
.header(CONTENT_TYPE_HEADER, "application/json")
.POST(HttpRequest.BodyPublishers.ofString(buildLoginBody()))
.POST(HttpRequest.BodyPublishers.ofString(
gson.toJson(RoleAuth.builder()
.roleId(properties.getRoleId())
.secretId(properties.getSecretId())
.build())
))
.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()));
.flatMap(this::doCallAuthApi)
.doOnSuccess(authResponse ->
logger.info("Successfully authenticated via role_id with vault")
);
}

private Mono<AuthResponse> performLoginWithK8s() {
return k8sTokenReader.getKubernetesServiceAccountToken()
.flatMap(token -> Mono.fromSupplier(() ->
HttpRequest.newBuilder()
.uri(URI.create(this.properties.buildUrl() + properties.getK8sAuthPath()))
.timeout(Duration.ofSeconds(5))
.header(CONTENT_TYPE_HEADER, "application/json")
.POST(HttpRequest.BodyPublishers.ofString(
gson.toJson(K8sAuth.builder()
.jwt(token)
.role(properties.getVaultRoleForK8sAuth())
.build())
))
.build()
))
.flatMap(this::doCallAuthApi)
.doOnSuccess(authResponse ->
logger.info("Successfully authenticated via k8s with vault")
);
}

private String buildLoginBody() {
return "{\"role_id\":\"" + properties.getRoleId() + "\",\"secret_id\":\"" + properties.getSecretId() + "\"}";
private Mono<AuthResponse> doCallAuthApi(HttpRequest request) {
return Mono.fromFuture(httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()))
.flatMap(response -> response.statusCode() != 200 ?
Mono.error(() -> new SecretException("Error performing operation 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())
.metadata(gson.fromJson(auth.get("metadata").toString(), mapType))
.build())
.doOnError(err -> logger.severe("Error performing operation with vault: " + err.getMessage()));
}

private void checkLeaseDurationAgainstCacheExpTime(AuthResponse authResponse) {
if (!authResponse.isRenewable()) {
return;
}
var leaseInSeconds = authResponse.getLeaseDuration();
var cacheExpTime = properties.getAuthCacheProperties().getExpireAfter();
if (cacheExpTime > leaseInSeconds) {
logger.warning("The configured token cache expiration time is greater " +
"than the maximum lease duration of the token. Calling Vault operations using such token, " +
"will fail when the token expires. Adjust your cache expiration to be similar to vault's token" +
"lease duration!!!");
}
}

private AsyncCache<String, AuthResponse> initCache() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,29 @@
import co.com.bancolombia.secretsmanager.config.VaultKeyStoreProperties;
import co.com.bancolombia.secretsmanager.config.VaultSecretsManagerProperties;
import co.com.bancolombia.secretsmanager.config.VaultTrustStoreProperties;
import co.com.bancolombia.secretsmanager.connector.auth.K8sTokenReader;
import co.com.bancolombia.secretsmanager.connector.ssl.SslConfig;
import lombok.Builder;

import java.net.http.HttpClient;
import java.time.Duration;
import java.util.logging.Logger;

/**
* This class is in charge of configuring the VaultSecretsManagerConnector
*/
@Builder(setterPrefix = "with", toBuilder = true)
public class VaultSecretManagerConfigurator {

private static final Logger logger = Logger.getLogger("config.VaultSecretManagerConfigurator");
private final VaultSecretsManagerProperties properties;
private final K8sTokenReader k8sTokenReader;

/**
* This method is in charge of configuring the HttpClient
* @return HttpClient configured.
* @throws SecretException
*/
public HttpClient getHttpClient() throws SecretException {
HttpClient.Builder clientBuilder = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
Expand All @@ -27,14 +37,25 @@ public HttpClient getHttpClient() throws SecretException {
return clientBuilder.build();
}

/**
* This method is in charge of configuring the VaultAuthenticator
* @return the VaultAuthenticator configured.
* @throws SecretException
*/
public VaultAuthenticator getVaultAuthenticator() throws SecretException {
return new VaultAuthenticator(getHttpClient(), properties);
return new VaultAuthenticator(getHttpClient(), properties,
k8sTokenReader != null? k8sTokenReader : new K8sTokenReader());
}

/**
* This method is in charge of configuring the VaultSecretsManagerConnector
* @return the VaultSecretsManagerConnector configured.
* @throws SecretException
*/
public VaultSecretsManagerConnectorAsync getVaultClient() throws SecretException {
HttpClient httpClient = getHttpClient();
return new VaultSecretsManagerConnectorAsync(httpClient,
new VaultAuthenticator(httpClient, properties),
getVaultAuthenticator(),
properties);
}

Expand All @@ -56,7 +77,7 @@ private SslConfig setTrustConfiguration(SslConfig sslConfig,
} else if (trustStoreProperties.getPemFile() != null) {
sslConfig.pemFile(trustStoreProperties.getPemFile());
} else {
logger.warning("No trust store file or pem resource provided");
throw new SecretException("VaultTrustStoreProperties was set, but no trust store file or pem resource provided");
}
return sslConfig;
}
Expand All @@ -70,7 +91,7 @@ private SslConfig setKeystoreConfiguration(SslConfig sslConfig,
sslConfig.clientPemFile(keyStoreProperties.getClientPem());
sslConfig.clientKeyPemFile(keyStoreProperties.getClientKeyPem());
} else {
logger.warning("No key store file or pem resources provided");
throw new SecretException("VaultKeyStoreProperties was set, but no key store file or pem resources provided");
}
return sslConfig;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

/**
* Connector to Vault Secrets Manager for reading secrets asynchronously.
*/
public class VaultSecretsManagerConnectorAsync implements GenericManagerAsync {

private static final Logger logger = Logger.getLogger("connector.VaultSecretsManagerConnectorAsync");
Expand Down Expand Up @@ -71,14 +74,8 @@ private Mono<String> getSecretValue(String secretName) {
}

private Mono<String> getToken() {
return Mono.defer(() -> {
if (this.properties.getToken() != null) {
return Mono.just(this.properties.getToken());
} else {
return vaultAuthenticator.loginByAppRole()
.map(AuthResponse::getClientToken);
}
});
return vaultAuthenticator.login()
.map(AuthResponse::getClientToken);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Getter
@NoArgsConstructor
Expand All @@ -19,6 +20,7 @@ public class AuthResponse {
private List<String> policies = new ArrayList<>();
@Builder.Default
private List<String> tokenPolicies = new ArrayList<>();
private Map<String, String> metadata;
private long leaseDuration;
private boolean renewable;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package co.com.bancolombia.secretsmanager.connector.auth;

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

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
public class K8sAuth {
private String jwt;
private String role;
}
Loading

0 comments on commit 3b41c1d

Please sign in to comment.