Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JENKINS-74836] Allow using a file based ssh credential via system property #1003

Merged
Merged
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,25 @@ the main "Manage Jenkins" \> "Configure System" page, and scroll down
near the bottom to the "Cloud" section. There, you click the "Add a new
cloud" button, and select the "Amazon EC2" option. This will display the
UI for configuring the EC2 plugin.  Then enter the Access Key and Secret
Access Key which act like a username/password (see IAM section). Because
of the way EC2 works, you also need to have an RSA private key that the
Access Key which act like a username/password (see IAM section).

Because of the way EC2 works, you also need to have an RSA private key that the
cloud has the other half for, to permit sshing into the instances that
are started. Please use the AWS console or any other tool of your choice
to generate the private key to interactively log in to EC2 instances.
to generate the private key to interactively log in to EC2 instances.

Once you have generated the needed private key you must either store it as
a Jenkins `SSH Private Key` credential (and select that credential in your cloud
config).

If you do not want to create a new Jenkins credential you may alterantively store it
in plain text on disk, indicating its file path via the Jenkins system property
`SSH_KEY_PAIR_PRIVATE_KEY_FILE`. If this system property has a non-empty value then
mikecirioli marked this conversation as resolved.
Show resolved Hide resolved
it will override the ssh credential specified in the cloud configuration page. This
approach works well for `k8s` secrets that are mounted in a jenkins container for example.

Once you have put in your Access Key and Secret Access Key, select a
region for the cloud (not shown in screenshot). You may define only one
Once you have put in your Access Key, Secret Access Key, and configured an ssh private key
select a region for the cloud (not shown in screenshot). You may define only one
cloud for each region, and the regions offered in the UI will show only
the regions that you don't already have clouds defined for them.

Expand Down
75 changes: 62 additions & 13 deletions src/main/java/hudson/plugins/ec2/EC2Cloud.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -127,6 +130,10 @@ public abstract class EC2Cloud extends Cloud {

private static final SimpleFormatter sf = new SimpleFormatter();

// if this system property is defined and its value points to a valid ssh private key on disk
// then this will be used instead of any configured ssh credential
public static final String SSH_PRIVATE_KEY_FILEPATH = EC2Cloud.class.getName() + ".sshPrivateKeyFilePath";

private transient ReentrantLock slaveCountingLock = new ReentrantLock();

private final boolean useInstanceProfileForCredentials;
Expand Down Expand Up @@ -195,7 +202,11 @@ protected EC2Cloud(String id, boolean useInstanceProfileForCredentials, String c

@CheckForNull
public EC2PrivateKey resolvePrivateKey(){
if (sshKeysCredentialsId != null) {
if (!System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) {
LOGGER.fine(() -> "(resolvePrivateKey) secret key file configured, will load from disk");
return fetchPrivateKeyFromDisk();
} else if (sshKeysCredentialsId != null) {
LOGGER.fine(() -> "(resolvePrivateKey) Using jenkins ssh credential");
SSHUserPrivateKey privateKeyCredential = getSshCredential(sshKeysCredentialsId, Jenkins.get());
if (privateKeyCredential != null) {
return new EC2PrivateKey(privateKeyCredential.getPrivateKey());
Expand All @@ -204,6 +215,25 @@ public EC2PrivateKey resolvePrivateKey(){
return null;
}

/* visible for testing */
@CheckForNull
public static EC2PrivateKey fetchPrivateKeyFromDisk() {
return fetchPrivateKeyFromDisk(System.getProperty(SSH_PRIVATE_KEY_FILEPATH, ""));
}

@CheckForNull
public static EC2PrivateKey fetchPrivateKeyFromDisk(String filepath) {
mikecirioli marked this conversation as resolved.
Show resolved Hide resolved
if (!(filepath == null) && !(filepath.isEmpty())) {
mikecirioli marked this conversation as resolved.
Show resolved Hide resolved
try {
return new EC2PrivateKey(Files.readString(Paths.get(filepath), StandardCharsets.UTF_8));
} catch (IOException e) {
LOGGER.log(Level.WARNING, "unable to read private key from file " + filepath, e);
return null;
}
}
return null;
}

public abstract URL getEc2EndpointUrl() throws IOException;

public abstract URL getS3EndpointUrl() throws IOException;
Expand Down Expand Up @@ -1122,6 +1152,7 @@ public ListBoxModel doFillSshKeysCredentialsIdItems(@AncestorInPath ItemGroup co
AbstractIdCredentialsListBoxModel result = new StandardListBoxModel();
if (Jenkins.get().hasPermission(Jenkins.ADMINISTER)) {
result = result
.includeEmptyValue()
fcojfernandez marked this conversation as resolved.
Show resolved Hide resolved
.includeMatchingAs(Jenkins.getAuthentication(), context, SSHUserPrivateKey.class, Collections.<DomainRequirement>emptyList(), CredentialsMatchers.always())
.includeMatchingAs(ACL.SYSTEM, context, SSHUserPrivateKey.class, Collections.<DomainRequirement>emptyList(), CredentialsMatchers.always())
.includeCurrentValue(sshKeysCredentialsId);
Expand All @@ -1135,16 +1166,26 @@ public FormValidation doCheckSshKeysCredentialsId(@AncestorInPath ItemGroup cont
// Don't do anything if the user is only reading the configuration
return FormValidation.ok();
}
if (value == null || value.isEmpty()){
return FormValidation.error("No ssh credentials selected");
}

SSHUserPrivateKey sshCredential = getSshCredential(value, context);
String privateKey = "";
if (sshCredential != null) {
privateKey = sshCredential.getPrivateKey();
String privateKey;

if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) {
if (value == null || value.isEmpty()) {
return FormValidation.error("No ssh credentials selected");
}

SSHUserPrivateKey sshCredential = getSshCredential(value, context);
if (sshCredential != null) {
privateKey = sshCredential.getPrivateKey();
} else {
return FormValidation.error("Failed to find credential \"" + value + "\" in store.");
}
} else {
return FormValidation.error("Failed to find credential \"" + value + "\" in store.");
EC2PrivateKey k = fetchPrivateKeyFromDisk();
if (k == null) {
return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH));
}
privateKey = k.getPrivateKey();
mikecirioli marked this conversation as resolved.
Show resolved Hide resolved
}

boolean hasStart = false, hasEnd = false;
Expand Down Expand Up @@ -1188,12 +1229,20 @@ protected FormValidation doTestConnection(@AncestorInPath ItemGroup context, URL
return FormValidation.ok();
}
try {
SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context);
String privateKey = "";
if (sshCredential != null) {
privateKey = sshCredential.getPrivateKey();
if (System.getProperty(SSH_PRIVATE_KEY_FILEPATH, "").isEmpty()) {
SSHUserPrivateKey sshCredential = getSshCredential(sshKeysCredentialsId, context);
if (sshCredential != null) {
privateKey = sshCredential.getPrivateKey();
} else {
return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store.");
}
} else {
return FormValidation.error("Failed to find credential \"" + sshKeysCredentialsId + "\" in store.");
EC2PrivateKey k = fetchPrivateKeyFromDisk();
if (k == null) {
return FormValidation.error("Failed to find private key file " + System.getProperty(SSH_PRIVATE_KEY_FILEPATH));
}
privateKey = k.getPrivateKey();
}

AWSCredentialsProvider credentialsProvider = createCredentialsProvider(useInstanceProfileForCredentials, credentialsId, roleArn, roleSessionName, region);
Expand Down
1 change: 1 addition & 0 deletions src/main/java/hudson/plugins/ec2/SlaveTemplate.java
Original file line number Diff line number Diff line change
Expand Up @@ -1655,6 +1655,7 @@ private KeyPair getKeyPair(AmazonEC2 ec2) throws IOException, AmazonClientExcept
if (keyPair == null) {
throw new AmazonClientException("No matching keypair found on EC2. Is the EC2 private key a valid one?");
}
LOGGER.fine("found matching keypair");
return keyPair;
}

Expand Down
5 changes: 5 additions & 0 deletions src/test/java/hudson/plugins/ec2/EC2CloudTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
@RunWith(MockitoJUnitRunner.class)
public class EC2CloudTest {

@Test
public void testFileBasedSShKey() {
assertTrue("file content should not have been empty", EC2Cloud.fetchPrivateKeyFromDisk(getClass().getClassLoader().getResource("hudson/plugins/ec2/test.pem").getPath()) != null);
mikecirioli marked this conversation as resolved.
Show resolved Hide resolved
}

@Test
public void testSlaveTemplateAddition() throws Exception {
AmazonEC2Cloud cloud = new AmazonEC2Cloud("us-east-1", true,
Expand Down
1 change: 1 addition & 0 deletions src/test/resources/hudson/plugins/ec2/test.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello, world!