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