Adding credentials to Jenkins when it is running in Docker container

Default featured post

Adding credentials to Jenkins when it is running in Docker container. One of the best practices to have a clean Jenkins without any funky magics is to dockerize the Jenkins. In this way, the Jenkins can be deployed in any infrastructure easily without problems.

To achieve that, however, all Jenkins configurations should be coded. So when rebuilding the Jenkins docker image everything will be in place.

There are two important issues that need to tackle when transforming all Jenkins configurations to code which are:

  • Plugins
  • Credentials

For plugins, one can create a plugins.txtfile which Jenkins picks up automatically when running it from docker.

But for credentials, especially if you have tons of them, no straightforward and secure approach exists.

Here, we discuss two relatively easy options and discuss the pros and cons of each. These approaches are:

  • Custom groovy file
  • Jenkins Configuration As Code plugin

Custom Groovy file

When Jenkins is built from a dockerfile, it has the ability to execute all Groovy scripts stored in groovy directory. Groovy scripts are quite flexible and allow users to customize/configure Jenkins as needed.

Writing a Groovy script to import credentials to Jenkins from a file could be a plausible option for many. To do so, here we showcase a script that reads a CSV file contains credentials and add them to Jenkins when executing docker run.

However, we shouldn’t bake credentials in a docker image. To avoid it, need to leverage on docker secret and to use it, need to create either a docker compose file or use docker swarm. In this article, we use the simplest approach possible which is docker compose.

First we need to create a docker compose file which reads credentials from a file as follows:

version: "3.1"

services:
  jenkins:
    build:
      context: .
    image: test-jenkins
    container_name: test-jenkins
    ports:
      - "8080:8080"
    restart: always
    secrets:
      - artifactoryPassword

secrets:
  artifactoryPassword:
    file: ./secrets/artifactoryPassword

Then, we need to write a Groovy script that reads the content of a virtual path that docker secret loaded the credentials. A simple script that reads a CSV credentials file would be like this:

import jenkins.model.*
import com.cloudbees.plugins.credentials.*
import com.cloudbees.plugins.credentials.common.*
import com.cloudbees.plugins.credentials.domains.*
import com.cloudbees.plugins.credentials.impl.*
import com.cloudbees.jenkins.plugins.sshcredentials.impl.*

println("Setting credentials")

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


def credsFile = new File("/run/secrets/artifactoryPassword")

credsFile.eachLine { line ->
    def credFields = line.split("[,]")
    def credential = ['username': credFields[0].trim(), 'password': credFields[1].trim(), description: credFields[2].trim()]
    def user = new UsernamePasswordCredentialsImpl(CredentialsScope.GLOBAL, credFields[2].trim(), credential.description, credential.username, credential.password)
    store.addCredentials(domain, user)
}

In the above code, the .csv file is read line by and line and then each line is inserted to Jenkins credentials. Jenkins takes care of encoding the password so we don’t need to worry about it.
The structure of the file is like this:

username, password, description

Of course, the file can contain more information such as CredentialScope. For that, need to tweak the script as well.

Lastly, we need to change the Jenkins Docker file to copy our Groovy scripts:

COPY groovy/* /usr/share/jenkins/ref/init.groovy.d/

This approach pros and cons are listed as follows:

Pros

  • High flexibility
  • Good for massive credentials import
  • Suitable for those that don’t want to have too many plugins

Cons

  • Can be insecure depends on how the file is imported
  • Need to code which is more prone to error
  • Can’t be used in servers that have FTP, SFTP restrictions
  • Need docker compose

Jenkins Configuration as code plugin

Jenkins has a great plugin called “Jenkins Configuration as Code Plugin“, which allows us to put all Jenkins configuration in code and keep them in a repository. This is the best approach that can avoid CI/CD server migration hell and we highly recommend it.

The plugins offer a various range of features which surely makes any DevOps engineer happy. One of the goodies is the ability to define all credentials in a YAML file which the plugin picks it up on startup.

A naive approach is to pass all the credentials in the YAML file. But then we have to commit them to the source control which is a wrong approach.
A good practice is to define credentials in a YAML but pass the values through environment variable when running the container. In this approach, we don’t also need docker-compose.

Let’s look at this example.

First, we need to add configuration-as-code and configuration-as-code-support plugins to our plugins.txt file.

Then need to configure the plugin through the Dockerfile like this:

ENV CASC_JENKINS_CONFIG="/var/jenkins_home/casc_configs"
COPY casc_configs /var/jenkins_home/casc_configs

Then need to define the Jenkins credentials skeleton like this and put it in our credentials.yml file.

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

[..]

credentials:
  system:
    domainCredentials:
      - credentials:
          - basicSSHUserPrivateKey:
              scope: GLOBAL
              id: "basic-SSH"
              username: "ssh-username"
              passphrase: "" #Doable, but not recommended
              description: "SSH Credentials for ssh-username"
              privateKeySource:
                directEntry:
                  privateKey: ${SSH_PRIVATE_KEY} #Load from Environment Variable
          - usernamePassword:
              scope: GLOBAL
              id: "username"
              username: "some-user"
              password: ${SomeUserPassword} #Load from Environment Variable
              description: "Username/Password Credentials for some-user"
          - string:
              scope: GLOBAL
              id: "secret-text"
              secret: ${SecretText} #Load from Environment Variable
              description: "Secret Text"
          - aws:
              scope: GLOBAL
              id: "AWS"
              accessKey: ${AWS_ACCESS_KEY_ID} #Load from Environment Variable
              secretKey: ${AWS_SECRET_ACCESS_KEY} #Load from Environment Variable
description: "AWS Credentials"

As you can see, instead of passing the values we pass environment variable names. Then define the variable names on host machine like this:

$ export SSH_PRIVATE_KEY = `cat ssh_private_key`
$ export SomeUserPassword = `cat someUserPass`
$ export SecretText = `cat SecretText.txt`
$ export AWS_ACCESS_KEY_ID = `AWS_ACCESS_KEY`
$ export AWS_SECRET_ACCESS_KEY = `someFile`

As you can see, we read the variable value indirectly from a file. This is due to security reasons. If one exports the environment variable directly, the secret will be leaked to BASH history which is dangerous. So, be mindful.

The last step is to pass environment variables when running the Jenkins container, like this:

$ docker run -d -p 80:8080 \
-e SSH_PRIVATE_KEY \
-e SomeUserPassword \
-e SecretText \
-e AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY \

Pros

  • Out of box solutions
  • Ease of use
  • Quick setup

Cons

  • Not flexible
  • Not suitable when having too many credentials

Verdict

Which approach to use depends highly on the need of the project. But we highly recommend Jenkins configuration as code approach if you need much customization and don’t have a stack of credentials.