Skip to content

Commit

Permalink
Adds support for using job-specific policies (#223)
Browse files Browse the repository at this point in the history
* Fixes #214, adds support for separating job policies

* Add configuration to credentials to enable using limited policies

* Fix handling of TTL in child tokens

* Add ability to disable folders or jobs from overriding policies

* Use StringSubstitutor for templating policies

* Fix flaky test

---------

Co-authored-by: saville <[email protected]>
  • Loading branch information
bluesliverx and saville authored Nov 20, 2023
1 parent af8c162 commit f5d54b3
Show file tree
Hide file tree
Showing 24 changed files with 492 additions and 37 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ When registering the approle backend you can set a couple of different parameter
* many more

This is just a short introduction, please refer to [Hashicorp itself](https://www.vaultproject.io/docs/auth/approle.html) to get detailed information.

### Isolating policies for different jobs
It may be desirable to have jobs or folders with separate Vault policies allocated. This may be done
with the optional `policies` configuration option combined with authentication such as the AppRole
credential. The process is the following:
* The Jenkins job attempts to retrieve a secret from Vault
* The AppRole authentication is used to retrieve a new token (if the old one has not expired yet)
* The Vault plugin then uses the `policies` configuration value with job info to come up with a list of policies
* If this list is not empty, the AppRole token is used to retrieve a new token that only has the specified policies applied
* This token is then used for all Vault plugin operations in the job

The policies list may be templatized with values that can come from each job in order to customize
policies per job or folder. See the `policies` configuration help for more information on available
tokens to use in the configuration. The `Limit Token Policies` option must also be enabled on the
auth credential. Please note that the AppRole (or other authentication method) should have all policies
configured as `token_policies` and not `identity_policies`, as job-specific tokens inherit all
`identity_policies` automatically.

### What about other backends?
Hashicorp explicitly recommends the AppRole Backend for machine-to-machine authentication. Token based auth is mainly supported for backward compatibility.
Other backends that might make sense are the AWS EC2 backend, the Azure backend, and the Kubernetes backend. But we do not support these yet. Feel free to contribute!
Expand Down
44 changes: 43 additions & 1 deletion src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.io.PrintStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -35,13 +36,15 @@
import java.util.stream.Collectors;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.text.StringSubstitutor;

public class VaultAccessor implements Serializable {

private static final long serialVersionUID = 1L;

private VaultConfig config;
private VaultCredential credential;
private List<String> policies;
private int maxRetries = 0;
private int retryIntervalMilliseconds = 1000;

Expand All @@ -63,7 +66,7 @@ public VaultAccessor init() {
if (credential == null) {
vault = new Vault(config);
} else {
vault = credential.authorizeWithVault(config);
vault = credential.authorizeWithVault(config, policies);
}

vault.withRetries(maxRetries, retryIntervalMilliseconds);
Expand All @@ -89,6 +92,14 @@ public void setCredential(VaultCredential credential) {
this.credential = credential;
}

public List<String> getPolicies() {
return policies;

Check warning on line 96 in src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 96 is not covered by tests
}

public void setPolicies(List<String> policies) {
this.policies = policies;
}

public int getMaxRetries() {
return maxRetries;
}
Expand Down Expand Up @@ -130,6 +141,36 @@ public VaultResponse revoke(String leaseId) {
}
}

private static StringSubstitutor getPolicyTokenSubstitutor(EnvVars envVars) {
String jobName = envVars.get("JOB_NAME");
String jobBaseName = envVars.get("JOB_BASE_NAME");
String folder = "";
if (!jobName.equals(jobBaseName) && jobName.contains("/")) {

Check warning on line 148 in src/main/java/com/datapipe/jenkins/vault/VaultAccessor.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 148 is only partially covered, one branch is missing
String[] jobElements = jobName.split("/");
folder = Arrays.stream(jobElements)
.limit(jobElements.length - 1)
.collect(Collectors.joining("/"));
}
Map<String, String> valueMap = new HashMap<>();
valueMap.put("job_base_name", jobBaseName);
valueMap.put("job_name", jobName);
valueMap.put("job_name_us", jobName.replaceAll("/", "_"));
valueMap.put("job_folder", folder);
valueMap.put("job_folder_us", folder.replaceAll("/", "_"));
valueMap.put("node_name", envVars.get("NODE_NAME"));
return new StringSubstitutor(valueMap);
}

protected static List<String> generatePolicies(String policies, EnvVars envVars) {
if (StringUtils.isBlank(policies)) {
return null;
}
return Arrays.stream(getPolicyTokenSubstitutor(envVars).replace(policies).split("\n"))
.filter(StringUtils::isNotBlank)
.map(String::trim)
.collect(Collectors.toList());
}

public static Map<String, String> retrieveVaultSecrets(Run<?,?> run, PrintStream logger, EnvVars envVars, VaultAccessor vaultAccessor, VaultConfiguration initialConfiguration, List<VaultSecret> vaultSecrets) {
Map<String, String> overrides = new HashMap<>();

Expand All @@ -156,6 +197,7 @@ public static Map<String, String> retrieveVaultSecrets(Run<?,?> run, PrintStream
}
vaultAccessor.setConfig(vaultConfig);
vaultAccessor.setCredential(credential);
vaultAccessor.setPolicies(generatePolicies(config.getPolicies(), envVars));
vaultAccessor.setMaxRetries(config.getMaxRetries());
vaultAccessor.setRetryIntervalMilliseconds(config.getRetryIntervalMilliseconds());
vaultAccessor.init();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public class VaultConfiguration extends AbstractDescribableImpl<VaultConfigurati

private String prefixPath;

private String policies;

private Boolean disableChildPoliciesOverride;

private Integer timeout = DEFAULT_TIMEOUT;

@DataBoundConstructor
Expand All @@ -73,6 +77,8 @@ public VaultConfiguration(VaultConfiguration toCopy) {
this.engineVersion = toCopy.engineVersion;
this.vaultNamespace = toCopy.vaultNamespace;
this.prefixPath = toCopy.prefixPath;
this.policies = toCopy.policies;
this.disableChildPoliciesOverride = toCopy.disableChildPoliciesOverride;
this.timeout = toCopy.timeout;
}

Expand All @@ -99,6 +105,10 @@ public VaultConfiguration mergeWithParent(VaultConfiguration parent) {
if (StringUtils.isBlank(result.getPrefixPath())) {
result.setPrefixPath(parent.getPrefixPath());
}
if (StringUtils.isBlank(result.getPolicies()) ||
(parent.getDisableChildPoliciesOverride() != null && parent.getDisableChildPoliciesOverride())) {

Check warning on line 109 in src/main/java/com/datapipe/jenkins/vault/configuration/VaultConfiguration.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 109 is only partially covered, one branch is missing
result.setPolicies(parent.getPolicies());
}
if (result.timeout == null) {
result.setTimeout(parent.getTimeout());
}
Expand Down Expand Up @@ -183,6 +193,24 @@ public void setPrefixPath(String prefixPath) {
this.prefixPath = fixEmptyAndTrim(prefixPath);
}

public String getPolicies() {
return policies;
}

@DataBoundSetter
public void setPolicies(String policies) {
this.policies = fixEmptyAndTrim(policies);
}

public Boolean getDisableChildPoliciesOverride() {
return disableChildPoliciesOverride;
}

@DataBoundSetter
public void setDisableChildPoliciesOverride(Boolean disableChildPoliciesOverride) {
this.disableChildPoliciesOverride = disableChildPoliciesOverride;
}

public Integer getTimeout() {
return timeout;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public void setNamespace(String namespace) {
}

@Override
protected final String getToken(Vault vault) {
protected Auth getVaultAuth(@NonNull Vault vault) {
// set authentication namespace if configured for this credential.
// importantly, this will not effect the underlying VaultConfig namespace.
Auth auth = vault.auth();
Expand All @@ -57,7 +57,12 @@ protected final String getToken(Vault vault) {
auth.withNameSpace(null);
}
}
return getToken(auth);
return auth;
}

@Override
protected final String getToken(Vault vault) {
return getToken(getVaultAuth(vault));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.bettercloud.vault.VaultConfig;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials;
import java.util.List;

public abstract class AbstractVaultTokenCredential
extends BaseStandardCredentials implements VaultCredential {
Expand All @@ -15,7 +16,7 @@ protected AbstractVaultTokenCredential(CredentialsScope scope, String id, String
protected abstract String getToken(Vault vault);

@Override
public Vault authorizeWithVault(VaultConfig config) {
public Vault authorizeWithVault(VaultConfig config, List<String> policies) {
Vault vault = new Vault(config);
return new Vault(config.token(getToken(vault)));
}
Expand Down
Loading

0 comments on commit f5d54b3

Please sign in to comment.