• Stars
    star
    102
  • Rank 335,584 (Top 7 %)
  • Language
    Shell
  • Created almost 7 years ago
  • Updated over 6 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

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:

More Repositories

1

ProxmoxIPv6

Fully routed IPv6 on Promox and Docker with WireGuard as IPv4to6 tunnel
84
star
2

vault-plugin-auth-u2f

This is a nonofficial plugin for HashiCorp Vault that uses a FIDO U2F enabled device as a way to authenticate a human.By requiring something you know, a PIN with something you have, the physical token.
Go
46
star
3

jenkins_pipeline

A lean Continuous Deployment, Testing and Integration Pipeline using CoreOS/Docker/Jenkins
Shell
45
star
4

hookah

Deploying apps directly from a git push
Shell
34
star
5

k8s_mcextfs

Midnight Commander extfs for kubernetes
Python
23
star
6

vault-docker-compose

Create multiple Vault and Consul clusters working as primary and secondary using docker-compose
HCL
16
star
7

vault-recovery-key

This tool will decrypt your Vault recovery keys when using KMS
Go
15
star
8

docker_ipv6_securely

Automatic provisioning of IPv6 for Docker containers in a secure maner.
7
star
9

EDVBB

Elite Dangerous Virtual Button Box
Go
7
star
10

vault-plugin-secrets-pkcs11

This is a plugin for HashiCorp Vault that implements storing data objects into HSM devices using PKCS#11
Go
5
star
11

archlinux-install-rtl8723bs

Archlinux install ISO with the rlt8723bs and EFI32bit
4
star
12

vault-kv-backup

Unofficial HashiCorp Vault utility that backups a KV backend and other configuration, encrypting it via the transit secret engine
Python
4
star
13

helm-vault-gcp-kms

This is a helm chart to install HashiCorp Vault with GCP KMS support
Smarty
4
star
14

vault-replication

A system to facilitate the creation of multiple Vault clusters
HCL
4
star
15

MauticSmtpBouncesBundle

This is a plugin for Mautic that will set the "bounced" flag for an email that is bounced by the smtp, trough a REST api.
PHP
2
star
16

vault-decrypt

This utility will decrypt any value from Vault storage provided you have the unseal keys
Go
2
star
17

vault-lxc

Shell
1
star
18

namecheap_dns_export

A php script that will export the DNS as a bind zone using RFC1035, without using the API
PHP
1
star
19

vault-log-viewer

Application to serve colorized HTML version of CSV export of Vault logs
HTML
1
star
20

lab-reproducer

HCL
1
star