diff --git a/README.md b/README.md index 8f7a64c..52df473 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,16 @@ You can reload the cache on the system configuration page if you need a new secr Use these credentials just as other normal credentials in Jenkins. +There are multiple supported credential types, `string` is used by default. +To use a different type add a tag called `type` with one of the below values: + +- `string` - Secret text +- `username` - Username with password + - add a tag `username` for the username of the credential +- `sshUserPrivateKey` - SSH Private key + - add a tag `username` for the username of the credential + - (optional) add a tag `username-is-secret` and set it to true to hide the username in the build logs + Declarative Pipeline: ```groovy @@ -276,9 +286,13 @@ node { } ``` -It is also possible to use it as a 'Username with password' credentials, to do so, tag the secret with the desired `username`: +#### Username with password + ```bash -az keyvault secret set --vault-name my-vault --name github-pat --value my-pat --tags username=github-user +az keyvault secret set --vault-name my-vault \ + --name github-pat \ + --value my-pat \ + --tags username=github-user type=username ``` Scripted Pipeline: @@ -295,6 +309,50 @@ job('my example') { } ``` +#### SSH Username with private key + +```bash +az keyvault secret set --tags type=sshUserPrivateKey username=my-username \ + --vault-name my-vault \ + --name test-ssh \ + -f ~/.ssh/my-ssh-key +``` + +Scripted pipeline: + +```bash +# This is a docker image that can be used to test out this feature +docker run --rm -it --publish 2222:2222 \ + -e "PUBLIC_KEY=my-public-key" linuxserver/openssh-server +``` + +```groovy +node { + withCredentials([sshUserPrivateKey(credentialsId: "test-ssh", keyFileVariable: "my_ssh_key", usernameVariable: "my_username")]) { + sh 'ssh -i $my_ssh_key -p 2222 $my_username@localhost "uname -r"' + } +} +``` + +Declarative pipeline: + +```groovy +pipeline { + agent any + environment { + SSH_PRIVATE_KEY = credentials('test-ssh') + } + stages { + stage('Foo') { + steps { + sh 'ssh -i $SSH_PRIVATE_KEY -p 2222 $SSH_PRIVATE_KEY_USR@localhost "cat world"' + } + } + } +} +``` + + ### SecretSource The plugin allows the [Configuration as Code plugin](https://plugins.jenkins.io/configuration-as-code) to interpolate string secrets from Azure KeyVault. diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java index 413591a..abb210d 100644 --- a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/AzureCredentialsProvider.java @@ -33,6 +33,7 @@ import jenkins.model.Jenkins; import org.acegisecurity.Authentication; import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.sshuserprivatekey.AzureSSHUserPrivateKeyCredentials; import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.string.AzureSecretStringCredentials; import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.usernamepassword.AzureUsernamePasswordCredentials; @@ -70,7 +71,7 @@ public List getCredentials(@NonNull Class aClass, for (IdCredentials credential : credentials) { if (aClass.isAssignableFrom(credential.getClass())) { - // cast to keep generics happy even though we are assignable.. + // cast to keep generics happy even though we are assignable list.add(aClass.cast(credential)); } LOG.log(Level.FINEST, "getCredentials {0} does not match", credential.getId()); @@ -132,15 +133,28 @@ private static Collection fetchCredentials() { case "string": { AzureSecretStringCredentials cred = new AzureSecretStringCredentials(getSecretName(id), "", new KeyVaultSecretRetriever(client, id)); credentials.add(cred); + break; } - break; case "username": { AzureUsernamePasswordCredentials cred = new AzureUsernamePasswordCredentials( getSecretName(id), tags.get("username"), "", new KeyVaultSecretRetriever(client, id) ); credentials.add(cred); + break; + } + case "sshUserPrivateKey": { + String usernameSecretTag = tags.get("username-is-secret"); + boolean usernameSecret = false; + if (StringUtils.isNotBlank(usernameSecretTag)) { + usernameSecret = Boolean.parseBoolean(usernameSecretTag); + } + + AzureSSHUserPrivateKeyCredentials cred = new AzureSSHUserPrivateKeyCredentials( + getSecretName(id), "", tags.get("username"), usernameSecret, new KeyVaultSecretRetriever(client, id) + ); + credentials.add(cred); + break; } - break; default: { throw new IllegalStateException("Unknown type: " + type); } diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/sshuserprivatekey/AzureSSHUserPrivateKeyCredentials.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/sshuserprivatekey/AzureSSHUserPrivateKeyCredentials.java new file mode 100644 index 0000000..591d14e --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/sshuserprivatekey/AzureSSHUserPrivateKeyCredentials.java @@ -0,0 +1,95 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.sshuserprivatekey; + +import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; +import edu.umd.cs.findbugs.annotations.NonNull; +import hudson.Extension; +import hudson.util.Secret; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; +import org.jenkinsci.plugins.azurekeyvaultplugin.AzureCredentialsProvider; +import org.jenkinsci.plugins.plaincredentials.impl.Messages; +import org.jvnet.localizer.ResourceBundleHolder; + +public class AzureSSHUserPrivateKeyCredentials extends BaseStandardCredentials implements SSHUserPrivateKey { + + private final String username; + private final boolean usernameSecret; + private final Supplier value; + + public AzureSSHUserPrivateKeyCredentials( + String id, + String description, + String username, + boolean usernameSecret, + Supplier privateKey + ) { + super(id, description); + this.username = username; + this.usernameSecret = usernameSecret; + this.value = privateKey; + } + + public Secret getSecretValue() { + return value.get(); + } + + @NonNull + @Override + public String getPrivateKey() { + String key = Secret.toString(value.get()); + + return appendNewLineIfMissing(key); + } + + @Override + public Secret getPassphrase() { + return null; + } + + @NonNull + @Override + public List getPrivateKeys() { + String privateKeys = Secret.toString(value.get()); + List keys = StringUtils.isBlank(privateKeys) ? Collections.emptyList() : Arrays.asList(StringUtils.split(privateKeys, "\f")); + + return keys.stream() + .map(AzureSSHUserPrivateKeyCredentials::appendNewLineIfMissing) + .collect(Collectors.toList()); + } + + private static String appendNewLineIfMissing(String key) { + return key.endsWith("\n") ? key : key + "\n"; + } + + @NonNull + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isUsernameSecret() { + return usernameSecret; + } + + @Extension + @SuppressWarnings("unused") + public static class DescriptorImpl extends BaseStandardCredentialsDescriptor { + @Override + @NonNull + public String getDisplayName() { + return ResourceBundleHolder.get(Messages.class).format("StringCredentialsImpl.secret_text"); + } + + @Override + public boolean isApplicable(CredentialsProvider provider) { + return provider instanceof AzureCredentialsProvider; + } + } +} diff --git a/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/sshuserprivatekey/AzureSSHUserPrivateKeyCredentialsSnapshotTaker.java b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/sshuserprivatekey/AzureSSHUserPrivateKeyCredentialsSnapshotTaker.java new file mode 100644 index 0000000..080332c --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/azurekeyvaultplugin/credentials/sshuserprivatekey/AzureSSHUserPrivateKeyCredentialsSnapshotTaker.java @@ -0,0 +1,33 @@ +package org.jenkinsci.plugins.azurekeyvaultplugin.credentials.sshuserprivatekey; + +import com.cloudbees.plugins.credentials.CredentialsSnapshotTaker; +import hudson.Extension; +import hudson.util.Secret; +import org.jenkinsci.plugins.azurekeyvaultplugin.credentials.Snapshot; + +@Extension +@SuppressWarnings("unused") +public class AzureSSHUserPrivateKeyCredentialsSnapshotTaker extends CredentialsSnapshotTaker { + @Override + public Class type() { + return AzureSSHUserPrivateKeyCredentials.class; + } + + @Override + public AzureSSHUserPrivateKeyCredentials snapshot(AzureSSHUserPrivateKeyCredentials credential) { + SecretSnapshot secretSnapshot = new SecretSnapshot(credential.getSecretValue()); + return new AzureSSHUserPrivateKeyCredentials( + credential.getId(), + credential.getDescription(), + credential.getUsername(), + credential.isUsernameSecret(), + secretSnapshot + ); + } + + private static class SecretSnapshot extends Snapshot { + SecretSnapshot(Secret value) { + super(value); + } + } +}