Securing your workflow using AppRoles on HashiCorp Vault

Machine authentication & authorization is a key pillar in the zero trust security model and in this article I will cover how Vault achieves this.

Glen Yu
4 min readOct 12, 2021

Introduction

This is the first of a two-part series that will explore how Vault can be leveraged to perform machine authentication & authorization. Part one will cover AppRoles and part two will show you how to setup a Vault Agent to use the GCP auth method.

I will continue with the narrative of a Jenkins server that needs credentials to perform deployments. The method of securing dynamic, short-lived credentials was covered here, but before all of that even happens, Jenkins must first securely authenticate to Vault. The most widely used solution is likely creating a Jenkins user (with a static password) in your LDAP or Active Directory for authentication to Vault. But where do you store that password? How do you manage the password age? Vault’s answer to this problem is the AppRole auth method.

An AppRole is, in its purest form, just another service account; it uses a username and password for authentication. In AppRole lingo they are called RoleID and SecretID and is just a way for a server to programmatically authenticate and subsequently request secrets from Vault. In lieu of a static password, you request one-time passwords as needed (depending on your configuration) to authenticate with your server, which then enables you to acquire a token to access secrets. You also get additional fine-grained control options such as restricting CIDRs which can authenticate successfully. I will discuss some of these features in more detail in the example below.

Setup

Image from HashiCorp

Enable the AppRole auth method:

vault auth enable -path=medium approle

TIP: make use of a custom path for better organization and better security by not using default endpoint names

I created a jenkins policy with the following permissions:

path "my-project-123/key/tfuser" {
capabilities = [ "read" ]
}

You may recognize the secret path as being the endpoint for the GCP Secrets Engine from an earlier article.

Create Jenkins auth role:

vault write auth/medium/role/jenkins \
secret_id_num_uses=1 \
secret_id_bound_cidrs="10.1.24.8/32" \
secret_id_ttl=5m \
token_policies="jenkins" \
token_ttl=30s \
token_max_ttl=30s \
token_bound_cidrs="10.1.24.8/32"

Three additional security constraints I highly recommend are secret_id_num_uses, secret_id_bound_cidrs and secret_id_ttl. The secret_id_num_uses parameter determine how many access token it can generate and can be thought of as being tied to the access token’s TTL. As long as the token is valid, you can continue to authenticate with the same SecretID from the same server and produce the same access token. This helps prevent the SecretID from being reused elsewhere as that would result in a different access token. secret_id_bound_cidrs defines the blocks of IP addresses which can log in (the same can be set for the resulting token with token_bound_cidrs). Finally, the secret_id_ttl setting will ensure that any generated SecretIDs that did not get used will be discarded. The attack surface can be greatly limited and windows of opportunity narrowed with these three settings. For a detailed list of parameters, please see the AuthRole API documentation.

TIP: if you need to pull secrets at different stages in your workflow, increase the secret_id_num_uses count to match the number of pulls required and reducing the access token TTL. E.g., secret_id_num_uses=3 and token_ttl=15s instead of secret_id_num_uses=1 and token_ttl=10m

How to Use

In order to log in, you will need to query the RoleID and generate a SecretID. The RoleID is static and not considered sensitive information so you are welcome to commit that to your code repos, Jenkinsfile, etc. I prefer to read it from Vault as I am authenticated anyway.

vault read -field=role_id auth/medium/role/jenkins/role-id

To generate a response-wrapped SecretID:

vault write -wrap-ttl=60s -force -field=wrapping_token auth/medium/role/jenkins/secret-id

Unwrap wrapping token to acquire SecretID:

VAULT_TOKEN=xxxxxxxxxx vault unwrap -field=secret_id

Log in with your RoleID and SecretID to get Vault token and access your secret:

VAULT_TOKEN=$(vault write -field=token auth/medium/login \
role_id=yyyyyyyyyy \
secret_id=zzzzzzzzzz) \
vault read -format=json my-project-123/key/tfuser

That is all there is to it!

While this may appear to be a lot of steps, it is meant to be embedded into your pipelines, so all these steps I have just outlined happens transparently in the background to support and secure your workflow.

Here is a diagram of what a more complex Jenkins workflow might look like:

Image from HashiCorp

Best Practices

In the example above, I used response wrapping when generating my SecretID. Response wrapping is part of HashiCorp’s AppRole Usage Best Practices and is Vault’s way of ensuring integrity and provide malfeasance detection. The wrapping token is a one-time token that needs to be unwrapped to access the secrets within; once unwrapped (or if wrapping token TTL expires), the wrapping token becomes invalid.

While the SecretID is meant to be a just-in-time generated password, you still need to be authenticated and authorized to do so. Leveraging a Vault Agent on the client host can help manage the initial authentication (steps 1 & 2 in the above diagram, also known as the secure introduction problem) and token lifecycles. This topic will be covered in my next article.

--

--

Glen Yu

Cloud Engineering @ PwC Canada. I'm a Google Cloud GDE, HashiCorp Ambassador and HashiCorp Core Contributor (Nomad). Also an ML/AI enthusiast!