Hashicorp Vault & Jenkins

What is HasiCorp Vault?

It is an open-source tool that helps teams and projects manage and protect sensitive data and secrets. We want to store and use secrets from vault as much as possible to:

  • Limit secret sprawl
  • To make it easier to rotate secrets from a central place
  • To have finer granularity on which pipelines have access to which secrets
  • Limit secret exposure; If Jenkins server is compromised, the secrets aren’t also compromised
  • Store the Jenkins Secret backup separately from the main backup.

Setting up a hashicorp vault instance

You can follow Vault’s official documentation to setup a vault instance or use Linode / AWS marketplaces to easi setup a configured instance.

Setup Jenkins access to Vault

We will be using the AppRole feature to enable Jenkins access to Vault. AppRole should generally be used whenever you have a service to connect to Vault.

vault auth enable approle

# Create Jenkins role
vault write auth/approle/role/jenkins-role token_num_uses=0 secret_id_num_uses=0 policies="jenkins"

# Retrieve Approle secrets
vault read auth/approle/role/jenkins-role/role-id
vault write -f auth/approle/role/jenkins-role/secret-id

# Create policy to access secrets
cat << EOF > jenkins-policy.hcl
path "secret/data/jenkins/*" {
 capabilities = ["read", "list"]
}
path "secret/data/jenkins/secret_backup" {
  capabilities = ["create", "read", "update", "patch", "delete", "list"]
}
EOF

# Write policy
vault policy write jenkins jenkins-policy.hcl

Install the “HashiCorp vault” plugin on Jenkins, then navigate to the Jenkins system settings to update the plugin config with the vault server URL and the AppRole credentials with the credential id of ‘jenkins-role’.

Using secrets stored in Vault in Jenkins pipelines

Let’s first create a test secret that we’ll retrieve from a Jenkins pipeline:

# Create KV Store for jenkins
vault secrets enable -path=jenkins kv-v2

# Create test Secret in a path accessible by Jenkins
vault kv put jenkins/test-secret test-secret=123456789

Create a test pipeline to access the secret:

pipeline{
    agent any
    environment { 
        VAULT_URL = '<change to your vault URL>'
        VAULT_CREDENTIAL_ID = 'jenkins-role'
    }
    stages {
        stage('Vault') {
            steps {
                withVault(configuration: [disableChildPoliciesOverride: false, timeout: 60, vaultCredentialId: env.VAULT_CREDENTIAL_ID, vaultUrl: env.VAULT_URL], vaultSecrets: [[path: 'jenkins/test-secret', secretValues: [[envVar: 'TEST_SECRET', vaultKey: 'test-secret']]]]) {
                    sh 'echo "Vault secret is $TEST_SECRET"'
                }
            }
        }
    }
}

To use vault secrets you need to use withVault and provide the path to the secret, they key of the secret you want to extract, and the name of the environment variable to extract the secret to.

Backing up Jenkins secrets directory to Vault

Let’s setup a Jenkins pipeline to write a backup of the secrets directory to Vault every month if anything changes. Create a new pipeline and enable the Build periodically build trigger with the following schedule H 0 1 * * which should equate to once a month.

For Pipeline, choose ‘Pipeline Script’, and paste the following script:

pipeline{
    agent any
    environment { 
        VAULT_URL = '<change to your vault URL>'
        VAULT_CREDENTIAL_ID = 'jenkins-role'
        JENKIS_SECRET_FOLDER = '/var/lib/jenkins/secrets'
    }
    stages {
        stage('Vault') {
            steps {
                withCredentials([[$class: 'VaultTokenCredentialBinding', credentialsId:  env.VAULT_CREDENTIAL_ID, vaultAddr: env.VAULT_URL]]) {

                sh '''#!/bin/bash
                    # Check if we need to update secret
                    touch jenkins_secret_backup_latest_sum_old.txt 
                    sha256sum <(find $JENKIS_SECRET_FOLDER -type f -exec sha256sum {} \\; | sort) > jenkins_secret_backup_latest_sum.txt
                    
                    echo "Old shaSum = $(cat jenkins_secret_backup_latest_sum_old.txt)"
                    echo "New shaSum = $(cat jenkins_secret_backup_latest_sum.txt)"
                    # Skip next steps if hash doesn't change
                    diff jenkins_secret_backup_latest_sum_old.txt jenkins_secret_backup_latest_sum.txt && echo "Files Match" && exit 0
                    echo "Going to backup Jenkins secret folder to Vault"
                    
                    # Compress Jenkins secrets
                    tar -czvf ./jenkins_secret_backup_latest.tar.gz $JENKIS_SECRET_FOLDER 
                    
                    # Create vault secret
                    cat ./jenkins_secret_backup_latest.tar.gz | base64  > jenkins_secret_backup_latest_org.base64
                    tr -d '\n' < jenkins_secret_backup_latest_org.base64 > jenkins_secret_backup_latest.base64
                    
                    cat <<-EOF > jenkins_secret_backup_latest.json
                        { "data": {"secret_dir": "$(cat jenkins_secret_backup_latest.base64 )" }}
EOF
                    #echo  '{ "data": {"secret_dir": "$(cat jenkins_secret_backup_latest.base64 )" }}' > jenkins_secret_backup_latest.json
                    
                    cat jenkins_secret_backup_latest.json
                    
                    # Push secret to vault
                    curl \
                        --header "X-Vault-Token: $VAULT_TOKEN" \
                        --request POST \
                        --data @jenkins_secret_backup_latest.json\
                        $VAULT_ADDR/v1/jenkins/data/secret_backup
                    
                    # Check secret    
                    curl \
                        --header "X-Vault-Token: $VAULT_TOKEN" \
                        --request GET \
                        $VAULT_ADDR/v1/jenkins/data/secret_backup | jq .data.data.secret_dir | tr -d '"'  | tr -d '\n' > jenkins_secret_backup_ret.base64
                    
                    diff jenkins_secret_backup_latest.base64 jenkins_secret_backup_ret.base64 || exit 1
                    
                    # Update hash tracking
                    echo "Updating shaSum of the latest upload"
                    cp jenkins_secret_backup_latest_sum.txt jenkins_secret_backup_latest_sum_old.txt
                    
                '''
                }

            }
        }
    }
}

In a disaster recovery situation, you’ll need to download the backed up secrets directory by using your root or other appropriate vault token, unencode the secret, then copy the tar file to the new jenkins machines and untar to the right directory path.

# Download secret backup
curl \
   --header "X-Vault-Token: $VAULT_TOKEN" \
    --request GET \
    $VAULT_ADDR/v1/jenkins/data/secret_backup | jq .data.data.secret_dir | tr -d '"'  | tr -d '\n' > jenkins_secret_backup_ret.base64

# decode base64 to restore tar
cat jenkins_secret_backup_ret.base64 | base64 --decode >  ./jenkins_secret_backup.tar.gz 

# untar to destination folder
tar -xzvf ./jenkins_secret_backup.tar.gz -C $JENKINS_DST