Nick Charlton

Setting Jenkins Credentials with Groovy

I’ve been building up a nice pattern for bootstrapping Jenkins’ secrets through init.groovy.d, storing the secrets themselves inside configuration management. So far, this has been the simplest way to get a working configuration without additional moving parts beyond Jenkins and a configuration management tool.

The Jenkins Credentials plugin supports a few different secret types: “secret text” (which can be used as an environment variable), username & password, files from the file system and a few others. We can handle SSH private keys using the SSH Credentials plugin. These can be made available globally (i.e.: across multiple build nodes), or just on specific build nodes but here we’re just going to treat them as global.

As init.groovy.d scripts

I’ve used a few sources to understand how to do this in the past, especially this Gist. But I wanted to do this incrementally and be cautious on which dependencies were being imported. So, here’s how to implement each:

Secret Text

#!/usr/bin/env groovy

import jenkins.model.Jenkins
import com.cloudbees.plugins.credentials.domains.Domain
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl
import com.cloudbees.plugins.credentials.CredentialsScope
import hudson.util.Secret

instance = Jenkins.instance
domain = Domain.global()
store = instance.getExtensionList(
  "com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0].getStore()

secretText = new StringCredentialsImpl(
  CredentialsScope.GLOBAL,
  "SECRET_NAME",
  "SECRET_DESCRIPTION",
  Secret.fromString("SECRET_TEXT")
)

store.addCredentials(domain, secretText)

Username & Password

#!/usr/bin/env groovy

import jenkins.model.Jenkins
import com.cloudbees.plugins.credentials.domains.Domain
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl
import com.cloudbees.plugins.credentials.CredentialsScope

instance = Jenkins.instance
domain = Domain.global()
store = instance.getExtensionList(
  "com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0].getStore()

usernameAndPassword = new UsernamePasswordCredentialsImpl(
  CredentialsScope.GLOBAL,
  "SECRET_NAME",
  "SECRET_DESCRIPTION",
  "USERNAME",
  "PASSWORD"
)

store.addCredentials(domain, usernameAndPassword)

The source for the implementation contains some additional options.

SSH Private Key

#!/usr/bin/env groovy

import jenkins.model.Jenkins
import com.cloudbees.plugins.credentials.domains.Domain
import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey
import com.cloudbees.plugins.credentials.CredentialsScope

instance = Jenkins.instance
domain = Domain.global()
store = instance.getExtensionList(
  "com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0].getStore()

privateKey = new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(
  '''
PRIVATE_KEY_TEXT
  '''
)

sshKey = new BasicSSHUserPrivateKey(
  CredentialsScope.GLOBAL,
  "SECRET_TEXT",
  "PRIVATE_KEY_USERNAME",
  privateKey,
  "PRIVATE_KEY_PASSPHRASE",
  "SECRET_DESCRIPTION"
)

store.addCredentials(domain, sshKey)

This was more difficult to figure out than the others, with the implementation of BasicSSHUserPrivateKey.java invaluable. The indentation of PRIVATE_KEY_TEXT is missing so that extraneous whitespace doesn’t cause problems. I found that where Jenkins reads the key in itself, indentation is not significant (like when connecting to build nodes) but where it’s reused elsewhere (like the Git plugin) would fail to connect to the repository.

It supports multiple different types of SSH keys: entering directly (what we’re doing here) but also reading off the disk. We need to use ''' to have a multi-line string as the private key will span a few lines. You’ll need the ssh-credentials plugin installed, too.

Configuring with Ansible

I’m using Ansible (with secrets stored in Ansible Vault), based around Jeff Geerling’s Ansible role for Jenkins, but this pattern could be replicated elsewhere.

Ansible Vault arranges secrets by encrypting variables which are accessible when playbooks are run. My original idea was to have a single variable name which when written would reflect on the type you set, for example:

---
jenkins_global_secrets:
  - name: EXAMPLE_SECRET_TEXT
    description: An example secret text value
    value: a_very_important_secret
    type: :secret_text

But this caused quite a bit complexity when trying to work out where the logic should be. To bridge the secrets from Ansible Vault to the provisioned machine, they’re written out from a template of the Groovy file and so splitting some of the logic between the template stage (which Ansible does) and the Groovy file (which is executed on runtime) felt misguided.

A much nicer approach seemed to be to use many top level variables, and instead you end up with this:

---
jenkins_secret_text_credentials:
  - name: EXAMPLE_SECRET_TEXT
    description: An example secret text value
    secret: a_very_important_secret

Which is very similar to how the Jenkins Chef cookbook solves this problem. From here, we can use this in a template like this (configure_jenkins_credentials.groovy.j2):

#!/usr/bin/env groovy

import jenkins.model.Jenkins
import com.cloudbees.plugins.credentials.domains.Domain
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl
import com.cloudbees.plugins.credentials.CredentialsScope
import hudson.util.Secret

instance = Jenkins.instance
domain = Domain.global()
store = instance.getExtensionList(
  "com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0].getStore()

{% for secret in jenkins_global_secrets %}
secretText = new StringCredentialsImpl(
  CredentialsScope.GLOBAL,
  "{{ secret['name'] }}",
  "{{ secret['description'] }}",
  Secret.fromString("{{ secret['text'] }}")
)

store.addCredentials(domain, secretText)
{% endfor %}

In Ansible, this would then be written to the right place with something like this:

- name: Place the Jenkins Credentials Groovy script
  template:
    src: "configure_jenkins_credentials.groovy.j2"
    dest: "{{ jenkins_home }}/init.groovy.d/configure_jenkins_credentials.groovy"

With multiple top-level variables like this, the final result ends up being:

#!/usr/bin/env groovy

import jenkins.model.Jenkins
import com.cloudbees.plugins.credentials.domains.Domain
import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl
import com.cloudbees.plugins.credentials.CredentialsScope
import hudson.util.Secret
import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey
import com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl

instance = Jenkins.instance
domain = Domain.global()
store = instance.getExtensionList(
  "com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0].getStore()

{% for credential in jenkins_secret_text_credentials %}
secretText = new StringCredentialsImpl(
  CredentialsScope.GLOBAL,
  "{{ credential['name'] }}",
  "{{ credential['description'] }}",
  Secret.fromString("{{ credential['text'] }}")
)

store.addCredentials(domain, secretText)
{% endfor %}

{% for credential in jenkins_ssh_credentials %}
privateKey = new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(
  '''
{{ credential['private_key'] }}
  '''
)

sshKey = new BasicSSHUserPrivateKey(CredentialsScope.GLOBAL,
                                    "{{ credential['name'] }}",
                                    "{{ credential['username'] }}",
                                    privateKey,
                                    "{{ credential['passphrase'] }}",
                                    "{{ credential['description'] }}"
)

store.addCredentials(domain, sshKey)
{% endfor %}

{% for credential in jenkins_username_password_credentials %}
usernameAndPassword = new UsernamePasswordCredentialsImpl(
  CredentialsScope.GLOBAL,
  "{{ credential['name'] }}",
  "{{ credential['description'] }}",
  "{{ credential['username'] }}",
  "{{ credential['password'] }}"
)

store.addCredentials(domain, usernameAndPassword)
{% endfor %}

…and then how the secrets would be structured:

---
jenkins_secret_text_credentials:
  - name: SECRET_TEXT
    description: SECRET_DESCRIPTION
    text: SECRET_TEXT
jenkins_ssh_credentials:
  - name: SSH_KEY
    username: PRIVATE_KEY_USERNAME
    private_key: |
      -----BEGIN OPENSSH PRIVATE KEY-----
      ...
      -----END OPENSSH PRIVATE KEY-----
    passphrase: PRIVATE_KEY_PASSPHRASE
    description: SECRET_DESCRIPTION
jenkins_username_password_credentials:
  - name: SECRET_USERNAME
    description: SECRET_DESCRIPTION
    username: SECRET_USERNAME
    password: SECRET_PASSWORD