Skip to content

Commit

Permalink
Add sshUserPrivateKey support to credentials provider (#142)
Browse files Browse the repository at this point in the history
Fixes #88
  • Loading branch information
timja authored Dec 7, 2022
1 parent 294e253 commit 711998f
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 5 deletions.
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -70,7 +71,7 @@ public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> 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());
Expand Down Expand Up @@ -132,15 +133,28 @@ private static Collection<IdCredentials> 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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Secret> value;

public AzureSSHUserPrivateKeyCredentials(
String id,
String description,
String username,
boolean usernameSecret,
Supplier<Secret> 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<String> getPrivateKeys() {
String privateKeys = Secret.toString(value.get());
List<String> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<AzureSSHUserPrivateKeyCredentials> {
@Override
public Class<AzureSSHUserPrivateKeyCredentials> 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<Secret> {
SecretSnapshot(Secret value) {
super(value);
}
}
}

0 comments on commit 711998f

Please sign in to comment.