Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

faet: added support for k8s auth method #45

Merged
merged 1 commit into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading