Developer

AppRole With Terraform & Chef

In the AppRole Pull Authentication guide, the question of how best to deliver the Role ID and Secret ID were brought up, and the role of trusted entities (Terraform, Chef, Nomad, Kubernetes, etc.) was mentioned.

AppRole auth method workflow

This intermediate Vault guide aims to provide a simple, end-to-end example of how to use Vault's AppRole authentication method, along with Terraform and Chef, to address the challenge of the secure introduction of an initial token to a target system.

The purpose of this guide is to provide the instruction to reproduce the working implementation demo introduced in the Delivering Secret Zero: Vault AppRole with Terraform and Chef webinar.

Challenge

The goal of the AppRole authentication method is to provide a mechanism for the secure introduction of secrets to target systems (servers, applications, containers, etc.).

The question becomes what systems within our environment do we trust to handle or deliver the RoleID and SecretID to our target systems.

Solution

Use Trusted Entities to deliver the AppRole authentication values. For example, use Terraform to deliver your RoleID or embed it into your AMI or Dockerfile. Then you might use Jenkins or Chef to obtain the response-wrapped SecretID and deliver it to the target system.

AppRole allows us to securely introduce the authentication token to the target system by preventing any single system from having full access to an authentication token that does not belong to it. This helps us maintain the security principles of least privilege and non-repudiation.

The important thing to note here is that regardless of what systems are considered as Trusted Entities, the same pattern applies.

For example:

Prerequisites

This guide assumes that you are proficient enough to perform basic Terraform tasks. If you are not familiar with Terraform, refer to the online documentation.

The following AWS resources are required to perform this demo:

Download demo assets

Clone or download the demo assets from the hashicorp/vault-guides GitHub repository to perform the steps described in this guide.

The following assets can be found in the repository:

  • Chef cookbook (/chef/cookbooks): A sample cookbook with a recipe that installs NGINX and demonstrates Vault Ruby Gem functionality used to interact with Vault APIs.
  • Terraform configurations (/terraform-aws):
    • /terraform-aws/mgmt-node: Configuration to set up a management server running both Vault and Chef Server, for demo purposes.
    • /terraform-aws/chef-node: Configuration to set up a Chef node and bootstrap it with the Chef Server, passing in Vault's AppRole RoleID and the appropriate Chef run-list.
  • Vault configuration (/scripts): Data scripts used to configure the appropriate mounts and policies in Vault for this demo.

Steps

The scenario in this guide uses Terraform and Chef as trusted entities to deliver RoleID and SecretID.

AppRole auth method workflow

For the simplicity of the demonstration, both Vault and Chef are installed on the same node. Terraform provisions the node which contains the RoleID as an environment variable. Chef pulls the SecretID from Vault.

Provisioning for this demo happens in 2 phases:

Phase 1: Provision our Vault and Chef Server

Step 1: Provision the Vault and Chef Server

This provides a quick and simple Vault and Chef Server configuration to help you get started.

NOTE: This is done for demonstration purpose and NOT a recommended practice for production.

In this phase, you use Terraform to spin up a server (and associated AWS resources) with both Vault and Chef Server installed. Once this server is up and running, you'll complete the appropriate configuration steps in Vault to set up our AppRole and tokens for use in the demo.

Using Terraform Open Source:

Task 1: Change the working directory (cd) to identity/vault-chef-approle/terraform-aws/mgmt-node.

Sample Code

Task 2: Update the terraform.tfvars.example file to match your account and rename it to terraform.tfvars.

At minimum, replace the following variable with appropriate values:

Task 3: Perform a terraform init to pull down the necessary provider resources. Then terraform plan to verify your changes and the resources that will be created. If all looks good, then perform a terraform apply to provision the resources. The Terraform output will display the public IP address to SSH into your server.

$ terraform init
Initializing provider plugins...
...
Terraform has been successfully initialized!


$ terraform plan
...
Plan: 5 to add, 0 to change, 0 to destroy.


$ terraform apply
...
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:
vault-public-ip = 192.0.2.0

The Terraform output will display the public IP address to SSH into your server.

For example:

$ ssh -i "/path/to/EC2/private_key.pem" ubuntu@192.0.2.0

Task 4: Initial setup of the Chef server takes several minutes. Once you can SSH into your mgmt server, run tail -f /var/log/tf-user-data.log to see when the initial configuration is complete.

$ tail -f /var/log/tf-user-data.log

When you see the following message, the initial setup is complete.

+ echo '2018/03/27 21:53:06 /var/lib/cloud/instance/scripts/part-001: Complete'

You can find the following subfolders in your home directory:

Step 2: Initialize and Unseal Vault

Before moving on, set your working environment variables in your mgmt server:

$ export VAULT_ADDR=http://127.0.0.1:8200
$ export VAULT_SKIP_VERIFY=true

Before you can do anything in Vault, you need to initialize and unseal it. Perform one of the following:

Step 3: AppRole Setup

First, initialize and unseal the Vault server using a shortcut.

Refer to the online documentation for initializing and unsealing Vault for more details.

# Initialize the Vault server and write out the unseal keys and root token into files
$ curl --silent \
       --request PUT \
       --data '{"secret_shares": 1, "secret_threshold": 1}' \
       ${VAULT_ADDR}/v1/sys/init | tee \
       >(jq -r .root_token > /home/ubuntu/vault-chef-approle-demo/root-token) \
       >(jq -r .keys[0] > /home/ubuntu/vault-chef-approle-demo/unseal-key)

# Unseal vault
$ vault operator unseal $(cat /home/ubuntu/vault-chef-approle-demo/unseal-key)

# Set the root token to VAULT_TOKEN env var
$ export VAULT_TOKEN=$(cat /home/ubuntu/vault-chef-approle-demo/root-token)

In the next few steps, you will create a number of policies and tokens within Vault. Below is a table that summarizes them:

PolicyDescriptionToken Attachment
app-1-secret-readSets the policy for the final token that will be delivered via the AppRole loginNone. This will be delivered to the client upon AppRole login
app-1-approle-roleid-getSets the policy for the token that you'll give to Terraform to deliver the RoleID (only)roleid-token
terraform-token-createThe Terraform Vault provider doesn't use the token supplied to it directly. This is to prevent the token from being exposed in Terraform's state file. Instead, the Token given to Terraform needs to have the capability to create child tokens with short TTLs. See here for more inforoleid-token
app-1-approle-secretid-createSets the policy for the token that you'll store in the Chef Data Bag. This will only be able to pull our AppRole's SecretIDsecretid-token

These setups only need to be performed upon initial creation of an AppRole, and would typically be done by a Vault administrator.

Now that you have your Vault server unsealed, you can begin to set up necessary policies, AppRole auth method, and tokens.

Task 1: Set up our AppRole policy

This is the policy that will be attached to secret zero which you are delivering to our application (app-1).

CLI command

# Policy to apply to AppRole token
$ tee app-1-secret-read.hcl <<EOF
path "secret/app-1" {
  capabilities = ["read", "list"]
}
EOF

# Create the app-1-secret-read policy in Vault
$ vault policy write app-1-secret-read app-1-secret-read.hcl

API call using cURL

# Policy to apply to AppRole token
$ tee app-1-secret-read.json <<EOF
{"policy":"path \"secret/app-1\" {capabilities = [\"read\", \"list\"]}"}
EOF

# Create the app-1-secret-read policy in Vault
$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request PUT \
       --data @app-1-secret-read.json \
       $VAULT_ADDR/v1/sys/policies/acl/app-1-secret-read

Task 2: Enable the AppRole authentication method

CLI command

$ vault auth enable -description="Demo AppRole auth method" approle

API call using cURL

# Payload for invoking sys/auth API endpoint
$ tee approle.json <<EOF
{
  "type": "approle",
  "description": "Demo AppRole auth method"
}
EOF

# Enable AppRole auth backend
$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request POST \
       --data @approle.json \
       $VAULT_ADDR/v1/sys/auth/approle

Task 3: Configure the AppRole

Now, you are going to create an AppRole role named app-1.

CLI command

# TTL is set to 10 minutes, and Max TTL to be 30 minutes
$ vault write auth/approle/role/app-1 policies="app-1-secret-read" \
        token_ttl="10m" token_max_ttl="30m"

API call using cURL

# Payload containing AppRole auth method configuration
# TTL is set to 10 minutes, and Max TTL to be 30 minutes
$ tee app-1-approle-role.json <<EOF
{
    "role_name": "app-1",
    "bind_secret_id": true,
    "secret_id_ttl": "10m",
    "secret_id_num_uses": "1",
    "token_ttl": "10m",
    "token_max_ttl": "30m",
    "period": 0,
    "policies": [
        "app-1-secret-read"
    ]
}
EOF

# AppRole backend configuration
$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request POST \
       --data @app-1-approle-role.json \
       $VAULT_ADDR/v1/auth/approle/role/app-1

Step 4: Configure Tokens for Terraform and Chef

Now, you're ready to configure the policies and tokens to Terraform and Chef to interact with Vault. Remember, the point here is that you are giving each system a limited token that is only able to pull either the RoleID or SecretID, but not both.

AppRole auth method workflow

Task 1: Create a policy and token for Terraform

Create a token with appropriate policies allowing Terraform to pull the RoleID from Vault:

CLI command

# Policy file granting to retrieve RoleID from Vault
$ tee app-1-approle-roleid-get.hcl <<EOF
path "auth/approle/role/app-1/role-id" {
  capabilities = [ "read" ]
}
EOF

# Create the app-1-approle-roleid-get policy in Vault
$ vault policy write app-1-approle-roleid-get app-1-approle-roleid-get.hcl

# For Terraform
# See: https://www.terraform.io/docs/providers/vault/index.html#token
# Policy granting to create tokens required by Terraform
$ tee terraform-token-create.hcl <<EOF
path "auth/token/create" {
  capabilities = [ "update" ]
}
EOF

# Create the app-1-approle-roleid-get policy in Vault
$ vault policy write terraform-token-create terraform-token-create.hcl

# Get token and save it in roleid-token.txt
$ vault token create -policy="app-1-approle-roleid-get" -policy="terraform-token-create" \
      -metadata="user"="terraform-user" > roleid-token.txt

The token and associated metadata will be written out to the file roleid-token.txt. The token value is what you'll give to Terraform. The file should look similar to the following:

$ cat roleid-token.txt
Key                  Value
---                  -----
token                s.BRzI82jp20nKSjwAKxaclwYr
token_accessor       59ZPmZFyqyAb1aSrwxqpjKaC
token_duration       768h
token_renewable      true
token_policies       ["app-1-approle-roleid-get" "default" "terraform-token-create"]
identity_policies    []
policies             ["app-1-approle-roleid-get" "default" "terraform-token-create"]
token_meta_user      terraform-user

API call using cURL

# Policy file granting to retrieve RoleID from Vault
$ tee app-1-approle-roleid-get.hcl <<EOF
{"policy":"path \"auth/approle/role/app-1/role-id\" {capabilities = [\"read\"]}"}
EOF

# Create the app-1-approle-roleid-get policy in Vault
$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request PUT \
       --data @app-1-approle-roleid-get.hcl \
       $VAULT_ADDR/v1/sys/policies/acl/app-1-approle-roleid-get

# For Terraform
# See: https://www.terraform.io/docs/providers/vault/index.html#token
# Policy granting to create tokens required by Terraform
$ tee terraform-token-create.hcl <<EOF
{"policy":"path \"/auth/token/create\" {capabilities = [\"update\"]}"}
EOF

# Create the app-1-approle-roleid-get policy in Vault
$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request PUT \
       --data @terraform-token-create.hcl \
       $VAULT_ADDR/v1/sys/policies/acl/terraform-token-create

# Payload to configure token for Terraform to pull RoleID
$ tee roleid-token-config.json <<EOF
{
  "policies": [
    "app-1-approle-roleid-get",
    "terraform-token-create"
  ],
  "meta": {
    "user": "terraform-demo"
  },
  "ttl": "720h",
  "renewable": true
}
EOF

# Get token and save it in roleid-token.json
$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request POST \
       --data @roleid-token-config.json \
       $VAULT_ADDR/v1/auth/token/create > roleid-token.json

The token and associated metadata will be written out to the file roleid-token.json. The client_token value is what you'll give to Terraform. The file should look similar to the following:

$ cat roleid-token.json | jq

{
  "request_id": "6b2a8c09-205a-d6d3-2856-77c850f6f1f6",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": null,
  "wrap_info": null,
  "warnings": null,
  "auth": {
    "client_token": "s.5cn5F88OuViRZcb0nscWppRU",
    "accessor": "39TTLfw3Gpa6bFTlvgToMaoa",
    "policies": [
      "app-1-approle-roleid-get",
      "default",
      "terraform-token-create"
    ],
    "token_policies": [
      "app-1-approle-roleid-get",
      "default",
      "terraform-token-create"
    ],
    "metadata": {
      "user": "terraform-demo"
    },
    "lease_duration": 2592000,
    "renewable": true,
    "entity_id": "",
    "token_type": "service"
  }
}

Task 2: Create a policy and token for Chef

Create a token with appropriate policies allowing Chef to pull the SecretID from Vault:

CLI command

# Policy file granting to retrieve SecretID
$ tee app-1-approle-secretid-create.hcl <<EOF
path "auth/approle/role/app-1/secret-id" {
  capabilities = [ "update" ]
}
EOF

# Create the app-1-approle-secretid-create policy in Vault
$ vault policy write app-1-approle-secretid-create app-1-approle-secretid-create.hcl

# Get token for Chef to get SecretID from Vault and store it in secretid-token.txt
$ vault token create -policy="app-1-approle-secretid-create" \
      -metadata="user"="chef-demo" > secretid-token.txt

The resulting file should look like this:

$ cat secretid-token.txt

Key                  Value
---                  -----
token                s.6ReXzkvsTPhS1UX4aWrCFs0h
token_accessor       4swdSbjwUvyaNVAq3WVmHXtF
token_duration       768h
token_renewable      true
token_policies       ["app-1-approle-secretid-create" "default"]
identity_policies    []
policies             ["app-1-approle-secretid-create" "default"]
token_meta_user      chef-demo

API call using cURL

# Policy file granting to retrieve SecretID
$ tee app-1-approle-secretid-create.hcl <<EOF
{"policy":"path \"auth/approle/role/app-1/secret-id\" {capabilities = [\"update\"]}"}
EOF

# Create the app-1-approle-secretid-create policy in Vault
$ curl --silent \
    --location \
    --header "X-Vault-Token: $VAULT_TOKEN" \
    --request PUT \
    --data @app-1-approle-secretid-create.hcl \
    $VAULT_ADDR/v1/sys/policies/acl/app-1-approle-secretid-create

# Payload to invoke auth/token/create endpoint
$ tee secretid-token-config.json <<EOF
{
  "policies": [
    "app-1-approle-secretid-create"
  ],
  "meta": {
    "user": "chef-demo"
  },
  "ttl": "720h",
  "renewable": true
}
EOF

# Get token for Chef to get SecretID from Vault and store it in secretid-token.json
$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request POST \
       --data @secretid-token-config.json \
       $VAULT_ADDR/v1/auth/token/create > secretid-token.json

The resulting file should look like this:

$ cat secretid-token.json | jq
{
  "request_id": "6780c7b1-f8de-067e-423e-8f4e54fd8c27",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": null,
  "wrap_info": null,
  "warnings": null,
  "auth": {
    "client_token": "s.5h3Kiuyj4FmA9UWQC7jR5rau",
    "accessor": "2goTm93Vv4EBGoXg8RoFadnF",
    "policies": [
      "app-1-approle-secretid-create",
      "default"
    ],
    "token_policies": [
      "app-1-approle-secretid-create",
      "default"
    ],
    "metadata": {
      "user": "chef-demo"
    },
    "lease_duration": 2592000,
    "renewable": true,
    "entity_id": "",
    "token_type": "service"
  }
}

Step 5: Save the Token in a Chef Data Bag

At this point, you have a client token generated for Terraform and another for Chef server to log into Vault. For the sake of simplicity, you can put the Chef's client token (secretid-token.json) in a Data Bag which is fine because this token can only retrieve SecretID from Vault which is not much of a use without a corresponding RoleID.

Now, create a Chef Data Bag and put the SecretID token (secretid-token.json) along with the rest of its metadata.

$ cd /home/ubuntu/vault-chef-approle-demo/chef/

# Use the path for where you created this file in the previous step
# You're just adding an 'id' field to the file as that's a required field for data bags
$ cat /home/ubuntu/secretid-token.json | jq --arg id approle-secretid-token '. + {id: $id}' > secretid-token.json

$ knife data bag create secretid-token

$ knife data bag from file secretid-token secretid-token.json

$ knife data bag list

$ knife data bag show secretid-token

$ knife data bag show secretid-token approle-secretid-token

The last step should show the following output:

$ knife data bag show secretid-token approle-secretid-token

WARNING: Unencrypted data bag detected, ignoring any provided secret options.
auth:
  accessor:       2goTm93Vv4EBGoXg8RoFadnF
  client_token:   s.5h3Kiuyj4FmA9UWQC7jR5rau
  entity_id:
  lease_duration: 2592000
  metadata:
    user: chef-demo
  policies:
    app-1-approle-secretid-create
    default
  renewable:      true
  token_policies:
    app-1-approle-secretid-create
    default
  token_type:     service
data:
id:             approle-secretid-token
lease_duration: 0
lease_id:
renewable:      false
request_id:     6780c7b1-f8de-067e-423e-8f4e54fd8c27
warnings:
wrap_info:

Step 6: Write Secrets

Let's write some test data in the secret/app-1 path so that the target app will have some secret to retrieve from Vault at a later step.

CLI command

# Write some demo secrets
$ vault write secret/app-1 username="app-1-user" password="$up3r$3cr3t!"

# Verify that you can read back the data:
$ vault read secret/app-1
Key                 Value
---                 -----
refresh_interval    768h
password            $up3r$3cr3t!
username            app-1-user

API call using cURL

# Write some demo secrets
$ tee demo-secrets.json <<'EOF'
{
  "username": "app-1-user",
  "password": "$up3r$3cr3t!"
}
EOF

$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request POST \
       --data @demo-secrets.json \
       $VAULT_ADDR/v1/secret/app-1

# Verify that you can read back the data:
$ curl --silent \
       --location \
       --header "X-Vault-Token: $VAULT_TOKEN" \
       --request GET \
       $VAULT_ADDR/v1/secret/app-1 | jq
{
  "request_id": "1f73c7ee-27fa-bad0-9c77-b330eef1ea88",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 2764800,
  "data": {
    "password": "$up3r$3cr3t!",
    "username": "app-1-user"
  },
  "wrap_info": null,
  "warnings": null,
  "auth": null
}

Phase 2: Provision our Chef Node to Show AppRole Login

To complete the demo, run the chef-node Terraform configuration to see how everything talks to each other.

Task 1: Change the working directory

Open another terminal on your host machine (not the mgmt-node) and cd into the identity/vault-chef-approle/terraform-aws/chef-node directory:

$ cd identity/vault-chef-approle/terraform-aws/chef-node

Task 2: Update terraform.tfvars.example

Replace the variable values in terraform.tfvars.example to match your environment and save it as terraform.tfvars like you have done at Step 1.

Note the following:

  • Update the vault_address and chef_server_address variables with the IP address of our mgmt-node from above.
  • Update the vault_token variable with the RoleID token from Task 1 in Step 4.
    • If you ran the demo-setup.sh script (Option 1), retrieve the client_token in the /home/ubuntu/vault-chef-approle-demo/roleid-token.json file:
$ cat ~/vault-chef-approle-demo/roleid-token.json | jq ".auth.client_token"

Task 3: Run Terraform

Perform a terraform init to pull down the necessary provider resources. Then terraform plan to verify your changes and the resources that will be created. If all looks good, then perform a terraform apply to provision the resources.

The Terraform output will display the public IP address to SSH into your server.

At this point, Terraform will perform the following actions:

  • Pull a RoleID from our Vault server
  • Provision an AWS instance
  • Write the RoleID to the AWS instance as an environment variable
  • Run the Chef provisioner to bootstrap the AWS instance with our Chef Server
  • Run our Chef recipe which will install NGINX, perform our AppRole login, get our secrets, and output them to our index.html file

AppRole auth method workflow

The Chef recipe can be found at identity/vault-chef-approle/chef/cookbooks/vault_chef_approle_demo/recipes/default.rb.

...
# Configure address for Vault Gem
Vault.address = ENV['VAULT_ADDR']

# Get AppRole RoleID from our environment variables (delivered via Terraform)
var_role_id = ENV['APPROLE_ROLEID']

# Get Vault token from data bag (used to retrieve the SecretID)
vault_token_data = data_bag_item('secretid-token', 'approle-secretid-token')

# Set Vault token (used to retrieve the SecretID)
Vault.token = vault_token_data['auth']['client_token']

# Get AppRole SecretID from Vault
var_secret_id = Vault.approle.create_secret_id('app-1').data[:secret_id]
...

NOTE: If terraform apply fails with recipe compilation error, run the following command manually on the provisioned Chef client node:

$ sudo chef-client -j "/etc/chef/first-boot.json" -E "_default"

Task 4: Verification

Once Terraform completes the apply operation, it will output the public IP address of our new server. You can plug that IP address into a browser to see the output. It should look similar to the following:

Chef output

Help and Reference