Vault Jenkins Save

How to use Vault to store secrets and use them in Jenkins

Project README

How to use HashiCorp Vault to store secrets and read them from Jenkins

by Rodrigo A. Diaz Leven

Description

In this article we will learn how to store secret or any other type of information you wish like Certificates in Vault .

Vault works by exchanging information for secrets, for example a client could havea RoleID and a SecretID or a temporary Token that it can trade for the actual Secret.

It also can create temporary access to a 3rd party services like AWS through the use of back ends.

Why do we want to use it.

  • Centralized secrets storage and role based access.

  • Replace secrets stored in source code or shell scripts which then are stored in places, difficult to replace with one time or temporary authentication credentials.

  • Create dynamic authentication for short lived jobs, ie AWS infrastructure creation. This means that a short lived set of AWS secret/key pair is created for a job and they won't need to be hard coded.

  • Optional, Human authentication to services like SSH, Jenkins, VPN, etc.

The setup

We will test this by using a docker-compose project that will orchestrate 3 containers:

  • Consul: service discovery and backend for storing encrypted secrets
  • Vault: Service for converting tokens to secrets
  • Jenkins: Automation

We persist Jenkins configuration in a Docker volume called "jenkins-data" and bind the local directory config to the Consul container so we can copy configuration later.

version: '2'
services:
 consul:
   image: "consul"
   hostname: "consul"
   command: "agent -dev -client 0.0.0.0"
   ports:
     - "8400:8400"
     - "8500:8500"
     - "8600:53/udp"
 vault:
   depends_on:
     - consul
   image: "vault"
   hostname: "vault"
   links:
     - "consul:consul"
   environment:
     VAULT_ADDR: http://127.0.0.1:8200
   ports:
     - "8200:8200"
   volumes:
     - ./tools/wait-for-it.sh:/wait-for-it.sh
     - ./config/vault:/config
     - ./config/vault/policies:/policies
   entrypoint: /wait-for-it.sh -t 20 -h consul -p 8500 -s -- vault server -config=/config/with-consul.hcl
 blueocean:
   depends_on:
     - vault
   image: "jenkinsci/blueocean:latest"
   ports:
     - "8080:8080"
   volumes:
     - /var/run/docker.sock:/var/run/docker.sock
     - 'jenkins-data:/var/jenkins_home'
volumes:
 jenkins-data:

Initial Vault configuration

The first command starts the container and the second one logs us in the Vault container to "unseal" it.

This is the term used by Vault to say the service is ready to be used and to do so we need 3 of the 5 keys that were generated by the init command.

This is only needed the first time we setup vault and you should store the keys in a secure place.

$ docker-compose up -d
Creating vaultjenkins_consul_1 ... 
Creating vaultjenkins_consul_1 ... done
Creating vaultjenkins_vault_1 ... 
Creating vaultjenkins_vault_1 ... done
Creating vaultjenkins_blueocean_1 ... 
Creating vaultjenkins_blueocean_1 ... done

$ docker exec -it vaultjenkins_vault_1 sh

(vaultjenkins_vault_1)$ vault init
Unseal Key 1: d28dc3e20848c499749450b411bdc55416cefb0ff6ddefd01ec02088aa5c90aa01
Unseal Key 2: ad2b7e9d02d0c1cb5b98fafbc2e3ea56bd4d4fa112a0c61882c1179d6c6585f302
Unseal Key 3: c393269f177ba3d07b14dbf14e25a325205dfbf5c91769b8e55bf91aff693ce603
Unseal Key 4: 87c605e5f766d2f76d39756b486cbdafbb1998e72d2f766c40911f1a288e53a404
Unseal Key 5: e97e5de7e2cdb0ec4db55461c4aaf4dc26092cb3f698d9cc270bf19dbb82eab105
Initial Root Token: 5a4a7e11-1e2f-6f76-170e-b8ec58cd2da5

(vaultjenkins_vault_1)$ vault unseal (run this 3 times with 3 different keys from above)

Checking Vault is unsealed

If the unseal was successful then you can login to the UI by going to:

http://localhost:8500/ui/#/dc1/services/vault

image alt text

Testing

The first command sets an alias so we dont have to keep typing the same thing over and over.

We will use the Vault client from inside the container but you can use one installed in your local machine too.

$ alias vault='docker exec -it vaultjenkins_vault_1 vault "$@"'

$ vault write -address=http://127.0.0.1:8200 secret/billion-dollars value=behind-super-secret-password

$ vault read secret/billion-dollars
Key             	Value
---             	-----
refresh_interval	2592000
value           	behind-super-secret-password

Here we store a secret text "behind-super-secret-password" in the path "secret/billion-dollars" and check in the UI if it was recorded encrypted.

image alt text

Reading secrets from Jenkins

AppRole

AppRole is a secure introduction method to establish machine identity.

In AppRole, in order for the application to get a token, it would need to login using a Role ID (which is static, and associated with a policy), and a Secret ID (which is dynamic, one time use, and can only be requested by a previously authenticated user/system. In this case, we have two options:

  • Store the Role IDs in Jenkins

  • Store the Role ID in the Jenkinsfile of each project

Generating Policies and Roles

Now Jenkins will need permissions to retrieve Secret IDs for our newly created role. Jenkins shouldn’t be able to access the secret itself, list other Secret IDs, or even the Role ID.

In this case, tokens assigned to the java-example policy would have permission to read a secret on the secret/hello path.

$ echo 'path "auth/approle/role/java-example/secret-id" {
  capabilities = ["read","create","update"]
}' > ./config/vault/policies/jenkins_policy.hcl

$ vault policy-write jenkins ./config/vault/policies/jenkins_policy.hcl

Now we have to create a **Role **that will generate **tokens **associated with that policy, and retrieve the token:

$ echo 'path "secret/hello" {
  capabilities = ["read", "list"]
}' > ./config/vault/policies/java-example_policy.hcl
$ vault policy-write java-example ./config/vault/policies/java-example_policy.hcl

Policy 'java-example' written.
$ vault auth-enable approle
$ vault write auth/approle/role/java-example \
> secret_id_ttl=60m \
> token_ttl=60m \
> token_max_tll=120m \
> policies="java-example"
Success! Data written to: auth/approle/role/java-example

$ vault read auth/approle/role/java-example
Key                 Value
---                 -----
bind_secret_id      true
bound_cidr_list
period              0
policies            [default java-example]
secret_id_num_uses  0
secret_id_ttl       3600
token_max_ttl       0
token_num_uses      0
token_ttl           3600

Generating Role ID

We generate a Role ID for our application that can be used in conjunction with the Jenkins Token ID to generate a Secret ID that will allow us to access the secret at the path "secret/hello" per our policy.

$ vault read auth/approle/role/java-example/role-id
Key    	Value
---    	-----
role_id	73ea4552-53da-6844-bab0-0d39d2dc06aa

Generating Jenkins Token ID

Jenkins needs a long lived token but it needs to be eventually rotated.

$ vault token-create -policy=jenkins
Key            	Value
---            	-----
token          	c70b74af-0e46-40da-ae15-c09b95a636f9
token_accessor 	1c1ad1bf-1b56-e398-169a-7e14600a3cb3
token_duration 	768h0m0s
token_renewable	true
token_policies 	[default jenkins]

Configuring Jenkins credentials to access Vault

This is the long lived Token ID we generated for Jenkins use.

image alt text

This is the Role ID for our example app and is restricted to "secret/hello" by our policy, it can also be hardcoded in the source code or job script.

image alt text

Reading the secret

Let’s write a secret that our application will consume:

$ vault write secret/hello value="You've Successfully retrieved a secret from Hashicorp Vault"
Success! Data written to: secret/hello

We are going to exchange **ROLE_ID **(belonging only to this particular job) and a temporary **SECRET_ID **(generated with the Jenkins token) for another SECRET_ID.

This second SECRET_ID has a policy attached that will only let us access the secrets at the path "secret/hello".

This way we can authenticate our jobs and assign them specific access to our secrets, without leaving any secrets in our our source code or shell scripts.

pipeline {
  agent any
stages {  
  stage('Integration Tests') {
      steps {
      sh 'curl -s -o vault.zip https://releases.hashicorp.com/vault/0.9.1/vault_0.9.1_linux_amd64.zip ; yes | unzip vault.zip '
        withCredentials([string(credentialsId: 'JAVA_EXAMPLE_ROLE_ID', variable: 'ROLE_ID'),string(credentialsId: 'JENKINS_VAULT_TOKEN', variable: 'VAULT_TOKEN')]) {
        sh '''
  set +x
  export VAULT_ADDR=http://vault:8200
  export VAULT_SKIP_VERIFY=true
  export SECRET_ID=$(./vault write -field=secret_id -f auth/approle/role/java-example/secret-id)
  export JOB_VAULT_TOKEN=$(./vault write -field=token auth/approle/login role_id=${ROLE_ID}   secret_id=${SECRET_ID})
./vault read -address=http://vault:8200 secret/hello 
        '''
    }
   }
  }
 }
}
  • We retrieve a **SECRET_ID **A using Jenkins administrative **JENKINS_VAULT_TOKEN **(that we’ve manually generated before). That’s the only one that would be relatively longed lived, but can only generate SECRET_IDs. Notice that we store this SECRET_ID A in the environment variable called VAULT_TOKEN so the vault client can use it.

    • ./vault write -field=secret_id -f auth/approle/role/java-example/secret-id
      

  • We do an AppRole login with the **ROLE_ID **and the **SECRET_ID A **(short lived), this gives us a second SECRET_ID B that we store in the environment variable VAULT_TOKEN:

    • ./vault write -field=token auth/approle/login role_id=${ROLE_ID} secret_id=${SECRET_ID}
      
  • We use this SECRET_ID B to read our plain text secret:

    • ./vault read -address=http://vault:8200 secret/hello 
      

Retrieving secrets from source code

We can also retrieve the secrets from an application at run time, using environment variables to authenticate to Vault like this:

<td>  String vaulttoken = System.getenv("VAULT_TOKEN");
    String vaulthost = System.getenv("VAULT_ADDR");
    System.out.format( "Using Vault Host %s\n", vaulthost);
    System.out.format( "With Vault Token %s\n", vaulttoken );
    /* This should be a separate method called from Main, then
     * again for simplicity...
     */
    final VaultConfig config = new VaultConfig().build();
    final Vault vault = new Vault(config);
    try {
    final String value = vault.logical()
                   .read("secret/hello")
                   .getData().get("value");
    System.out.format( "value key in secret/hello is " + value +"\n");
    } catch(VaultException e) {
      System.out.println("Exception thrown: " + e);
    }

Job Output

image alt text

Optional: Installing Vault plugin for Jenkins

More information: https://github.com/jenkinsci/hashicorp-vault-plugin

image alt text

If we decide to use this plugin then we will be using the "Response Wrapping" feature from vault.

This feature creates one time use tokens that are used to access specific secrets and limiting the time they can be used.

node {
  // define the secrets and the env variables
  def secrets = [
      [$class: 'VaultSecret', path: 'secret/testing', secretValues: [
          [$class: 'VaultSecretValue', envVar: 'testing', vaultKey: 'value_one'],
          [$class: 'VaultSecretValue', envVar: 'testing_again', vaultKey: 'value_two']]],
      [$class: 'VaultSecret', path: 'secret/another_test', secretValues: [
          [$class: 'VaultSecretValue', envVar: 'another_test', vaultKey: 'value']]]
  ]

  // optional configuration, if you do not provide this the next higher configuration
  // (e.g. folder or global) will be used
  def configuration = [$class: 'VaultConfiguration',
                       vaultUrl: 'http://my-very-other-vault-url.com',
                       vaultCredentialId: 'my-vault-cred-id']

  // inside this block your credentials will be available as env variables
  wrap([$class: 'VaultBuildWrapper', configuration: configuration, vaultSecrets: secrets]) {
      sh 'echo $testing'
      sh 'echo $testing_again'
      sh 'echo $another_test'
  }
}

References

A lot of information for this article was used from:

The Docker compose project was based on:

Open Source Agenda is not affiliated with "Vault Jenkins" Project. README Source: bruj0/vault_jenkins

Open Source Agenda Badge

Open Source Agenda Rating