This post is based on my experience writing a plugin for HashiCorp’s Vault. While HashiCorp provides thorough documentation, I found that I had to read the code of several open-source plugins before I grokked a bigger picture. This is my attempt to document details that are not spelled out in HashiCorp’s basic example of a secrets engine.
Vault Paths & Policies
Paths are a key concept of Vault. It’s fundamental to understand how they work, because Vault gets its utility and flexibility largely from the path mechanism. In fact, there’s so much flexibility that documentation can come across as vague. Later in this post I’ll describe paths exposed by a secrets engine that I created, to give a concrete example.
Users interact with each secret engine through a limited number of verbs (i.e. read, write, list, delete) acting on any number of paths. The specific paths may be built-in to the secrets engine, or created by a user, or a combination of both.
Within a plugin, each operation on a path is processed by a handler function. Handlers can store and retrieve data; so, a plugin can expose configuration on some paths and features on others.
Paths are closely tied to policies, the mechanism that controls which users can perform which operations on which paths. A Vault administrator defines policies to assign privileges to users. Simply put, policies promote some users into privileged “operators”, although in practice there may be many nuanced types of privileged user.
The author of a secrets engine, should think not only about what features the engine supports, but also how to divide features across multiple paths - specifically so that the Vault administrator can better isolate privileges to specific users. Typically, this means configuration can be performed only by privileged operators, while features can be accessed by non-operators.
Vault provides administrators an important control over paths and plugins: the root path where the plugin is enabled (or “mounted”). The same plugin can be enabled at multiple root paths. Importantly, each of those root paths has its own storage. What’s nice about this is not only that administrators assign each path its own policies. It’s also that a plugin with only a simple set of options can serve relatively complex needs, by having multiple configurations enabled concurrently on different paths.
The example that follows takes advantage of this. Specifically one mount path is used to store signing keys, while another mount stores multisign accounts of a particular configuration. With this in mind, let’s look at a hypothetical scheme of signing privileges, and show how this particular Vault plugin can be configured to acheive it.
A Secrets Engine for Algorand
To illustrate, consider a secrets engine designed to sign transactions for the Algorand blockchain. The purpose of this plugin is to leverage Vault to restrict access to digital assets on a blockchain.
By design, the plugin presents functionality across multiple paths. Transaction signing takes place on algorand/address/… paths, while security settings are controlled on algorand/config/… paths. The plugin anticipates a logical separation of privilege, to make the administrator’s job easier when defining policies.
Multiple Signer Example
Let’s say a business is running software services that from time to time make Algorand payments. These services are hosted on less trusted servers, so they should not manage the private keys which would allow them to sign arbitrary Algorand transactions. And also, the business requires compliance checks before payments, so the services are allowed to initiate payments, but each must be approved by a risk department before digital assets are transferred.
Here’s how we’ll address each of these constraints:
Our accounts will use a 2-of-2 multisign scheme. One signature from a software service, the other from a member of the risk department.
Software services will use Vault to sign. Each service has a short-lived, revokable Vault access token, instead of a permanent secret signing key.
Members of the risk department will use Vault to put a second signature on each approved transaction.
Once set up, our paths will look something like the figure below.
The following Vault commands create this setup. To follow along, it helps to be familiar with Vault and secrets engines. The details of which are beyond the scope of this blog post.
First, an administrator enables the algorand plugin on two mounts. One mount will hold the risk department’s signing key. Another mount will hold the multisign addresses.
vault secrets enable -path=algorand vault-plugin-algorand vault secrets enable -path=2of2 vault-plugin-algorand
Next, an operator generates the signing key for the risk department. Note that Vault will generate a keypair, and show only the public key (encoded as an Algorand address). The secret key never leaves Vault.
vault write algorand/address/risk/generate
The previous operation generates a keypair named “risk” on the
mount. To refer to the newly generated public key, we’ll use an
export RISK_KEY=$(vault read -field=address algorand/address/risk)
Now, we turn to the more complicated multisign account setup. This is
where the operator configures the
vault write 2of2/config/risk signer=$RISK_KEY vault write 2of2/config threshold=2
The two Vault operations above accomplish several things. All
addresses generated on
2of2/ will be multisign. All of them will
$RISK_KEY as one of the signers. And, the number of signatures
required will be two.
Configred this way, we can create a 2-of-2 account for each software service.
vault write -force 2of2/address/service1
This triggers the plugin to generate a keypair unique to
“service1”. Further, the plugin defines an Algorand multisig account
defined by the newly generated key and
$RISK_KEY. (We use
because no additional fields are required - it’s a Vault thing.)
The setup of the “service1” account is complete. Let’s say the service constructs a transaction (in a file called unsigned.txt). It would produce the first signature via Vault:
vault write -field signed 2of2/address/service1/sign -email@example.com > signed_once.txn
signed_once.txn can be passed to the risk department,
initiating compliance checks. If they pass, then a member of the risk
team would produce the second signature:
vault write -field signed algorand/address/risk/sign -txn=@signed_once.txt > signed_twice.txn
Finally, the signed_twice.txn transaction can be submitted to an Algorand node, and propagated to the blockchain.