Skip to content

Instantly share code, notes, and snippets.

@slamdev
Last active March 27, 2023 15:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save slamdev/e5b7d535ad1ddcdab84e5030b7e1d819 to your computer and use it in GitHub Desktop.
Save slamdev/e5b7d535ad1ddcdab84e5030b7e1d819 to your computer and use it in GitHub Desktop.
vault versioning
org.springframework.boot.env.EnvironmentPostProcessor=\
com.maersk.service.SecretLeaseContainerConfiguration
package com.example;
import org.apache.commons.logging.Log;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ReflectionUtils;
import org.springframework.vault.core.VaultOperations;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.core.VaultVersionedKeyValueTemplate;
import org.springframework.vault.core.lease.SecretLeaseContainer;
import org.springframework.vault.core.lease.domain.Lease;
import org.springframework.vault.core.lease.domain.RequestedSecret;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.Versioned;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* spring-cloud-starter-vault-config doesn't support vault versioning
* (<a href="https://github.com/spring-cloud/spring-cloud-vault/issues/247">github issue</a>)
* so we implement it ourselves by overriding the SecretLeaseContainer bean that is responsible for populating
* Vault property source; this allows to use the following format:
* <pre>
* spring.config.import=vault:///secret/anguishedazureworshipers/dev/heartfeltivoryhorses?v=1
* </pre>
* if the specified version is not found, the implementation falls back to the latest version
*/
public class SecretLeaseContainerConfiguration implements EnvironmentPostProcessor, Ordered {
private final ConfigurableBootstrapContext bootstrapContext;
private final Log logger;
private final DeferredLogFactory logFactory;
public SecretLeaseContainerConfiguration(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext) {
logger = logFactory.getLog(SecretLeaseContainerConfiguration.class);
this.logFactory = logFactory;
this.bootstrapContext = bootstrapContext;
}
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
logger.debug("registering custom SecretLeaseContainer");
bootstrapContext.register(SecretLeaseContainer.class, ctx -> {
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(2);
threadPoolTaskScheduler.setDaemon(true);
threadPoolTaskScheduler.setThreadNamePrefix("Spring-Cloud-Vault-");
VaultTemplate vaultOperations = ctx.get(VaultTemplate.class);
SecretLeaseContainer container = new VersionedSecretLeaseContainer(vaultOperations, threadPoolTaskScheduler, logFactory);
try {
container.afterPropertiesSet();
} catch (Exception e) {
ReflectionUtils.rethrowRuntimeException(e);
}
container.start();
return container;
});
bootstrapContext.addCloseListener(event -> {
GenericApplicationContext gac = (GenericApplicationContext) event.getApplicationContext();
gac.registerShutdownHook();
SecretLeaseContainer instance = event.getBootstrapContext().get(SecretLeaseContainer.class);
gac.registerBean("secretLeaseContainer", SecretLeaseContainer.class, () -> instance);
});
}
@Override
public int getOrder() {
// we should register SecretLeaseContainer before it is done in VaultConfigDataLoader
return ConfigDataEnvironmentPostProcessor.ORDER - 10;
}
private static class VersionedSecretLeaseContainer extends SecretLeaseContainer {
private static final Pattern VERSIONED_PATH_PATTERN = Pattern.compile("^([^/]+)/(.+)\\?v=(\\d)$");
private final VaultOperations operations;
private final Log logger;
public VersionedSecretLeaseContainer(VaultOperations operations, TaskScheduler taskScheduler, DeferredLogFactory logFactory) {
super(operations, taskScheduler);
this.operations = operations;
logger = logFactory.getLog(getClass());
}
@Nullable
protected VaultResponseSupport<Map<String, Object>> doGetSecrets(RequestedSecret requestedSecret) {
try {
Matcher matcher = VERSIONED_PATH_PATTERN.matcher(requestedSecret.getPath());
if (matcher.matches()) {
String backend = matcher.group(1);
String path = matcher.group(2);
int version = Integer.parseInt(matcher.group(3));
VaultVersionedKeyValueTemplate versionedVaultOps = new VaultVersionedKeyValueTemplate(operations, backend);
Versioned<Map<String, Object>> versionedResponse = versionedVaultOps.get(path, Versioned.Version.from(version));
if (versionedResponse != null) {
VaultResponse response = new VaultResponse();
response.setData(versionedResponse.getData());
response.setMetadata(response.getMetadata());
return response;
}
logger.warn(String.format("failed to read secret version %d from %s path; failing back to the latest version", version, path));
}
logger.warn(String.format("failed to detect version for %s secret path; failing back to the latest version", requestedSecret.getPath()));
} catch (RuntimeException e) {
onError(requestedSecret, Lease.none(), e);
return null;
}
return super.doGetSecrets(requestedSecret);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment