Skip to content

Commit 16bdcbe

Browse files
authored
Key based authentication (#414)
1 parent 267830b commit 16bdcbe

22 files changed

Lines changed: 432 additions & 116 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ To use this plugin to create VM agents, first you need to have an Azure Service
6868
Jenkins will only build a project on this node when that project is restricted to certain nodes using a label expression, and that expression matches this node's name and/or labels.
6969
This allows an agent to be reserved for certain kinds of jobs.
7070
10. Select a built-in image, you can choose between Windows Server 2016 and Ubuntu 16.04 LTS. You can also choose to install some tools on the agent, including Git, Maven and Docker (JDK is always installed).
71-
11. Specify Admin Credentials (a username/password credentials), this is the username and password if you want to log into the agent VM.
71+
11. Specify Admin Credentials - This needs to be either an "SSH Username with private key" or "Username with password" credential
7272
12. Click Verify Template to make sure all your configurations are correct, then Save.
7373

7474
### Run Jenkins Jobs on Azure VM Agents

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@
7777
<groupId>org.jenkins-ci.plugins</groupId>
7878
<artifactId>jsch</artifactId>
7979
</dependency>
80+
<dependency>
81+
<groupId>com.sshtools</groupId>
82+
<artifactId>maverick-synergy-client</artifactId>
83+
<version>3.0.10</version>
84+
</dependency>
8085
<dependency>
8186
<groupId>org.mockito</groupId>
8287
<artifactId>mockito-core</artifactId>

src/main/java/com/microsoft/azure/vmagent/AzureVMAgent.java

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@
1616
package com.microsoft.azure.vmagent;
1717

1818
import com.azure.resourcemanager.compute.models.OperatingSystemTypes;
19-
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
2019
import com.microsoft.azure.util.AzureCredentials;
2120
import com.microsoft.azure.vmagent.remote.AzureVMAgentSSHLauncher;
22-
import com.microsoft.azure.vmagent.util.AzureUtil;
2321
import com.microsoft.azure.vmagent.util.CleanUpAction;
2422
import com.microsoft.azure.vmagent.util.Constants;
2523
import edu.umd.cs.findbugs.annotations.CheckForNull;
@@ -40,8 +38,15 @@
4038
import hudson.slaves.SlaveComputer;
4139
import hudson.util.FormValidation;
4240
import hudson.util.LogTaskListener;
41+
import java.io.ByteArrayInputStream;
42+
import java.io.IOException;
43+
import java.io.PrintStream;
44+
import java.nio.charset.StandardCharsets;
45+
import java.util.Collections;
46+
import java.util.List;
47+
import java.util.logging.Level;
48+
import java.util.logging.Logger;
4349
import jenkins.model.Jenkins;
44-
4550
import org.apache.commons.lang.StringUtils;
4651
import org.jenkinsci.plugins.cloudstats.CloudStatistics;
4752
import org.jenkinsci.plugins.cloudstats.ProvisioningActivity;
@@ -51,15 +56,6 @@
5156
import org.kohsuke.stapler.QueryParameter;
5257
import org.kohsuke.stapler.interceptor.RequirePOST;
5358

54-
import java.io.IOException;
55-
import java.io.PrintStream;
56-
import java.io.ByteArrayInputStream;
57-
import java.nio.charset.StandardCharsets;
58-
import java.util.Collections;
59-
import java.util.List;
60-
import java.util.logging.Level;
61-
import java.util.logging.Logger;
62-
6359
public class AzureVMAgent extends AbstractCloudSlave implements TrackedItem {
6460

6561
private static final long serialVersionUID = -760014706860995557L;
@@ -633,9 +629,6 @@ public synchronized void deprovision(Localizable reason) throws Exception {
633629
// Execute termination script
634630
// Make sure to change file permission for execute if needed.
635631

636-
// Grab the username/pass
637-
StandardUsernamePasswordCredentials creds = AzureUtil.getCredentials(vmCredentialsId);
638-
639632
if (isUnix) {
640633
command = "sh " + REMOTE_TERMINATE_FILE_NAME;
641634
} else {
@@ -647,8 +640,7 @@ public synchronized void deprovision(Localizable reason) throws Exception {
647640
command,
648641
terminateStream,
649642
isUnix,
650-
executeInitScriptAsRoot,
651-
creds.getPassword().getPlainText());
643+
executeInitScriptAsRoot);
652644
if (exitStatus != 0) {
653645
LOGGER.log(Level.SEVERE,
654646
"Terminate script failed: exit code={0} ", exitStatus);

src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@
2121
import com.azure.resourcemanager.compute.models.DiskSkuTypes;
2222
import com.azure.resourcemanager.storage.models.SkuName;
2323
import com.azure.resourcemanager.storage.models.StorageAccount;
24+
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
2425
import com.cloudbees.plugins.credentials.CredentialsProvider;
2526
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
27+
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
2628
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
27-
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
2829
import com.jcraft.jsch.OpenSSHConfig;
2930
import com.microsoft.azure.util.AzureBaseCredentials;
3031
import com.microsoft.azure.util.AzureCredentialUtil;
@@ -49,7 +50,6 @@
4950
import hudson.Util;
5051
import hudson.model.Describable;
5152
import hudson.model.Descriptor;
52-
import hudson.model.Item;
5353
import hudson.model.Label;
5454
import hudson.model.Node;
5555
import hudson.model.TaskListener;
@@ -65,7 +65,6 @@
6565
import java.security.NoSuchAlgorithmException;
6666
import java.util.ArrayList;
6767
import java.util.Base64;
68-
import java.util.Collections;
6968
import java.util.HashMap;
7069
import java.util.List;
7170
import java.util.Map;
@@ -80,11 +79,11 @@
8079
import org.kohsuke.accmod.Restricted;
8180
import org.kohsuke.accmod.restrictions.DoNotUse;
8281
import org.kohsuke.accmod.restrictions.NoExternalUse;
83-
import org.kohsuke.stapler.AncestorInPath;
8482
import org.kohsuke.stapler.DataBoundConstructor;
8583
import org.kohsuke.stapler.DataBoundSetter;
8684
import org.kohsuke.stapler.QueryParameter;
8785
import org.kohsuke.stapler.interceptor.RequirePOST;
86+
import org.kohsuke.stapler.verb.POST;
8887

8988
/**
9089
* This class defines the configuration of Azure instance templates.
@@ -1086,7 +1085,7 @@ public String getCredentialsId() {
10861085
return credentialsId;
10871086
}
10881087

1089-
public StandardUsernamePasswordCredentials getVMCredentials() throws AzureCloudException {
1088+
public StandardUsernameCredentials getVMCredentials() throws AzureCloudException {
10901089
return AzureUtil.getCredentials(credentialsId);
10911090
}
10921091

@@ -1444,16 +1443,19 @@ public ListBoxModel doFillVirtualMachineSizeItems(
14441443
return model;
14451444
}
14461445

1447-
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item owner) {
1448-
// when configuring the job, you only want those credentials that are available to ACL.SYSTEM selectable
1449-
// as we cannot select from a user's credentials unless they are the only user submitting the build
1450-
// (which we cannot assume) thus ACL.SYSTEM is correct here.
1451-
return new StandardListBoxModel().withAll(
1452-
CredentialsProvider.lookupCredentials(
1453-
StandardUsernamePasswordCredentials.class,
1454-
owner,
1455-
ACL.SYSTEM,
1456-
Collections.<DomainRequirement>emptyList()));
1446+
@POST
1447+
public ListBoxModel doFillCredentialsIdItems(@QueryParameter String credentialsId) {
1448+
StandardListBoxModel model = new StandardListBoxModel();
1449+
Jenkins context = Jenkins.get();
1450+
if (!context.hasPermission(CredentialsProvider.CREATE)
1451+
&& !context.hasPermission(CredentialsProvider.UPDATE)) {
1452+
return model.includeCurrentValue(credentialsId);
1453+
}
1454+
1455+
return model
1456+
.includeAs(ACL.SYSTEM, context, SSHUserPrivateKey.class)
1457+
.includeAs(ACL.SYSTEM, context, StandardUsernamePasswordCredentials.class)
1458+
.includeCurrentValue(credentialsId);
14571459
}
14581460

14591461
public ListBoxModel doFillOsTypeItems() throws IOException, ServletException {

src/main/java/com/microsoft/azure/vmagent/AzureVMCloud.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,7 @@ private void releaseLockForAgent(AzureVMAgent agent) {
10291029
}
10301030

10311031
public AzureResourceManager getAzureClient() {
1032-
if (azureClient == null) {
1032+
if (azureClient == null && credentialsId != null) {
10331033
this.azureClient = AzureResourceManagerCache.get(credentialsId);
10341034
}
10351035

src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
import com.azure.storage.blob.BlobUrlParts;
5050
import com.azure.storage.blob.models.BlobItem;
5151
import com.azure.storage.common.StorageSharedKeyCredential;
52+
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
53+
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
5254
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
5355
import com.fasterxml.jackson.databind.JsonNode;
5456
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -60,10 +62,12 @@
6062
import com.microsoft.azure.vmagent.exceptions.AzureCloudException;
6163
import com.microsoft.azure.vmagent.launcher.AzureComputerLauncher;
6264
import com.microsoft.azure.vmagent.launcher.AzureSSHLauncher;
63-
import com.microsoft.azure.vmagent.retry.NoRetryStrategy;
6465
import com.microsoft.azure.vmagent.util.*;
6566
import com.microsoft.jenkins.credentials.AzureResourceManagerCache;
67+
import com.sshtools.common.publickey.InvalidPassphraseException;
68+
import com.sshtools.common.ssh.SshException;
6669
import hudson.model.Descriptor.FormException;
70+
import hudson.util.Secret;
6771
import io.jenkins.plugins.azuresdk.HttpClientRetriever;
6872
import jenkins.model.Jenkins;
6973
import jenkins.slaves.JnlpAgentReceiver;
@@ -579,9 +583,8 @@ public AzureVMDeploymentInfo createDeployment(
579583

580584
putVariable(tmp, "vmSize", template.getVirtualMachineSize());
581585
// Grab the username/pass
582-
StandardUsernamePasswordCredentials creds = template.getVMCredentials();
586+
StandardUsernameCredentials creds = template.getVMCredentials();
583587

584-
putVariable(tmp, "adminUsername", creds.getUsername());
585588
putVariableIfNotBlank(tmp, "storageAccountName", storageAccountName);
586589
putVariableIfNotBlank(tmp, "storageAccountType", storageAccountType);
587590
putVariableIfNotBlank(tmp, "blobEndpointSuffix", blobEndpointSuffix);
@@ -618,8 +621,22 @@ public AzureVMDeploymentInfo createDeployment(
618621

619622
final ObjectNode parameters = MAPPER.createObjectNode();
620623

621-
defineParameter(tmp, "adminPassword", "secureString");
622-
putParameter(parameters, "adminPassword", creds.getPassword().getPlainText());
624+
defineParameter(tmp, "adminUsername", "string");
625+
putParameter(parameters, "adminUsername", creds.getUsername());
626+
627+
defineParameter(tmp, "authenticationType", "string");
628+
defineParameter(tmp, "adminPasswordOrKey", "secureString");
629+
if (creds instanceof StandardUsernamePasswordCredentials) {
630+
StandardUsernamePasswordCredentials passwordCredentials = (StandardUsernamePasswordCredentials) creds;
631+
putParameter(parameters, "adminPasswordOrKey", passwordCredentials.getPassword().getPlainText());
632+
putParameter(parameters, "authenticationType", "password");
633+
} else {
634+
SSHUserPrivateKey sshCredentials = (SSHUserPrivateKey) creds;
635+
String privateKey = sshCredentials.getPrivateKeys().get(0);
636+
String rsaPublicKey = KeyDecoder.getPublicKey(privateKey, Secret.toString(sshCredentials.getPassphrase()));
637+
putParameter(parameters, "adminPasswordOrKey", rsaPublicKey);
638+
putParameter(parameters, "authenticationType", "key");
639+
}
623640

624641
// Register the deployment for cleanup
625642
deploymentRegistrar.registerDeployment(
@@ -2014,18 +2031,23 @@ public List<String> verifyTemplate(
20142031
}
20152032

20162033
//verify password
2017-
String adminPassword = "";
2018-
try {
2019-
StandardUsernamePasswordCredentials creds = AzureUtil.getCredentials(credentialsId);
2020-
adminPassword = creds.getPassword().getPlainText();
2021-
} catch (AzureCloudException e) {
2022-
LOGGER.log(Level.SEVERE, "Could not load the VM credentials", e);
2023-
}
2034+
String adminPassword;
2035+
StandardUsernameCredentials creds = AzureUtil.getCredentials(credentialsId);
2036+
if (creds instanceof StandardUsernamePasswordCredentials) {
2037+
adminPassword = ((StandardUsernamePasswordCredentials) creds).getPassword().getPlainText();
20242038

2025-
validationResult = verifyAdminPassword(adminPassword);
2026-
addValidationResultIfFailed(validationResult, errors);
2027-
if (returnOnSingleError && !errors.isEmpty()) {
2028-
return errors;
2039+
validationResult = verifyAdminPassword(adminPassword);
2040+
addValidationResultIfFailed(validationResult, errors);
2041+
if (returnOnSingleError && !errors.isEmpty()) {
2042+
return errors;
2043+
}
2044+
} else {
2045+
SSHUserPrivateKey sshCredentials = (SSHUserPrivateKey) creds;
2046+
validationResult = verifySSHKey(sshCredentials.getPrivateKeys(), sshCredentials.getPassphrase());
2047+
addValidationResultIfFailed(validationResult, errors);
2048+
if (returnOnSingleError && !errors.isEmpty()) {
2049+
return errors;
2050+
}
20292051
}
20302052

20312053
//verify JVM Options
@@ -2394,6 +2416,26 @@ private static String verifyAdminPassword(String adminPassword) {
23942416
}
23952417
}
23962418

2419+
private static String verifySSHKey(List<String> sshKeys, Secret passphrase) throws RuntimeException {
2420+
if (sshKeys == null || sshKeys.isEmpty()) {
2421+
return Messages.AzureVMManagementServiceDelegate_SSH_Missing_Key();
2422+
}
2423+
2424+
if (sshKeys.size() > 1) {
2425+
return Messages.AzureVMManagementServiceDelegate_SSH_Multiple_Keys_Found();
2426+
}
2427+
2428+
String sshKey = sshKeys.get(0);
2429+
try {
2430+
KeyDecoder.getPublicKey(sshKey, Secret.toString(passphrase));
2431+
} catch (IOException | InvalidPassphraseException | SshException e) {
2432+
LOGGER.log(Level.INFO, "Failed to validate SSH key", e);
2433+
return Messages.AzureVMManagementServiceDelegate_SSH_Invalid_Key_Format();
2434+
}
2435+
2436+
return Constants.OP_SUCCESS;
2437+
}
2438+
23972439
private static String verifyJvmOptions(String jvmOptions) {
23982440
if (StringUtils.isBlank(jvmOptions) || AzureUtil.isValidJvmOption(jvmOptions)) {
23992441
return Constants.OP_SUCCESS;

0 commit comments

Comments
 (0)