Skip to content

Commit

Permalink
Beta: Secondary IAM roles (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskilding authored Jul 23, 2020
1 parent cebb3cc commit bdf2539
Show file tree
Hide file tree
Showing 42 changed files with 933 additions and 114 deletions.
3 changes: 1 addition & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Access credentials from AWS Secrets Manager in your Jenkins jobs.
## Contents

- [Authentication](authentication/index.md)
- [Beta Features](beta/index.md)
- [Caching](caching/index.md)
- [Filters](filters/index.md)
- [Networking](networking/index.md)
Expand All @@ -22,7 +23,6 @@ Access credentials from AWS Secrets Manager in your Jenkins jobs.
- Read-only view of Secrets Manager.
- `CredentialsProvider` and `SecretSource` API support.
- Credential metadata caching (duration: 5 minutes).
- [Cross-account](http://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html) Secrets Manager support with IAM roles.

## Setup

Expand Down Expand Up @@ -356,4 +356,3 @@ In your IDE:
3. Start Moto: `mvn docker:build docker:start`.
4. Run tests.
5. Stop Moto: `mvn docker:stop`.

19 changes: 19 additions & 0 deletions docs/beta/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Beta

The plugin contains the following beta features:

- [Secondary IAM Roles](roles/index.md)

## Warnings

- Beta features are not officially supported.
- Beta features may be added, changed, or removed without warning.
- Beta features may break Jenkins.
- Beta feature configuration must be manually updated when the config schema changes.

## Recommendations

- Test beta features on a non-production Jenkins.
- Read the plugin release notes before upgrading.
- Use Jenkins Configuration As Code (CasC). (CasC can warn you when the configuration schema changes.)
- Report bugs and feedback on the Jenkins issue tracker.
51 changes: 51 additions & 0 deletions docs/beta/roles/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## Roles

The plugin can access more secrets using secondary IAM roles.

The most common use case is to access secrets in other accounts using [cross-account roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html). In this setup, Jenkins accesses secrets in its own account using its (implicit) primary role, and is assigned a secondary role for each other account that it should read secrets from.

Secrets in different accounts may have the same name. To allow them to co-exist within Jenkins, credentials from primary and secondary roles use different secret attributes for their IDs:

<table>
<thead>
<tr>
<td>Role</td>
<td>Credential ID</td>
</tr>
</thead>
<tbody>
<tr>
<td>Primary</td>
<td>Secret Name</td>
</tr>
<tr>
<td>Secondary</td>
<td>Secret ARN</td>
</tr>
</tbody>
</table>

## Setup

For each secondary role:

1. Create the role and associated policies in AWS.
2. Test that Jenkins can assume the role and retrieve secrets.
3. Add the role ARN to the `roles` list in the plugin configuration.

```yaml
unclassified:
awsCredentialsProvider:
beta:
roles:
- arn:aws:iam::111111111111:role/foo
- arn:aws:iam::222222222222:role/bar
```
## Considerations
**Do not add more roles than necessary.** Each additional role necessitates another set of HTTP requests to retrieve secrets. This increases the time to populate the credential list. It also increases the risk of service degradation, as any of those requests could fail.
## Limitations
The primary role cannot currently be turned off. This might be a problem if you use the primary role to access a gateway account, and the secondary roles to access your 'real' accounts, and don't want Jenkins to use Secrets Manager in the gateway account.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.jenkins.plugins.credentials.secretsmanager;

public abstract class AssumeRoleDefaults {

public static final int SESSION_DURATION_SECONDS = 900;
public static final String SESSION_NAME = "io.jenkins.plugins.aws-secrets-manager-credentials-provider";

private AssumeRoleDefaults() {

}
}
Original file line number Diff line number Diff line change
@@ -1,48 +1,30 @@
package io.jenkins.plugins.credentials.secretsmanager;

import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.google.common.base.Suppliers;

import com.amazonaws.SdkBaseException;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.secretsmanager.AWSSecretsManager;
import com.amazonaws.services.secretsmanager.AWSSecretsManagerClient;
import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder;
import com.amazonaws.services.secretsmanager.model.SecretListEntry;
import com.amazonaws.services.secretsmanager.model.Tag;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.common.IdCredentials;

import io.jenkins.plugins.credentials.secretsmanager.factory.CredentialsFactory;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.google.common.base.Suppliers;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.security.ACL;
import io.jenkins.plugins.credentials.secretsmanager.supplier.CredentialsSupplier;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;

import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nonnull;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.security.ACL;
import io.jenkins.plugins.credentials.secretsmanager.config.EndpointConfiguration;
import io.jenkins.plugins.credentials.secretsmanager.config.Filters;
import io.jenkins.plugins.credentials.secretsmanager.config.PluginConfiguration;
import jenkins.model.Jenkins;

@Extension
public class AwsCredentialsProvider extends CredentialsProvider {
Expand All @@ -51,16 +33,16 @@ public class AwsCredentialsProvider extends CredentialsProvider {

private final AwsCredentialsStore store = new AwsCredentialsStore(this);

private final Supplier<Collection<IdCredentials>> credentialsSupplier =
memoizeWithExpiration(AwsCredentialsProvider::fetchCredentials, Duration.ofMinutes(5));
private final Supplier<Collection<StandardCredentials>> credentialsSupplier =
memoizeWithExpiration(CredentialsSupplier.standard(), Duration.ofMinutes(5));

@Override
@NonNull
public <C extends Credentials> List<C> getCredentials(@Nonnull Class<C> type,
ItemGroup itemGroup,
Authentication authentication) {
if (ACL.SYSTEM.equals(authentication)) {
Collection<IdCredentials> allCredentials = Collections.emptyList();
Collection<StandardCredentials> allCredentials = Collections.emptyList();
try {
allCredentials = credentialsSupplier.get();
} catch (SdkBaseException e) {
Expand Down Expand Up @@ -90,56 +72,4 @@ public String getIconClassName() {
private static <T> Supplier<T> memoizeWithExpiration(Supplier<T> base, Duration duration) {
return Suppliers.memoizeWithExpiration(base::get, duration.toMillis(), TimeUnit.MILLISECONDS)::get;
}

private static Collection<IdCredentials> fetchCredentials() {
LOG.log(Level.FINE,"Retrieve secrets from AWS Secrets Manager");

final PluginConfiguration config = PluginConfiguration.getInstance();
final EndpointConfiguration ec = config.getEndpointConfiguration();
final Filters filters = config.getFilters();

final AWSSecretsManagerClientBuilder builder = AWSSecretsManagerClient.builder();
if (ec == null || (ec.getServiceEndpoint() == null || ec.getSigningRegion() == null)) {
LOG.log(Level.CONFIG, "Default Endpoint Configuration");
} else {
LOG.log(Level.CONFIG, "Custom Endpoint Configuration: {0}", ec);
final AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(ec.getServiceEndpoint(), ec.getSigningRegion());
builder.setEndpointConfiguration(endpointConfiguration);
}
final AWSSecretsManager client = builder.build();

final Predicate<SecretListEntry> secretFilter;
if (filters != null && filters.getTag() != null) {
final Tag filterTag = new Tag().withKey(filters.getTag().getKey()).withValue(filters.getTag().getValue());
secretFilter = s -> Optional.ofNullable(s.getTags()).orElse(Collections.emptyList()).contains(filterTag);
} else {
secretFilter = s -> true;
}

final Map<String, IdCredentials> credentials = new ListSecretsOperation(client).get().stream()
.filter(secretFilter)
.flatMap(s -> {
final String name = s.getName();
final String description = Optional.ofNullable(s.getDescription()).orElse("");
final Map<String, String> tags = Optional.ofNullable(s.getTags()).orElse(Collections.emptyList()).stream()
.filter(tag -> (tag.getKey() != null) && (tag.getValue() != null))
.collect(Collectors.toMap(Tag::getKey, Tag::getValue));
final Optional<StandardCredentials> cred = CredentialsFactory.create(name, description, tags, client);
return optionalToStream(cred);
})
.collect(Collectors.toMap(IdCredentials::getId, cred -> cred));

return credentials.values();
}

/**
* Polyfill for Java 9 Optional::stream.
*
* @param thing the optional
* @param <T> the type
* @return the stream
*/
private static <T> Stream<T> optionalToStream(Optional<T> thing) {
return thing.map(Stream::of).orElse(Stream.empty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.jenkins.plugins.credentials.secretsmanager.config;

import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import io.jenkins.plugins.credentials.secretsmanager.Messages;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import javax.annotation.Nonnull;
import java.io.Serializable;

public class ARN extends AbstractDescribableImpl<ARN> implements Serializable {

private static final long serialVersionUID = 1L;

private String value;

@DataBoundConstructor
public ARN(String value) {
this.value = value;
}

public String getValue() {
return value;
}

@DataBoundSetter
public void setValue(String value) {
this.value = value;
}

@Extension
@Symbol("arn")
@SuppressWarnings("unused")
public static class DescriptorImpl extends Descriptor<ARN> {

@Override
@Nonnull
public String getDisplayName() {
return Messages.arn();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.jenkins.plugins.credentials.secretsmanager.config;

import hudson.Extension;
import hudson.model.AbstractDescribableImpl;
import hudson.model.Descriptor;
import io.jenkins.plugins.credentials.secretsmanager.Messages;
import org.jenkinsci.Symbol;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;

import javax.annotation.Nonnull;
import java.io.Serializable;

/**
* Configuration for beta features.
*/
public class Beta extends AbstractDescribableImpl<Beta> implements Serializable {

private static final long serialVersionUID = 1L;

/**
* The IAM role ARNs to assume. For multi-account secrets retrieval.
*/
private Roles roles;

@DataBoundConstructor
public Beta(Roles roles) {
this.roles = roles;
}

public Roles getRoles() {
return roles;
}

@DataBoundSetter
public void setRoles(Roles roles) {
this.roles = roles;
}

@Extension
@Symbol("beta")
@SuppressWarnings("unused")
public static class DescriptorImpl extends Descriptor<Beta> {
@Override
@Nonnull
public String getDisplayName() {
return Messages.beta();
}
}
}
Loading

0 comments on commit bdf2539

Please sign in to comment.