Last active
March 27, 2023 15:59
-
-
Save slamdev/e5b7d535ad1ddcdab84e5030b7e1d819 to your computer and use it in GitHub Desktop.
vault versioning
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
org.springframework.boot.env.EnvironmentPostProcessor=\ | |
com.maersk.service.SecretLeaseContainerConfiguration |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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