HashiCorp Vault with AppRole
Securing Secrets using Vault and AppRole Auth
Security is an essential and a core part of operations and thus keeping secrets secured is vital. Unfortunately, for many an organization, this is often not a priority.
The lack of zeal toward managing secrets is likely related toward the complexity involved. Managing configuration artifacts have well established patterns using change configuration (Puppet, Chef, Ansible, Salt Stack) tools, using service discovery with KV stores (etcd, Consul, Zookeeper), or through simpler means like environment vars and config files.
When the configuration artifacts are secrets, called secrets artifacts, you have to not only encrypt the secrets, but also secure who or what has access to these secrets. If the secret is ever compromised, you need a centralized location to recycle the secret artifacts. For this problem, Hashicorp Vault is one of the most popular solution for this.
Securing secrets with Vault
The secret artifacts can be secured in HashiCorp Vault using the AppRole method.
In this guide, I will walk you through the following:
- Setting up a Vault server using Docker Compose†
- Setting up KV Secrets v2 store and the AppRole method
- Creating a admin role that will be used to create secrets and an
dgraph
application role (see below) - Testing access to the secrets using the
dgraph
application role - Configuration the application Dgraph (see below) to use the
dgraph
application role. - Walk-through in testing features (ACL and encrypted export and backup) enabled through Dgraph
vault
configuration.
† NOTE: Docker Compose is used as an example to get the ball rolling. With this, you can see how the services fit together, and then with this knowledge move this to your own environment, such as Systemd, Swarm, or Kubernetes.
For this guide, the current stable version 1.7.0
will be used:
export VAULT_VERSION="1.7.0"
About Dgraph
For the demonstration application, this guide will use Dgraph, a highly performant distributed graph database. Two features used are Encryption At Rest and Access Control Lists. These features require secret artifacts to be saved on disk, which is well, not all that secure. Fortunately, Dgraph supports fetching secret artifacts directly from Vault.
Dgraph has two types of servers: Dgraph Zero nodes (Raft consensus group 0
) that control state of the cluster and Dgraph Alpha nodes (Raft consensus group 1
) that are the actual database nodes. For simplicity, we will just use a single Dgraph Zero node and a single Dgraph Alpha node.
For this guide, the current stable version v21.03.0
will be used:
export DGRAPH_VERSION="v21.03.0"
Prerequisites
These instructions will use shell commands that are tested in Bash.
- Docker and Docker Compose (required)
- curl and jq (required): interact with REST or GraphQL APIs.
- GNU bash, GNU sed and GNU grep (optional): shell and command features will be used in examples.
On macOS, all these tools, including Docker Destkop, can be easily installed with brew
command (see Homebrew).
On Windows 10, the command line tools can be installed with pacman
in the MSYS2 environment. Both MSYS2 and Docker Desktop can be installed with the choco
command (see Chocolatey).
Docker Compose setup
Use the following docker-compose.yaml
configuration for this guide.
For this configuration, save default environment variables for VAULT_VERSION
and DGRAPH_VERSION
can be saved in an .env
file:
cat <<-EOF > .env
DGRAPH_VERSION=v21.03.0
VAULT_VERSION=1.7.0
EOF
Create two directories vault
and dgraph
to store configuration files. We can do this and populate with blank files that will be use later using Bash:
mkdir -p {vault,dgraph}
touch vault/config.hcl \
dgraph/{vault_secret_id,vault_role_id,alpha.yaml}
The file structure should now look like the following:
.
├── .env
├── dgraph
│ ├── alpha.yaml
│ ├── vault_role_id
│ └── vault_secret_id
├── docker-compose.yaml
└── vault
└── config.hcl
Vault service
For the Vault service, use the following configuration and save it as ./vault/config.hcl
:
Once ready, start the Vault service, and only the vault service with:
## launch vault server
docker-compose up --detach "vault"
Unseal Vault Service
Once the Vault service is ready, initialize a fresh new Vault environment with:
## initialize vault and copy secrets down
docker exec -t vault vault operator init
Copy down the secrets from this process, and use it for the unsealing process.
## unseal vault using copied secrets
docker exec -ti vault vault operator unseal
docker exec -ti vault vault operator unseal
docker exec -ti vault vault operator unseal
When finished, in the local session, set these two environment variables up, using the root token from above. These two environment variables will be used in further steps.
export VAULT_ROOT_TOKEN="<root-token>"
export VAULT_ADDRESS="127.0.0.1:8200"
Configure Vault with root privileges
This guide will use Vault’s RESTful API rather than the vault command. This may seem more complex, but it is the easiest way to access the Vault service and also demonstrates how to interact with the Vault server.
These steps will use the root token (VAULT_ROOT_TOKEN
) copied from earlier from initializing the Vault server.
Create AppRole and KV Secrets
Configure AppRole:
curl --silent \
--header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
--request POST \
--data '{"type": "approle"}' \
$VAULT_ADDRESS/v1/sys/auth/approle
Configure KV Secrets v2:
curl --silent \
--header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
--request POST \
--data '{ "type": "kv-v2" }' \
$VAULT_ADDRESS/v1/sys/mounts/secret
Create the admin policy
We need to create a policy that establishes an admin persona. Save the file below as ./vault/policy_admin.hcl
.
We will need to package the policy file in HCL format as JSON. This can be done with sed
(tested with GNU sed):
cat <<EOF > ./vault/policy_admin.json
{
"policy": "$(sed -e ':a;N;$!ba;s/\n/\\n/g' \
-e 's/"/\\"/g' \
vault/policy_admin.hcl)"
}
EOF
Upload the admin
policy:
curl --silent \
--header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
--request PUT --data @./vault/policy_admin.json \
http://$VAULT_ADDRESS/v1/sys/policies/acl/admin
Verify that this was a success:
curl --silent \
--header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
--request GET \
http://$VAULT_ADDRESS/v1/sys/policies/acl/admin | jq
Create the admin role
Now that the admin policy is uploaded, create the admin
role that uses this policy for the admin
persona using the root token:
## create the admin role with an attached policy
curl --silent \
--header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
--request POST \
--data '{
"token_policies": "admin",
"token_ttl": "1h",
"token_max_ttl": "4h"
}' \
http://$VAULT_ADDRESS/v1/auth/approle/role/admin
Verify the admin
role was created successfully:
## verify the role
curl --silent \
--header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
--request GET \
http://$VAULT_ADDRESS/v1/auth/approle/role/admin | jq
Retreive Admin Token
For this point forward, we will use the admin
persona to setup secrets and the application persona called dgraph
. We need to retreive the admin token by logging into Vault with the admin persona.
First fetch the admin
role-id:
VAULT_ADMIN_ROLE_ID=$(curl --silent \
--header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
http://$VAULT_ADDRESS/v1/auth/approle/role/admin/role-id \
| jq -r '.data.role_id'
)
Then fetch the corresponding admin
secret-id:
VAULT_ADMIN_SECRET_ID=$(curl --silent \
--header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
--request POST \
http://$VAULT_ADDRESS/v1/auth/approle/role/admin/secret-id \
| jq -r '.data.secret_id'
)
Using the role id and secret id, get and admin token and save this as the environment variable VAULT_ADMIN_TOKEN
:
export VAULT_ADMIN_TOKEN=$(curl --silent \
--request POST \
--data "{ \"role_id\": \"$VAULT_ADMIN_ROLE_ID\", \"secret_id\": \"$VAULT_ADMIN_SECRET_ID\" }" \
http://$VAULT_ADDRESS/v1/auth/approle/login \
| jq -r '.auth.client_token'
)
Configure Vault with admin persona
For this section, the admin token (VAULT_ADMIN_TOKEN
) will be used to upload secrets and create the dgraph
application policy and role.
Save Secrets
There are two secrets used, the encryption key and HMAC secret file, which can uploaded using example JSON below (saved as ./vault/payload_alpha_secrets.json
):
For the KV Secrets v2, we add a cas
(create-and-set) operation to guard against overwriting existing secrets (see Writing/Reading arbitrary data). Any update to the secrets will require incrementing the cas
number.
When ready, upload the secrets payload:
curl --silent \
--header "X-Vault-Token: $VAULT_ADMIN_TOKEN" \
--request POST \
--data @./vault/payload_alpha_secrets.json \
http://$VAULT_ADDRESS/v1/secret/data/dgraph/alpha | jq
Create Dgraph application policy
The application policy is limited to reading the secrets (save this as ./vault/policy_dgraph.hcl
):
The first step is to package the policy as JSON using sed
(tested with GNU sed):
## convert policies to json format
cat <<EOF > ./vault/policy_dgraph.json
{
"policy": "$(sed -e ':a;N;$!ba;s/\n/\\n/g' \
-e 's/"/\\"/g' \
vault/policy_dgraph.hcl)"
}
EOF
Once converted, upload the dgraph
policy:
## create the dgraph policy
curl --silent \
--header "X-Vault-Token: $VAULT_ADMIN_TOKEN" \
--request PUT --data @./vault/policy_dgraph.json \
http://$VAULT_ADDRESS/v1/sys/policies/acl/dgraph
Verify the dgraph
policy was uploaded:
## verify the policy
curl --silent \
--header "X-Vault-Token: $VAULT_ADMIN_TOKEN" \
--request GET \
http://$VAULT_ADDRESS/v1/sys/policies/acl/dgraph | jq
Create Dgraph application role
Create the dgraph
application role:
## create the dgraph role with an attached policy
curl --silent \
--header "X-Vault-Token: $VAULT_ADMIN_TOKEN" \
--request POST \
--data '{ "token_policies": "dgraph", "token_ttl": "1h", "token_max_ttl": "4h" }' \
http://$VAULT_ADDRESS/v1/auth/approle/role/dgraph
Verify that the role was created:
## verify the role
curl --silent \
--header "X-Vault-Token: $VAULT_ADMIN_TOKEN" \
--request GET \
http://$VAULT_ADDRESS/v1/auth/approle/role/dgraph | jq
Retreive the Dgraph application token
Fetch the dgraph
role-id:
VAULT_DGRAPH_ROLE_ID=$(curl --silent \
--header "X-Vault-Token: $VAULT_ADMIN_TOKEN" \
http://$VAULT_ADDRESS/v1/auth/approle/role/dgraph/role-id \
| jq -r '.data.role_id'
)
Fetch the dgraph
secret-id:
VAULT_DGRAPH_SECRET_ID=$(curl --silent \
--header "X-Vault-Token: $VAULT_ADMIN_TOKEN" \
--request POST \
http://$VAULT_ADDRESS/v1/auth/approle/role/dgraph/secret-id \
| jq -r '.data.secret_id'
)
Retrieve and save the dgraph token as VAULT_DGRAPH_TOKEN
:
export VAULT_DGRAPH_TOKEN=$(curl --silent \
--request POST \
--data "{ \"role_id\": \"$VAULT_DGRAPH_ROLE_ID\", \"secret_id\": \"$VAULT_DGRAPH_SECRET_ID\" }" \
http://$VAULT_ADDRESS/v1/auth/approle/login \
| jq -r '.auth.client_token'
)
Save the dgraph
role-id and secret-id as files to be used with configuring Dgraph:
echo $VAULT_DGRAPH_ROLE_ID > ./dgraph/vault_role_id
echo $VAULT_DGRAPH_SECRET_ID > ./dgraph/vault_secret_id
Verify Secrets with Dgraph application persona
Using the dgraph
token (VAULT_DGRAPH_TOKEN
), verify that secrets can be retreived:
curl --silent \
--header "X-Vault-Token: $VAULT_DGRAPH_TOKEN" \
--request GET \
http://$VAULT_ADDRESS/v1/secret/data/dgraph/alpha | jq
Dgraph Service
The Dgraph Alpha service will need to be configured to use Vault to fetch secrets for the encryption and ACL features. Save the following to ./dgraph/alpha.yaml
:
This configuration is relative to the Dgraph Alpha container, where file paths are mounted into the container, and DNS vault
address is accessed from within the Docker Compose’s implicit private network that is automatically created, which you can view with:
docker network inspect "${PWD##*/}_default" | jq '.[].Containers'
Bring the remaining services up with:
docker-compose up --detach
You can inspect the logs with:
docker logs alpha1
You will want to scan for ACL secret key loaded successfully
, and Encryption feature enabled
in the logs.
Check the health as well as list of features available by querying the /health
path on Dgraph Alpha with:
# print a list of features enabled
curl --silent http://localhost:8080/health \
| jq -r '.[].ee_features | .[]' \
| sed 's/^/* /' \
| grep --color --extended-regexp 'acl|encrypt.*|$'
The full feature list with features enabled by vault
configuration in ./dgraph/alpha.yaml
highlighted:
* acl
* multi_tenancy
* encryption_at_rest
* encrypted_backup_restore
* encrypted_export
* cdc
Testing Dgraph features
This area demonstrates how to directly test the Dgraph ACL (access control lists) and encyption at rest features when enabled through the vault
configuration.
For the Dgraph ACL(access control lists) feature, we just need to simply login and retreive an access JWT token, which is required to trigger the other features. For the export and backup operations, we need to supply a the token with the X-Dgraph-AccessToken
field in the header. If ACL is not enabled, this token will be ignored and not used.
For Dgraph encyption at rest feature, we can verify encryption is working performing a backup or export operations of the Dgraph database. From the file system, we should see that the contents are encrypted.
Testing Dgraph ACL Feature
The ACL feature is only enabled if a secret was saved. We can tested it by using the default administrative user (groot
) and password (password
) with RESTful request to /login
path:
DGRAPH_ADMIN_USER="groot"
DGRAPH_ADMIN_PSWD="password"
export DGRAPH_ALPHA_ADDRESS="localhost:8080"## login using RESTful API
export DGRAPH_ACCESS_TOKEN=$(curl --silent \
--request POST \
--data "{
\"userid\": \"$DGRAPH_ADMIN_USER\",
\"password\": \"$DGRAPH_ADMIN_PSWD\",
\"namespace\": 0
}" \
http://$DGRAPH_ALPHA_ADDRESS/login \
| grep -oP '(?<=accessJWT":")[^"]*'
)
When ACLs are enabled with Dgraph, administrative tasks like binary backups or exports will need have an access JWT tokens.
The environment variables DGRAPH_ACCESS_TOKEN
and DGRAPH_ALPHA_ADDRESS
will be used for other tests.
Testing Dgraph export operation
The export feature will export the whole Dgraph database in RDF or JSON formats. When encyption at rest is enabled, the exported database will be encypted.
For this administrative chore, we will create a GraphQL query and save it as ./dgraph/export.graphql
:
After this, using the access token from earlier, we can trigger an export to the /admin
path.
curl --silent \
--header "Content-Type: application/graphql" \
--header "X-Dgraph-AccessToken: $DGRAPH_ACCESS_TOKEN" \
--request POST \
--upload-file ./dgraph/export.graphql \
http://$DGRAPH_ALPHA_ADDRESS/admin | jq
This will create an export file structure similar to:
./dgraph/export
└── dgraph.r8.u0419.1810
├── g01.gql_schema.gz
├── g01.json.gz
└── g01.schema.gz
You can get information about these files with:
find ./dgraph/export/ -name '*.gz' \
| xargs -n 1 file | awk -F/ '{print $NF}'
If these files are encrypted SUCCESSFULLY then the result should say:
g01.schema.gz: data
g01.json.gz: data
g01.gql_schema.gz: data
Otherwise (FAILURE), it will say something like the following:
g01.schema.gz: gzip compressed data, max speed, original size modulo 2^32 381
g01.json.gz: gzip compressed data, max speed, original size modulo 2^32 5
g01.gql_schema.gz: gzip compressed data, max speed, original size modulo 2^32 5
Testing Dgraph backup operation
The binary backups feature will, as expected, backup the Dgraph database with either full or incremental backups. This can be used to restore the database to a Dgraph cluster. When encyption at rest is enabled, the backups will be encrypted.
For this administrative chore, we will create a GraphQL query and save it as ./dgraph/backup.graphql
:
You can trigger a backup with the following command to /admin
path:
curl --silent \
--header "Content-Type: application/graphql" \
--header "X-Dgraph-AccessToken: $DGRAPH_ACCESS_TOKEN" \
--request POST \
--upload-file ./dgraph/backup.graphql \
http://$DGRAPH_ALPHA_ADDRESS/admin | jq
When completed, this will create an file structure similar to:
./dgraph/backups
├── dgraph.20210419.181116.903
│ └── r8-g1.backup
└── manifest.json
You can get information about the backup files with:
find ./dgraph/backups/ -name '*.backup' \
| xargs -n 1 file | awk -F/ '{print $NF}'
A result that is SUCCESSFUL will show something like the following:
r8-g1.backup: data
If this is not encrypted (FAILURE), then you would see something like this:
r8-g1.backup: snappy framed data
Announcement: Updated Article
I recently created updated version of this article.
One major thing has changed in the Docker platform is that the python tool docker-compose (previously introduced as fig) is deprecated and rewritten in go as a plugin for Docker.
Vault AppRole Auth: The Hard Way
This article mirrors this article in that the REST API is used.
Vault AppRole Auth: The Hard Way
This article uses the Vault CLI, but is otherwise similar to the content in this article.
Resources
The source code files for this tutorial can be found from here:
Conclusion
There you have it, a full overiew of saving secrets using AppRole auth method and KV Secrets v2 store using with an application service, Dgraph, that has direct support for this feature.
If the application(s) you support do not have direct support for Hashicorp Vault, you can use the methods presented here in your automation to get the same functionality, and then provide the secrets to the application in a format it understands, such as environment variables, configuration files, etc.
With the AppRole role, you can also use an bound CIDR list (also called a whitelist in the industry), where you can filter in an address or range of addresses that can access the server. This can be an alternative method to the required secret-id or and additional form of screening along with the secret-id. For example (attention to bind_secret_id
and bound_cidr_list
keys):
curl --silent \
--header "X-Vault-Token: $VAULT_ADMIN_TOKEN" \
--request POST \
--data '{
"token_policies": "dgraph",
"token_ttl": "1h",
"token_max_ttl": "4h",
"bind_secret_id": false,
"bound_cidr_list": [
"10.0.0.0/8",
"172.0.0.0/8",
"192.168.0.0/16",
"127.0.0.1/32"
]
}' \
http://$VAULT_ADDRESS/v1/auth/approle/role/dgraph
Beyond the AppRole auth method, there are several other auth methods, such as these:
- cloud provider solutions (AWS, Google Cloud, Azure),
- integration with Kubernetes,
- classical methods like LDAP, Kerberos, RADIUS,
- through HTTP(S) with TLS certificates, token, or JWT/OIDC,
- integration with Github,
In a follow-up article, I would like to explore deploying an HA Vault cluster using Kubernetes with popular cloud providers, such as EKS, GKE, and AKS. With the cloud providers, you can use the cloud provider’s KMS to auto-unseal the secrets, without the need to run vault operator unseal
several times on all of the vault nodes. If you use a shared backend, such as Consul, you only need to initialize one node with vault operator init
. In a self-managed Kubernetes scenario, you can also use small vault backend to unseal the main HA Vault cluster. This article will be useful to help springboard toward building a HA vault.
I hope this is useful to get your started on your Hashicorp Vault journey. Best of wishes and success.