Skip to content

Commit

Permalink
Key based authentication (#414)
Browse files Browse the repository at this point in the history
  • Loading branch information
timja committed May 4, 2023
1 parent 267830b commit 16bdcbe
Show file tree
Hide file tree
Showing 22 changed files with 432 additions and 116 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -68,7 +68,7 @@ To use this plugin to create VM agents, first you need to have an Azure Service
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.
This allows an agent to be reserved for certain kinds of jobs.
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).
11. Specify Admin Credentials (a username/password credentials), this is the username and password if you want to log into the agent VM.
11. Specify Admin Credentials - This needs to be either an "SSH Username with private key" or "Username with password" credential
12. Click Verify Template to make sure all your configurations are correct, then Save.

### Run Jenkins Jobs on Azure VM Agents
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Expand Up @@ -77,6 +77,11 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>jsch</artifactId>
</dependency>
<dependency>
<groupId>com.sshtools</groupId>
<artifactId>maverick-synergy-client</artifactId>
<version>3.0.10</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down
26 changes: 9 additions & 17 deletions src/main/java/com/microsoft/azure/vmagent/AzureVMAgent.java
Expand Up @@ -16,10 +16,8 @@
package com.microsoft.azure.vmagent;

import com.azure.resourcemanager.compute.models.OperatingSystemTypes;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.microsoft.azure.util.AzureCredentials;
import com.microsoft.azure.vmagent.remote.AzureVMAgentSSHLauncher;
import com.microsoft.azure.vmagent.util.AzureUtil;
import com.microsoft.azure.vmagent.util.CleanUpAction;
import com.microsoft.azure.vmagent.util.Constants;
import edu.umd.cs.findbugs.annotations.CheckForNull;
Expand All @@ -40,8 +38,15 @@
import hudson.slaves.SlaveComputer;
import hudson.util.FormValidation;
import hudson.util.LogTaskListener;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;

import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.cloudstats.CloudStatistics;
import org.jenkinsci.plugins.cloudstats.ProvisioningActivity;
Expand All @@ -51,15 +56,6 @@
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;

import java.io.IOException;
import java.io.PrintStream;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

public class AzureVMAgent extends AbstractCloudSlave implements TrackedItem {

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

// Grab the username/pass
StandardUsernamePasswordCredentials creds = AzureUtil.getCredentials(vmCredentialsId);

if (isUnix) {
command = "sh " + REMOTE_TERMINATE_FILE_NAME;
} else {
Expand All @@ -647,8 +640,7 @@ public synchronized void deprovision(Localizable reason) throws Exception {
command,
terminateStream,
isUnix,
executeInitScriptAsRoot,
creds.getPassword().getPlainText());
executeInitScriptAsRoot);
if (exitStatus != 0) {
LOGGER.log(Level.SEVERE,
"Terminate script failed: exit code={0} ", exitStatus);
Expand Down
32 changes: 17 additions & 15 deletions src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java
Expand Up @@ -21,10 +21,11 @@
import com.azure.resourcemanager.compute.models.DiskSkuTypes;
import com.azure.resourcemanager.storage.models.SkuName;
import com.azure.resourcemanager.storage.models.StorageAccount;
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.jcraft.jsch.OpenSSHConfig;
import com.microsoft.azure.util.AzureBaseCredentials;
import com.microsoft.azure.util.AzureCredentialUtil;
Expand All @@ -49,7 +50,6 @@
import hudson.Util;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.TaskListener;
Expand All @@ -65,7 +65,6 @@
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -80,11 +79,11 @@
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.kohsuke.stapler.verb.POST;

/**
* This class defines the configuration of Azure instance templates.
Expand Down Expand Up @@ -1086,7 +1085,7 @@ public String getCredentialsId() {
return credentialsId;
}

public StandardUsernamePasswordCredentials getVMCredentials() throws AzureCloudException {
public StandardUsernameCredentials getVMCredentials() throws AzureCloudException {
return AzureUtil.getCredentials(credentialsId);
}

Expand Down Expand Up @@ -1444,16 +1443,19 @@ public ListBoxModel doFillVirtualMachineSizeItems(
return model;
}

public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item owner) {
// when configuring the job, you only want those credentials that are available to ACL.SYSTEM selectable
// as we cannot select from a user's credentials unless they are the only user submitting the build
// (which we cannot assume) thus ACL.SYSTEM is correct here.
return new StandardListBoxModel().withAll(
CredentialsProvider.lookupCredentials(
StandardUsernamePasswordCredentials.class,
owner,
ACL.SYSTEM,
Collections.<DomainRequirement>emptyList()));
@POST
public ListBoxModel doFillCredentialsIdItems(@QueryParameter String credentialsId) {
StandardListBoxModel model = new StandardListBoxModel();

Check warning on line 1448 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1448 is not covered by tests
Jenkins context = Jenkins.get();

Check warning on line 1449 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1449 is not covered by tests
if (!context.hasPermission(CredentialsProvider.CREATE)

Check warning on line 1450 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1450 is only partially covered, 2 branches are missing
&& !context.hasPermission(CredentialsProvider.UPDATE)) {

Check warning on line 1451 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1451 is only partially covered, 2 branches are missing
return model.includeCurrentValue(credentialsId);

Check warning on line 1452 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1452 is not covered by tests
}

return model

Check warning on line 1455 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1455 is not covered by tests
.includeAs(ACL.SYSTEM, context, SSHUserPrivateKey.class)

Check warning on line 1456 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1456 is not covered by tests
.includeAs(ACL.SYSTEM, context, StandardUsernamePasswordCredentials.class)

Check warning on line 1457 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1457 is not covered by tests
.includeCurrentValue(credentialsId);

Check warning on line 1458 in src/main/java/com/microsoft/azure/vmagent/AzureVMAgentTemplate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 1458 is not covered by tests
}

public ListBoxModel doFillOsTypeItems() throws IOException, ServletException {
Expand Down
Expand Up @@ -1029,7 +1029,7 @@ private void releaseLockForAgent(AzureVMAgent agent) {
}

public AzureResourceManager getAzureClient() {
if (azureClient == null) {
if (azureClient == null && credentialsId != null) {

Check warning on line 1032 in src/main/java/com/microsoft/azure/vmagent/AzureVMCloud.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 1032 is only partially covered, 4 branches are missing
this.azureClient = AzureResourceManagerCache.get(credentialsId);
}

Expand Down
Expand Up @@ -49,6 +49,8 @@
import com.azure.storage.blob.BlobUrlParts;
import com.azure.storage.blob.models.BlobItem;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -60,10 +62,12 @@
import com.microsoft.azure.vmagent.exceptions.AzureCloudException;
import com.microsoft.azure.vmagent.launcher.AzureComputerLauncher;
import com.microsoft.azure.vmagent.launcher.AzureSSHLauncher;
import com.microsoft.azure.vmagent.retry.NoRetryStrategy;
import com.microsoft.azure.vmagent.util.*;
import com.microsoft.jenkins.credentials.AzureResourceManagerCache;
import com.sshtools.common.publickey.InvalidPassphraseException;
import com.sshtools.common.ssh.SshException;
import hudson.model.Descriptor.FormException;
import hudson.util.Secret;
import io.jenkins.plugins.azuresdk.HttpClientRetriever;
import jenkins.model.Jenkins;
import jenkins.slaves.JnlpAgentReceiver;
Expand Down Expand Up @@ -579,9 +583,8 @@ public AzureVMDeploymentInfo createDeployment(

putVariable(tmp, "vmSize", template.getVirtualMachineSize());
// Grab the username/pass
StandardUsernamePasswordCredentials creds = template.getVMCredentials();
StandardUsernameCredentials creds = template.getVMCredentials();

Check warning on line 586 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 586 is not covered by tests

putVariable(tmp, "adminUsername", creds.getUsername());
putVariableIfNotBlank(tmp, "storageAccountName", storageAccountName);
putVariableIfNotBlank(tmp, "storageAccountType", storageAccountType);
putVariableIfNotBlank(tmp, "blobEndpointSuffix", blobEndpointSuffix);
Expand Down Expand Up @@ -618,8 +621,22 @@ public AzureVMDeploymentInfo createDeployment(

final ObjectNode parameters = MAPPER.createObjectNode();

defineParameter(tmp, "adminPassword", "secureString");
putParameter(parameters, "adminPassword", creds.getPassword().getPlainText());
defineParameter(tmp, "adminUsername", "string");

Check warning on line 624 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 624 is not covered by tests
putParameter(parameters, "adminUsername", creds.getUsername());

Check warning on line 625 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 625 is not covered by tests

defineParameter(tmp, "authenticationType", "string");

Check warning on line 627 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 627 is not covered by tests
defineParameter(tmp, "adminPasswordOrKey", "secureString");

Check warning on line 628 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 628 is not covered by tests
if (creds instanceof StandardUsernamePasswordCredentials) {

Check warning on line 629 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 629 is only partially covered, 2 branches are missing
StandardUsernamePasswordCredentials passwordCredentials = (StandardUsernamePasswordCredentials) creds;

Check warning on line 630 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 630 is not covered by tests
putParameter(parameters, "adminPasswordOrKey", passwordCredentials.getPassword().getPlainText());

Check warning on line 631 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 631 is not covered by tests
putParameter(parameters, "authenticationType", "password");

Check warning on line 632 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 632 is not covered by tests
} else {

Check warning on line 633 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 633 is not covered by tests
SSHUserPrivateKey sshCredentials = (SSHUserPrivateKey) creds;

Check warning on line 634 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 634 is not covered by tests
String privateKey = sshCredentials.getPrivateKeys().get(0);

Check warning on line 635 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 635 is not covered by tests
String rsaPublicKey = KeyDecoder.getPublicKey(privateKey, Secret.toString(sshCredentials.getPassphrase()));

Check warning on line 636 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 636 is not covered by tests
putParameter(parameters, "adminPasswordOrKey", rsaPublicKey);

Check warning on line 637 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 637 is not covered by tests
putParameter(parameters, "authenticationType", "key");

Check warning on line 638 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 638 is not covered by tests
}

// Register the deployment for cleanup
deploymentRegistrar.registerDeployment(
Expand Down Expand Up @@ -2014,18 +2031,23 @@ public List<String> verifyTemplate(
}

//verify password
String adminPassword = "";
try {
StandardUsernamePasswordCredentials creds = AzureUtil.getCredentials(credentialsId);
adminPassword = creds.getPassword().getPlainText();
} catch (AzureCloudException e) {
LOGGER.log(Level.SEVERE, "Could not load the VM credentials", e);
}
String adminPassword;
StandardUsernameCredentials creds = AzureUtil.getCredentials(credentialsId);

Check warning on line 2035 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2035 is not covered by tests
if (creds instanceof StandardUsernamePasswordCredentials) {

Check warning on line 2036 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 2036 is only partially covered, 2 branches are missing
adminPassword = ((StandardUsernamePasswordCredentials) creds).getPassword().getPlainText();

Check warning on line 2037 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2037 is not covered by tests

validationResult = verifyAdminPassword(adminPassword);
addValidationResultIfFailed(validationResult, errors);
if (returnOnSingleError && !errors.isEmpty()) {
return errors;
validationResult = verifyAdminPassword(adminPassword);
addValidationResultIfFailed(validationResult, errors);
if (returnOnSingleError && !errors.isEmpty()) {
return errors;
}
} else {
SSHUserPrivateKey sshCredentials = (SSHUserPrivateKey) creds;

Check warning on line 2045 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2045 is not covered by tests
validationResult = verifySSHKey(sshCredentials.getPrivateKeys(), sshCredentials.getPassphrase());

Check warning on line 2046 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2046 is not covered by tests
addValidationResultIfFailed(validationResult, errors);

Check warning on line 2047 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2047 is not covered by tests
if (returnOnSingleError && !errors.isEmpty()) {

Check warning on line 2048 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 2048 is only partially covered, 4 branches are missing
return errors;

Check warning on line 2049 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2049 is not covered by tests
}
}

//verify JVM Options
Expand Down Expand Up @@ -2394,6 +2416,26 @@ private static String verifyAdminPassword(String adminPassword) {
}
}

private static String verifySSHKey(List<String> sshKeys, Secret passphrase) throws RuntimeException {
if (sshKeys == null || sshKeys.isEmpty()) {

Check warning on line 2420 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 2420 is only partially covered, 4 branches are missing
return Messages.AzureVMManagementServiceDelegate_SSH_Missing_Key();

Check warning on line 2421 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2421 is not covered by tests
}

if (sshKeys.size() > 1) {

Check warning on line 2424 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 2424 is only partially covered, 2 branches are missing
return Messages.AzureVMManagementServiceDelegate_SSH_Multiple_Keys_Found();

Check warning on line 2425 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2425 is not covered by tests
}

String sshKey = sshKeys.get(0);

Check warning on line 2428 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2428 is not covered by tests
try {
KeyDecoder.getPublicKey(sshKey, Secret.toString(passphrase));

Check warning on line 2430 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2430 is not covered by tests
} catch (IOException | InvalidPassphraseException | SshException e) {

Check warning on line 2431 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2431 is not covered by tests
LOGGER.log(Level.INFO, "Failed to validate SSH key", e);

Check warning on line 2432 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2432 is not covered by tests
return Messages.AzureVMManagementServiceDelegate_SSH_Invalid_Key_Format();

Check warning on line 2433 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2433 is not covered by tests
}

Check warning on line 2434 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2434 is not covered by tests

return Constants.OP_SUCCESS;

Check warning on line 2436 in src/main/java/com/microsoft/azure/vmagent/AzureVMManagementServiceDelegate.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 2436 is not covered by tests
}

private static String verifyJvmOptions(String jvmOptions) {
if (StringUtils.isBlank(jvmOptions) || AzureUtil.isValidJvmOption(jvmOptions)) {
return Constants.OP_SUCCESS;
Expand Down

0 comments on commit 16bdcbe

Please sign in to comment.