Bash and Curl for Secrets

July 4, 2019

There often comes a time where you have to make a decision to take a dependency on another piece of software or just write it yourself because you are a firm believer in NIH, and know that you can do it better, faster, and cheaper than anyone else. After all, writing code is easier than reading it.

Maybe it’s not that simple, but you want an excuse to learn something new. You could write a self-contained go app to do it, but then you’d need to create, unit tests, a devops pipeline, and a way for other people to get your binary. I like to use docker for most of those things, and I think I’ll make another post about that. But for now, you don’t want to write unit tests or anything extra, so you decide you’ll just put the code you want in a bash script.

Here’s the scenario. You have a hacked up piece of software that needs to access files from a file share. You’re running this software in Linux docker containers on Kubernetes because you like to use the newest shiny toys. You’re authoring the YAML using Kustomize, because you’re opinionated and think Helm is an anti-pattern. You need to store the file share secret in a YAML file because that’s how the kubernetes azure file share volume mounter reads them, but you don’t want to check secrets into git. But you want to check everything into git so that how to do it isn’t lost with your .bash_history. So you write a bash script to download those secrets and generate the YAML as a part of an existing azure devops pipeline. That’s because you didn’t find any other solutions when searching online and it’s bothersome to read documentation for some existing solution like Sealed Secrets or kustomize-sops.

First you need to figure out how to get KeyVault secrets from a bash script. Thankfully, there is a handy tool called Postman that allows you to practice sending HTTP requests and then grab the curl syntax, which then you put into your bash script. You decide to use Argbash to generate some code for reading arguments because you don’t remember bash syntax, but then you quickly remove the parts you don’t like to clear the evidence you used a tool to generate the code for your script (the other half was generated by Postman, remember?). You come up with something like this and it works on the first try because you’re awesome:

#!/bin/bash
set -eo pipefail

_arg_client_id=
_arg_client_secret=
_arg_tenant_id=
_arg_keyvault_secret=

die()
{
	local _ret=$2
	test -n "$_ret" || _ret=1
	test "$_PRINT_HELP" = yes && print_help >&2
	echo "$1" >&2
	exit ${_ret}
}

print_help ()
{
	printf '%s\n' "Get a keyvault secret"
	printf 'Usage: %s [--client-id <arg>] [--client-secret <arg>] [--tenant-id <arg>] [--keyvault-secret <arg>] [-h|--help]\n' "$0"
	printf '\t%s\n' "--client-id: The AAD service principal client id with access to key vault (no default)"
	printf '\t%s\n' "--client-secret: The AAD service principal client secret (no default)"
	printf '\t%s\n' "--tenant-id: The AAD tenant of the service principal with access (no default)"
	printf '\t%s\n' "--keyvault-secret: The keyvault secret to get (no default)"
	printf '\t%s\n' "-h,--help: Prints help"
}

parse_commandline ()
{
	while test $# -gt 0
	do
		_key="$1"
		case "$_key" in
			--client-id)
				test $# -lt 2 && die "Missing value for the argument '$_key'." 1
				_arg_client_id="$2"
				shift
				;;
			--client-id=*)
				_arg_client_id="${_key##--client-id=}"
				;;
			--client-secret)
				test $# -lt 2 && die "Missing value for the argument '$_key'." 1
				_arg_client_secret="$2"
				shift
				;;
			--client-secret=*)
				_arg_client_secret="${_key##--client-secret=}"
				;;
			--tenant-id)
				test $# -lt 2 && die "Missing value for the argument '$_key'." 1
				_arg_tenant_id="$2"
				shift
				;;
			--tenant-id=*)
				_arg_tenant_id="${_key##--tenant-id=}"
				;;
			--keyvault-secret)
				test $# -lt 2 && die "Missing value for the argument '$_key'." 1
				_arg_keyvault_secret="$2"
				shift
				;;
			--keyvault-secret=*)
				_arg_keyvault_secret="${_key##--keyvault-secret=}"
				;;
			-h|--help)
				print_help
				exit 0
				;;
			-h*)
				print_help
				exit 0
				;;
			*)
				_PRINT_HELP=yes die "FATAL ERROR: Got an unexpected argument '$1'" 1
				;;
		esac
		shift
	done
}

parse_commandline "$@"

[ "$_arg_client_id" ] || die "--client-id is required"
[ "$_arg_client_secret" ] || die "--client-secret is required"
[ "$_arg_tenant_id" ] || die "--tenant-id is required"
[ "$_arg_keyvault_secret" ] || die "--keyvault-secret is required"

_access_token=$(curl -X POST --silent \
  "https://login.windows.net/$_arg_tenant_id/oauth2/token" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode "client_id=$_arg_client_id" \
  --data-urlencode "client_secret=$_arg_client_secret" \
  --data-urlencode "resource=https://vault.azure.net" \
  | jq -r '.access_token')

[ "$_access_token" ] || die "access token is empty"
[ "$_access_token" == "null" ] && die "access token is null"

curl -X GET --silent \
  "$_arg_keyvault_secret?api-version=7.0" \
  -H 'Accept: application/json' \
  -H "Authorization: Bearer $_access_token" \
   | jq -r '.value'

Then you run it like this (values changed):

./test.sh --client-id d8a5093f-c3ec-48b5-a2e9-34db83bdfe96 \
          --client-secret '7jBtPvNrUo+f............*oW_?842' \
          --tenant-id 99f9d6f8-aaaa-aaaa-aaaa-3f0edc53ad25 \
          --keyvault-secret https://mykvtest.vault.azure.net/secrets/hihi/5017f40564074d8cbd08b248637566f2

Remember to set VS Code to use LF endings if you’re running on Windows!