Skip to main content

Automate Code: Signing with Vault App Roles

Lyndseyferguson Lyndsey Ferguson

Header (5)

Background

As I wrote in “Bring Customers Joy With Automation,” Appian builds custom versions of its mobile application for its customers. To do this, we offer customers the option to send us their code-signing credentials which we secure in Vault, as I described in the article, “Automate Securing Code-Signing Assets.”

The Problem

As I described in the previous article, “Customizing and Code Signing An Existing Mobile App”, our build system can now code-sign mobile builds without us ever seeing the passwords, but we still have to manually get a Vault token and give it to the build system. That is a small burden on us and an unnecessary delay for the customer. It would be much better if our build system could do the whole thing by itself.

The Plan

First, let’s revisit where we left off — we added fastlane code to our build system that customizes and code-signs an existing application. When the customer wants a build, an Engineering employee has to create a Vault token that only the build system can use to get the code-signing assets. The employee would provide that token to a Jenkins job, which would then run the fastlane code to fetch the Appian mobile application, customize it with the customer’s assets, and then use the Vault token to fetch the code-signing assets to sign the mobile app as the customer.

This is where the Vault App Role comes into play. It allows each build machine to authenticate to Vault in a way that restricts it to its attached policy. It does this by logging in to Vault with a “role id” and a “secret id” — think of the “role id” as the username and the “secret id” as the password. By tying the role id to a predefined role, we can define what this “role” can do. In this case, it can “read” a very small set of secrets only on a set of machines in a specific network subnet.

We can improve this process by creating a Trusted Entity server that will periodically change the secret id. This reduces the impact if a malicious actor ever copies the secret id.

A build system that can log itself in and get code-signing assets

To demonstrate how to do this, we’ll use the Vault instance that we set up in “Automate Securing Code-Signing Assets” to create:

  • An App Role in that Vault server.

Note: I am using GitHub Actions rather than Jenkins to demonstrate the CI/CD as it is easier to share the code with you and should be simple to replicate if you want to experiment with this yourself.

The Vault App Role

First, we have to enable the Vault App Role as an auth backend. Assuming that you have run through the “Automate Securing Code Signing Assets” article, navigate to the same directory that the docker-compose.yaml file is in and run:

docker-compose exec -T vault vault auth enable approle

Next, we have to create the actual App Role. In this case, I’m going to use an explicit name that is hard to mistake for anything else: custom-mobile-apps-signer.

docker-compose exec -T vault vault write \
auth/approle/role/custom-mobile-apps-signer \
token_policies=”custom-mobile-apps-read-policy” \
secret_id_ttl=55m \
token_ttl=60m

Let’s walk through what is going on here:

  1. We told Vault to create a new App Role named custom-mobile-apps-signer.

Now that we have the App Role created, we need to do something with it — we need its role id that I mentioned before and put it in a place where only our GitHub Actions Runner can use it.

GitHub Actions

GitHub Actions allow developers to integrate Continuous Integration and Deployment options directly within the GitHub environment instead of connecting third-party systems such as Jenkins or CircleCI. In a way, I consider GitHub Actions to be a response to GitLab’s integrated CI/CD offering.

I will assume that you have some familiarity with GitHub Actions. If not, check out this article, “An Introduction to GitHub Actions.” For our purposes, we’ll connect a GitHub Action to the fastlane code from “Customizing and Code Signing An Existing Mobile App.” This action will run a GitHub Runner that has access to the Vault App Role’s role id and secret id via GitHub Secrets.

Let’s set up those secrets. We want to do this such that a program can automatically update the GitHub Secrets. To do so, we need to create a GitHub Personal Access Token with these permissions. Follow the GitHub instructions to create the GitHub token. If this is for a public repo (which mine is), select the public_repo checkbox — otherwise, select the top repo checkbox.

Give that GitHub token a name (I named mine “Update Public Repo Secret”) and save it locally. I saved it as a file, update-public-repos-token.txt, in the same directory as my Vault example. I added it to the .gitignore file so that I don’t accidentally submit it to GitHub and give you the power to change my public repo 😉.

Creating the Role ID

Now that we have a GitHub token that will allow us to write secrets, we need to actually create them. At this point, we have enabled App Roles and have the policy attached to a role that allows it to get the secrets to code-sign a mobile build.

The next step is to get the role id and then save it in a GitHub Secret so that our GitHub Action runner can use it (and a secret id) to log into Vault and get the code-signing assets. Let’s create the role id and write it as a GitHub Secret. In the Terminal, type:

# note that I use -field=role_id so I get only that value 
# and nothing moreVAULT_ROLE_ID=$(docker-compose exec -T vault vault read \
-field=role_id auth/approle/role/custom-mobile-apps-signer/role-id)

We now have the role id and need to put it in a GitHub Secret. Using the GitHub Secrets API documentation, I created a Ruby script to do that using the GitHub token that we created above in the directory, update_custom_mobile_apps_secret_id (you’ll want to change the repo path to one of your GitHub repos):

 

#!/usr/bin/env ruby
   
  require 'vault'
  require "rbnacl"
  require "base64"
  require 'octokit'
  require 'optparse'
   
  # Replace this with your own GitHub Repo
  GITHUB_REPO = 'lyndsey-ferguson/article-example-customize-existing-mobile-app'
   
  def create_box(public_key)
  b64_key = RbNaCl::PublicKey.new(Base64.decode64(public_key[:key]))
  {
  key_id: public_key[:key_id],
  box: RbNaCl::Boxes::Sealed.from_public_key(b64_key)
  }
  end
   
  def update_secret_value(secret_key, secret_value)
  github_token = File.read(File.join(__dir__, 'update-public-repos-token.txt'))
  github_client = Octokit::Client.new(:access_token => github_token)
  repo = github_client.repository(GITHUB_REPO)
  puts "working with repo: #{repo.url}"
  box = create_box(github_client.get("#{repo.url}/actions/secrets/public-key"))
  encrypted = box[:box].encrypt(secret_value)
  response = github_client.put("#{repo.url}/actions/secrets/#{secret_key}",
  encrypted_value: Base64.strict_encode64(encrypted),
  key_id: box[:key_id]
  )
  end
   
  if __FILE__ == $0
  secret_key = ARGV[0]
  secret_value = ARGV[1]
   
  update_secret_value(secret_key, secret_value)
  end

This script encrypts the secret_value with the public key for the repo and then sends it to the endpoint for the given secret_key. We can run it like this:

chmod a+x update_github_secret_value.rb
./update_github_secret_value.rb VAULT_CODESIGNING_ROLE_ID \
$VAULT_ROLE_ID

Creating a Rotating Secret ID

Next, we need to give our GitHub Action runner the secret id so that it can log in. We don’t want a static long-lived password just in case someone gets a hold of it and causes mayhem over a long period of time. Instead, we want to periodically change, or “rotate”, that password. To do so, we need to create what Hashicorp calls a Trusted Entity. It will have the ability to create a new secret id on a schedule and write it to a GitHub Secret.

For this example, I’ll use a docker container to simulate an independent instance, but in the real world you will want to use a computer that is behind a Virtual Private Network (VPN) alongside the Vault server. Theoretically, the runner should also run in this VPN and only be open to an internal CI/CD system rather than a GitHub public repository (although you could do this with restricted CIDRs).

Let’s look at code that the Trusted Entity will run and then build the Docker container that will run it. In the same directory that you put the update_github_secret_value.rb file in, create the following files:

 

#!/usr/bin/env ruby
   
  require_relative 'update_github_secret_value'
   
  while true do
  secret = Vault.approle.create_secret_id("custom-mobile-apps-signer")
  update_secret_value('VAULT_CODESIGNING_SECRET_ID', secret.data[:secret_id])
  puts "Updated VAULT_CODESIGNING_SECRET_ID"
  STDOUT.flush
  sleep 2700
  end

 

This application is simply an infinite loop that creates a new Vault App Role secret id every 45 minutes and sets the GitHub Secret for it using the script we created earlier.

Next, create the Dockerfile that we will use to build and run the above ruby script:

 

FROM ruby:2.5
   
  ARG VAULT_ADDR
  ARG VAULT_TOKEN
   
  ENV VAULT_ADDR=${VAULT_ADDR}
  ENV VAULT_TOKEN=${VAULT_TOKEN}
  # throw errors if Gemfile has been modified since Gemfile.lock
  RUN bundle config
   
  WORKDIR /usr/src/app
   
  ADD https://download.libsodium.org/libsodium/releases/LATEST.tar.gz ./
  RUN tar -zxvf LATEST.tar.gz
   
  WORKDIR ./libsodium-stable
  RUN ./configure
  RUN make && make check
  RUN make install
  RUN ldconfig
   
  WORKDIR ..
  COPY Gemfile update-public-repos-token.txt ./
  RUN bundle install
   
  COPY . .
   
  ENTRYPOINT ["ruby", "update_custom_mobile_apps_secret_id.rb"]
 

 

This Dockerfile hard-codes VAULT_ADDR to be the IP Address of the Vault server in the Docker container’s network, installs the libraries needed to encrypt the GitHub Secret, and finally copies the Ruby Gemfile and update_custom_mobile_apps_secret_id.rb script into the container.

Here is the Gemfile that accompanies it:

source "https://rubygems.org"

gem 'octokit' # the module to talk to GitHub
gem 'rbnacl' # the module to encrypt GitHub Secrets
gem 'vault' # the module to work with Vault

Now that we have the Dockerfile set up along with the accompanying Gemfile, let’s build it and spin it up so that it can periodically update the GitHub Secret VAULT_CODESIGNING_SECRET_ID.

In the directory update_custom_mobile_apps_secret_id, where the Dockerfile is, run:

# Let’s get the IP Address of the Vault serverDOCKER_VAULT_ADDR=$(docker inspect --format=’{{range \
.NetworkSettings.Networks}}{{.IPAddress}}{{end}}’ vault)# I would suggest not using the root token and the only reason
# I have done so is to keep this article simple and easy to followdocker build . --build-arg VAULT_TOKEN=<root token> \
VAULT_ADDR=$DOCKER_VAULT_ADDR \
-t trusted-entity

Now that we have built the image, we have to run the Trusted Entity in the same network as the Vault server so that it can communicate over that network. Let’s get the network:

VAULT_NETWORK=$(docker inspect --format=’{{range $k,$v := \
.NetworkSettings.Networks}} {{$k}} {{end}}’ vault)

Now that we have the network, let’s spin up the Trusted Entity docker container in that network:

# spin up the Trusted Entity container in the vault network
docker run -d — network $VAULT_NETWORK --name trusted-entity \
trusted-entity# add that container to the network to allow containers to talk to each other
docker network connect vault-net trusted-entity

With the Trusted Entity running, it is time to create a GitHub Actions workflow that can execute a runner with access to Vault and the secrets. Following the GitHub Actions documentation, let’s create such a workflow:

 

name: CI

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]
  workflow_dispatch:
    inputs:
      company_name:
        description: "Name of company"
        required: false
        default: 'acme'

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: self-hosted

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
    # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
    - uses: actions/checkout@v2
      
    - name: install gem dependencies
      run: bundle install
      
    - name: customize an already built ios app
      run: bundle exec fastlane ios customize_built_app
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        VAULT_APPROLE_ROLE_ID: ${{ secrets.VAULT_CODESIGNING_ROLE_ID }}
        VAULT_APPROLE_SECRET_ID: ${{ secrets.VAULT_CODESIGNING_SECRET_ID }}


We have configured this workflow to run when someone pushes to master for convenience; normally, I would have this workflow run only when someone merges to master to ensure security and correctness. The workflow runs the job build, which executes the customize_build_app fastlane to customize and code-sign the application. It code-signs with credentials pulled from Vault using the GitHub Secrets VAULT_CODESIGNING_ROLE_ID and VAULT_CODESIGNING_SECRET_ID authenticate.

The only thing missing now is the actual GitHub runner. Let’s set that up by following Github’s instructions: Adding self-hosted runners. Pay attention to the warning as you don’t want bad code running on your machine — as this is an example, I monitor the runner’s logs and shut down this action when not using it, so I think I’m okay.

Push this up to your branch and then you’ll have your build. Let’s run this manually by navigating to the Actions tab, selecting the build workflow, and then clicking the “Run workflow” button.

This will start the GitHub runner. It will use the GitHub Secrets to log into Vault, get the code-signing assets, build the iOS application, and sign it with the credentials.

Summary

So there you have it. I’ve walked you through how you can set up your own Vault App Roles with credentials that are rotated by a Trusted Entity with GitHub Secrets. A GitHub Action runner uses these Secrets to customize and code-sign different configurations of a mobile application. If you would like to see all the code together, checkout the Github repo lyndsey-ferguson/article-example-vault-approles.

Keep in mind that we’re not done yet, as our Appian Engineer still has to kick off the build. In later articles, I’ll describe how we can make this a self-service system that the customer can do themselves.

Keep an eye out for future articles, and I hope that you enjoyed this one! If you have any questions, please feel free to ask in the Comments section below. If you find this kind of work interesting and want to work with creative, passionate, and smart people, there are plenty of other projects to work on like this at Appian. Apply today!








Lyndseyferguson

Written by

Lyndsey Ferguson

Lyndsey is a Principal Software Engineer at Appian.