Provisioning Vault with Code

A couple of years ago, Hashicorp published a blog post “Codifying Vault Policies and Configuration“. We used a heavily modified version of their scripts to get us going with Vault.

However there are a few problems with the approach, some of which are noted in the original post.

The main one is that if we remove a policy from the configuration, applying it again will not remove the objects from Vault. Essentially it is additive only, and while it will modify existing objects and create new ones, removing objects that are no longer declared is arguably just as important.

Another problem is that shell scripts inevitably have dependencies, which you may not want to install on your shell servers. Curl, in particular, is extremely useful for hackers, and we don’t want to have it available in production (in our environment, access to the vault API from outside the network is not allowed).

Finally, shell scripts aren’t easy to test, and don’t scale particularly well as complexity grows. You can do some amazing things in bash, but once it gets beyond a few hundred lines it’s time to break out into a proper language.

So that’s what I did.

The result is a tool called vaultsmith, and it’s designed to do one thing – take a directory of json files and apply them to your vault server.

Being written in Go gives it several advantages over shell scripts:

  • It compiles to a single binary (no curl dependency)
  • It uses the official Vault golang api
  • Authentication can be handled by the vault client, in fact if you do vault login, vaultsmith will also be logged in
  • There are tests! Coverage could be better though.

How it works

Essentially, the directory structure (--document-path) reflects the API endpoints of Vault. Vaultsmith puts the contents of the documents within to Vault, using the built-in Vault client.

While with curl you can PUT the documents no matter what the endpoint, it gets a little more complicated when using the Go client, as endpoints such as sys/auth and sys/policy have special methods. So these directories are assigned handlers, which make the right calls.

This has resulted in some duplication of code. At some point I’d like to design a proper interface for the handlers, but as there aren’t very many of them it didn’t seem like a smart use of time.

Features

  • Fetch configuration from local directory, local tar.gz, or tar.gz via http (adding support for other fetch methods should be fairly simple).
  • Supports passing a github personal access token in the http header.
  • Support for basic templating. Documentation is needed here, but for now check out the example directory for an idea of how it works.

Trying it out

With a golang environment configured (I’d recommend using your OS’s package manager, or homebrew if on Mac OS):

go get github.com/starlingbank/vaultsmith
docker run -p 8200:8200 vault:latest

Copy the root token, and in another shell replace $root_token with it as below:

export VAULT_TOKEN=$root_token
export VAULT_ADDR=http://localhost:8200

Now run vaultsmith with --dry:

vaultsmith --document-path https://raw.githubusercontent.com/starlingbank/vaultsmith/master/example/example.tar.gz

You should see a few lines logged which match the following:

WARN[0000] Placeholder has no values fileName="{{service_role}}.json" placeholder=account_id

Vaultsmith is warning us that we haven’t specified a parameter that is templated. Had we not used --dry here, it would have written the document to vault with {{account_id}} still in the document, but {{service_role}} would still be replaced, as it is specified in _vaultsmith.json.

Try it again, but this time append --template-params account_id=00000:

The warning has disappeared, and now the document written would now contain the specified account_id. If the account ID never changes, it can be specified in _vaultsmith.json, under “variables”.

If you want to see what would actually be written, append --log-level debug and run the command again. You should see lines similar to:

INFO[0000] Applying document handler=Generic path=auth/aws/role/example_role sourceFile=example_role.json
DEBU[0000] No Vault API call made action=Write data="map[max_ttl:1h inferred_entity_type:ec2_instance inferred_aws_region:us-east-1 policies:[read_secrets write_secrets] resolve_aws_unique_ids:true auth_type:iam bound_iam_instance_profile_arn:test:00000]" path=auth/aws/role/example_role readonly=true

At present the “data” field, which contains the document, is printed using Go’s string representation of the object; a potential improvement could be to represent it as json instead.

Now remove --dry and it will apply the example configuration to your local vault instance. If you leave --log-level-debug in place, you’ll see that “No Vault api call made” is replaced by “Calling Vault API“.

Managing the vault data

We keep our vault configuration in its own git repository. As the documents are all in json format, I created a Makefile which does a basic syntax check with jq:

.DEFAULT_GOAL = test

ifndef VERBOSE
REDIRECT= 1>/dev/null
endif

# This breaks if there are any spaces in files names
FILES := $(shell find . -name *.json)

test: all

all: $(FILES)

$(FILES):
    @echo $@
    @jq . $@ $(REDIRECT)


.PHONY: test all $(FILES)

Running make will then syntax-check any json files in the tree, so long as they don’t have any spaces in the file name!

A docker build does the actual testing:

FROM alpine:3.8 as base

RUN apk --no-cache update && \
    apk --no-cache add \
        jq \
        make

FROM base as tester
ADD Makefile /data/
ADD vault-documents /data

WORKDIR /data
RUN make test
ENTRYPOINT ["make"]
CMD ["test"]

Which is run by our CI environment with:

docker build --target tester .

From here it would be relatively simple to combine this with a run of a dev vault container and test applying the documents to a real instance. I haven’ t done this yet though.

Status

At this point I consider the code to be in a beta-quality state. It’s functional, and I’ve used it successfully to deploy our vault configuration in our environments, but it’s highly likely there are use-cases that I haven’t accounted for.

Some things that could be improved:

  • The path handlers need a proper interface. The existing PathHandler interface and BaseHandler struct are fairly simple, but not particularly well designed. It feels that there could be more code reuse between the implementations.
  • At the moment there are no restrictions of where the configuration can be pulled from, or signature verification. Both of these are really basic features in a high-security environment.
  • Vaultsmith will exit with an error if it doesn’t have rights to read and list sys/auth/ or auth/. These should be optional and give a warning rather than error.

If you use the tool and encounter any issues, please raise an issue on github and I’ll do my best to help. Pull requests are of course very welcome!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.