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

Adds support for using job-specific policies #223

Merged
merged 6 commits into from
Nov 20, 2023
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
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 @@
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 @@
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 @@
}
}

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 @@
}
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 @@

private String prefixPath;

private String policies;

private Boolean disableChildPoliciesOverride;

private Integer timeout = DEFAULT_TIMEOUT;

@DataBoundConstructor
Expand All @@ -73,6 +77,8 @@
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 @@
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 @@
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
Loading