'create jenkins ssh username with private key credential via rest xml api

Basically, I am trying to create a credential on jenkins via Rest API. Using xml data below:

<?xml version='1.0' encoding='UTF-8'?>
<com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>
    <scope>GLOBAL</scope>
    <id>jenkins-github-ssh</id>
    <description>jenkins-github-ssh</description>
    <username>username</username>
    <directEntryPrivateKeySource>
        <privateKey>-----BEGIN OPENSSH PRIVATE KEY-----
*****************************************
-----END OPENSSH PRIVATE KEY-----</privateKey>
    </directEntryPrivateKeySource>
</com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>

I can see the credential after calling REST post request. But when I use this credential for a GitHub repository, Jenkins says:

Failed to connect to repository : Command "git ls-remote -h -- [email protected]:***.git HEAD" returned status code 128: stdout: stderr: Load key "/tmp/ssh3978703187838467164.key": invalid format [email protected]: Permission denied (publickey). fatal: Could not read from remote repository.

But If I update the credential which is created by rest api with same private key above manually. It works. Somehow key is broken while posting. Do you guys have any idea to point me the solution?

Jenkins 2.198 & SSH Credentials Plugin 1.17.3

Thanks



Solution 1:[1]

I faced exactly the same problem while pushing private SSH keys to Jenkins by a Python script. I'm using the Requests library to create and update SSH key credential sets in arbitrary credential stores on the Jenkins server.

The general problem is that your XML structure is partially wrong. The tag

<directEntryPrivateKeySource>

must be replaced by

<privateKeySource class="com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource">

Getting the basic XML structure

You can get the correct XML structure by yourself from the Jenkins server when you follow these steps:

  1. Create a SSH key credential item manually. In the example below the key's id is test-sshkey. Let's place it in a credential store which is located in the folder "API-Test" which is a subfolder of "Playground", i.e. Playground/API-Test.
  2. When you click on the newly created credential item in the Jenkins UI its URL should look like this:

    https://JENKINS_HOSTNAME/job/Playground/job/API-Test/credentials/store/folder/domain/_/credential/test-sshkey/

  3. Add /config.xml to the URL above so that it looks like this:

    https://JENKINS_HOSTNAME/job/Playground/job/API-Test/credentials/store/folder/domain/_/credential/test-sshkey/config.xml

The XML structure returned by the URL in step 3 has almost the structure that we need for using with the Credentials API but is partially incomplete:

<com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey plugin="[email protected]">
  <id>test-sshkey</id>
  <description>DELETE AFTER USE</description>
  <username>test</username>
  <privateKeySource class="com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource">
    <privateKey>
      <secret-redacted/>
    </privateKey>
  </privateKeySource>
</com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>

Using the Credentials API

Add the tags <scope> and <passphrase> for a valid XML scaffold that you can POST to the Credentials API:

<com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>
    <scope>GLOBAL</scope>
    <id>CREDENTIAL_ID</id>
    <description>MY_DESCRIPTION</description>
    <username>A_USERNAME</username>
    <passphrase>OPTIONAL_KEY_PASSWORD</passphrase>
    <privateKeySource class="com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource">
        <privateKey>YOUR_PRIVATE_SSH_KEY_GOES_HERE</privateKey>
    </privateKeySource>
</com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>

Caveat: If the submitted XML has a wrong structure the REST API of the Credentials Plugin will nevertheless accept it and return a misleading HTTP status code 200!

Now we can use this XML structure to POST it to the API endpoints for creating or updating by cURL or similar tools. We assume that all operations are executed in the credential store of the folder "Playground/API-Test".

The following code example in Python is "dumbed down" completely to focus on the general approach:

def addCredentialSetSshPrivateKey(self, credentialDataObj):
    """
    Adds a credential set with a private SSH key to a credential store
    credentialDataObj: An instance of a simple DTO
    """
    jenkinsRequestUrl = "https://ci-yoda-new.codemanufaktur.com/job/Playground/job/API-Test/credentials/store/folder/domain/_/createCredentials"
    authentication = ("jenkins_admin_user", "API-TOKEN_FOR_THE_USER")

    completeSamlData = """
    <com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>
        <scope>GLOBAL</scope>
        <id>{0}</id>
        <description>{1}</description>
        <username>{2}</username>
        <passphrase>{3}</passphrase>
        <privateKeySource class="com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey$DirectEntryPrivateKeySource">
            <privateKey>{4}</privateKey>
        </privateKeySource>
    </com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey>
    """.format(credentialDataObj.id(), credentialDataObj.description(), credentialDataObj.username(), credentialDataObj.key_passphrase(), credentialDataObj.private_ssh_key())

    # When using CSRF protection in Jenkins a API crumb must be included in the actual REST call.
    # The following method requests the Jenkins Crumb Issuer for a API crumb and returns a JSON object like this:
    # {'_class': 'hudson.security.csrf.DefaultCrumbIssuer', 'crumb': 'a5d36ef09e063322169888f0b81341fe13b4109482a7936bc08c6f9a01badd39', 'crumbRequestField': 'Jenkins-Crumb'}
    jsonCrumb = self._requestApiCrumb()

    # The actual REST call with headers, XML payload and all other bells and whistles.
    remoteSession = requests.Session()
    return remoteSession.post(jenkinsRequestUrl, auth = authentication, headers = {"content-type":"application/xml", jsonCrumb['crumbRequestField']:jsonCrumb['crumb']}, data = completeSamlData)

REST endpoint for creating a SSH credential item: https://JENKINS_HOSTNAME/job/Playground/job/API-Test/credentials/store/folder/domain/_/createCredentials

REST endpoint for updating a SSH credential item: https://ci-yoda-new.codemanufaktur.com/job/Playground/job/API-Test/credentials/store/folder/domain/_/credential/credential_ci-yoda-new-project-apex_privatekey/config.xml

Apparently in the latter case you just update the config.xml file of an existing credential item.

Also see the user guide for the Credentials Plugin, section REST API, expecially for constructing the correct REST URLs. For requesting the Jenkins crumb issuer with Python see this StackOverflow answer.

Solution tested with:

  • Jenkins 2.214
  • Credentials Plugin 2.3.1
  • SSH Credentials Plugin 1.18.1

Solution 2:[2]

For the people who are having the exact same problem;

I've tried uploading it as a file, uploading it with API, using jenkins CLI, etc. Everything I tried has failed. Same issue is alsoposted in here;

https://issues.jenkins.io/browse/JENKINS-60714

So steps that finally worked is explained as follows;

  1. Install and configure the Jenkins Configuration as Code Plugin.

  2. Upload your configuration similar to yaml file below.

You might also want to define the private key content as an environment variable in the Jenkins instance and use it as "${private_key}" instead of pasting it visibly.

jenkins:
  systemMessage: "Example of configuring credentials in Jenkins"

credentials:
  system:
    domainCredentials:
    - credentials:
      - basicSSHUserPrivateKey:
          description: "kro"
          id: "kro"
          scope: GLOBAL
          username: "kro"
          privateKeySource:
            directEntry:
              privateKey: |
                -----BEGIN RSA PRIVATE KEY-----
                MIIG5AIBAAKCAYEAvuiaIDs+ydzR7Xxo5Owvv+G9/arbqN0YwhaGQQlicJjM4ZvI
              ..........YOUR KEY.............
                53Zg4QmSb1XGKUTXxIeGd27OIvgkwAn7K/cjQsU9t802iYV3tisnfA==
                -----END RSA PRIVATE KEY-----

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2