Vault is a tool for securely accessing secrets. In this guide we'll be standing up development instances of Vault and Consul to illustrate features such as;
- Dynamic secret generation
- LDAP authentication
- Policy based authorization
This guide assumes familiarity with command line execution, making HTTP API requests, and basic Docker usage.
Command line examples are taken from a bash shell. Adjust as necessary for your platform. The $
is used to denote command execution and set it apart from it's output. Exclude it when running the commands.
Please have the following available on your system before you begin.
- Hashicorp Vault
- Hashicorp Consul
- HTTP Client for API development, such as Postman or Curl
- Docker
While Docker is not required for the early parts of this guide, it will be required to setup an LDAP server to demonstrate LDAP authentication, and when extending this setup for later demonstrations.
If you have Docker, start by creating an overlay network for our services. Let's call it hashicorp
.
Create an overlay network (if using Docker)
$ docker network create hashicorp
Consul is a distributed key-value (kv) store. It has many components to extend it's kv store to multiple use cases, such as; service discovery, health checking, and dynamic configuration. You can read more about it here.
For this guide, Consul will be used to demonstrate Vault's ability to generate dynamic secrets. The generated tokens and their privileges will be based on group membership of the requesting user.
Dynamic secrets are generated when they are a requested and have a limited duration in which they are valid. This Secrets as a Service model simplifies key-rolling and the built-in revoation limits exposure to lost or stolen secrets.
Let's start by bringing up a single Consul server in development mode.
WARNING: This section will setup a single node Consul cluster in development mode. Development mode does not persist any data upon shutdown. It is meant for quick prototyping. It is not to be used in production.
$ cat > consul-config.json
{
"datacenter": "dc1",
"acl_datacenter": "dc1",
"acl_master_token": "root",
"acl_agent_token": "root",
"acl_default_policy": "deny",
"acl_down_policy": "extend-cache"
}
^C
This configuration will enable Access Control Lists (ACL) and set root
as a mater token. For production deployments, the master token should be complex and kept in a safe location. You can read more about Consul configuration here
Using Docker
$ docker run -d --name consul-server --network hashicorp -v $PWD/consul-config.json:/consul/config/server.json -p 8500:8500 -p 8600:8600/udp consul
Without Docker
$ consul agent -dev -config-file=consul-config.json
$ export CONSUL_HTTP_ADDR=http://127.0.0.1:8500
$ export CONSUL_HTTP_TOKEN=root
$ curl -XPUT --data '{ "Name": "vault", "Type": "management" }' "$CONSUL_HTTP_ADDR/v1/acl/create?token=$CONSUL_HTTP_TOKEN"
{
"ID": "b23eeb23-2f28-8469-83d5-fba99b728f9a"
}
This token will be used in the next section to allow Vault to dynamically create Consul authentication tokens.
Using Docker
$ cat > vault-config.json
storage "consul" {
address = "consul-server:8500",
token = "b23eeb23-2f28-8469-83d5-fba99b728f9a",
path = "vault",
scheme = "http"
}
^C
Without Docker
$ cat > vault-config.json
disable_mlock = true
storage "consul" {
address = "127.0.0.1:8500",
token = "b23eeb23-2f28-8469-83d5-fba99b728f9a",
path = "vault",
scheme = "http"
}
^C
token
is the Consul management token created in the earlier Setup Consul section.
For production deployments, scheme
should be set to https
for secure communication between Vault and Consul.
Using Docker
$ docker run -d --name vault-server --network hashicorp --cap-add=IPC_LOCK -v $PWD/vault-config.json:/vault/config/server.json -p 8200:8200 vault
Without Docker
$ vault server -dev -config=vault-config.json
During initial startup, Vault will display its Unseal Key
and Root Token
.
The Unseal Key
is required to decrypt the Vault instance itself. A sealed Vault is completely inaccessible until unsealed. Suffice to say all data contained within a Vault will be irrovocably lost without the Unseal Key(s).
The Root Token
is the master access token which has full privileges. The Root Token
should only be used during initial setup in order to create additional privileged keys.
It is critical that both the Unseal Key(s)
and Root Token
are kept in a safe location for production deployments.
For this guide, we have started our Vault server in development mode which, in addition to skipping some initial setup, automatically starts the service with an unsealed vault. For this reason, we will only need the Root Token
.
Using Docker
$ docker logs vault-server
If you started Vault without Docker, the Root Token
will be displayed during service startup.
$ export VAULT_ADDR=http://127.0.0.1:8200
$ export VAULT_ROOT_TOKEN=MY_VAULT_ROOT_TOKEN_VALUE
Authenticate with the Root Token
for initial configuration
$ vault login $VAULT_ROOT_TOKEN
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token f10c32be-690f-3ff3-70e0-6cc3b289cb5a
token_accessor f4c3b0e3-4b41-9404-a9b3-914405b7373d
token_duration ∞
token_renewable false
token_policies [root]
Dynamic secrets can be enabled for services with supported secret engines. A full list can be seen here
$ vault secrets enable consul
Success! Enabled the consul secrets engine at: consul/
$ vault write consul/config/access address=consul-server:8500 token=b23eeb23-2f28-8469-83d5-fba99b728f9a
Success! Data written to: consul/config/access
If you are not using Docker, replace consul-server
with 127.0.0.1:8500
The token value is the Consul managemnt token created in the earlier section.
We'll be creating two roles, sysadmins
and developers
, which will map to the authenticating user's group membership. The policy will be attached to the dynamically created Consul authentication token to assign privilieges. You can read more on Consul ACL system here.
Create a sysadmins
role with write
access to any key
in Consul.
$ vault write consul/roles/sysadmins policy=$(echo -n 'key "" { policy = "write" }' | base64 -)
Success! Data written to: consul/roles/sysadmins
Create a developers
role with read
access to any key
in Consul.
$ vault write consul/roles/developers policy=$(echo -n 'key "" { policy = "read" }' | base64 -)
Success! Data written to: consul/roles/developers
Request a Consul authentication token for both the developers
and sysadmins
roles.
$ vault read consul/creds/developers
Key Value
--- -----
lease_id consul/creds/developers/676dc883-3587-e41e-61bc-0c1879150483
lease_duration 768h
lease_renewable true
token 4a604fd2-bc96-a5d4-1d93-1ab6fc79d10e
$ vault read consul/creds/sysadmins
Key Value
--- -----
lease_id consul/creds/sysadmins/3ff82430-c099-ae38-b09f-06c444ae5ca0
lease_duration 768h
lease_renewable true
token 3ca6576c-bd92-7a08-1f08-f18c3703949c
Notice that the tokens are created with a lease to limit the valid period of each token. Upon expiry, Vault will automatically revoke the tokens in Consul. Read more about token leases here.
$ curl -s "$CONSUL_HTTP_ADDR/v1/acl/list?token=$CONSUL_ROOT_TOKEN"
[
{
"ID": "3ca6576c-bd92-7a08-1f08-f18c3703949c",
"Name": "Vault sysadmins root 1526043919535409250",
"Type": "client",
"Rules": "key \"\" { policy = \"write\" }",
"CreateIndex": 267,
"ModifyIndex": 267
},
{
"ID": "4a604fd2-bc96-a5d4-1d93-1ab6fc79d10e",
"Name": "Vault developers root 1526043718144862257",
"Type": "client",
"Rules": "key \"\" { policy = \"read\" }",
"CreateIndex": 252,
"ModifyIndex": 252
},
{
"ID": "anonymous",
"Name": "Anonymous Token",
"Type": "client",
"Rules": "",
"CreateIndex": 4,
"ModifyIndex": 4
},
{
"ID": "b23eeb23-2f28-8469-83d5-fba99b728f9a",
"Name": "vault",
"Type": "management",
"Rules": "",
"CreateIndex": 21,
"ModifyIndex": 21
},
{
"ID": "root",
"Name": "Master Token",
"Type": "management",
"Rules": "",
"CreateIndex": 5,
"ModifyIndex": 5
}
]
Next, take the sysadmins
and developers
authentication token and verify its Consul ACL priviliges by reading and writing a value into Consul. This can be done through the Consul HTTP API or with the Consul CLI. I'll list both as a brief example.
Using the HTTP API
$ curl -s -XPUT --data 'http://www.google.ca' "$CONSUL_HTTP_ADDR/v1/kv/proxies/webproxy01?token=4a604fd2-bc96-a5d4-1d93-1ab6fc79d10e"
Permission denied
Using the Consul command
$ consul kv put -token=4a604fd2-bc96-a5d4-1d93-1ab6fc79d10e proxies/webproxy01 http://www.google.ca
Error! Failed writing data: Unexpected response code: 403 (Permission denied)
Using the HTTP API
$ curl -s -XPUT --data 'http://www.google.ca' "$CONSUL_HTTP_ADDR/v1/kv/proxies/webproxy01?token=3ca6576c-bd92-7a08-1f08-f18c3703949c"
true
Using the Consul command
$ consul kv put -token=3ca6576c-bd92-7a08-1f08-f18c3703949c proxies/webproxy01 http://www.google.ca
Success! Data written to: proxies/webproxy01
Using the HTTP API
$ curl "$CONSUL_HTTP_ADDR/v1/kv/proxies/webproxy01?token=4a604fd2-bc96-a5d4-1d93-1ab6fc79d10e"
[
{
"LockIndex": 0,
"Key": "proxies/webproxy01",
"Flags": 0,
"Value": "aHR0cDovL3d3dy5nb29nbGUuY2E=",
"CreateIndex": 322,
"ModifyIndex": 322
}
]
Using the Consul command
$ consul kv get -token=4a604fd2-bc96-a5d4-1d93-1ab6fc79d10e proxies/webproxy01
http://www.google.ca
So far we've stood up distributed kv store (Consul) and means to dynamically provision role based access (Vault).
Next we'll focus on authentication, mapping users to the sysadmins
and developers
roles we've previously created.
We'll accomplish this by standing up an OpenLDAP server, seeding it with some user accounts and groups, and connecting it to Vault.
In this section we'll be using Docker and the osixia/openldap image to quickly start the OpenLDAP server. If you do not have Docker available on your system you can install it by going here, otherwise you may use another available LDAP service and make adjustments to this guide as needed.
Create a seed.ldif
file with the following contents
dn: ou=users,dc=example,dc=org
changeType: add
objectClass: organizationalUnit
description: Organization users
ou: users
dn: ou=groups,dc=example,dc=org
changeType: add
objectClass: organizationalUnit
description: Organization groups
ou: groups
dn: cn=sysadmins,ou=groups,dc=example,dc=org
changeType: add
objectClass: groupOfUniqueNames
description: System Admin role
cn: sysadmins
uniqueMember: uid=dfayden,ou=users,dc=example,dc=org
uniqueMember: uid=etirel,ou=users,dc=example,dc=org
dn: cn=developers,ou=groups,dc=example,dc=org
changeType: add
objectClass: groupOfUniqueNames
description: Developer role
cn: developers
uniqueMember: uid=gjura,ou=users,dc=example,dc=org
uniqueMember: uid=jbeleren,ou=users,dc=example,dc=org
dn: uid=dfayden,ou=users,dc=example,dc=org
changeType: add
objectClass: inetOrgPerson
description: Self-proclaimed "greatest thief in the Multiverse."
sn: Fayden
givenName: Dack
cn: Dack Fayden
uid: dfayden
mail: [email protected]
memberOf: cn=sysadmins,ou=groups,dc=example,dc=org
userPassword: dfayden!
dn: uid=etirel,ou=users,dc=example,dc=org
changeType: add
objectClass: inetOrgPerson
description: Left her embattled homeland in search of a new place to call home.
sn: Tirel
givenName: Elspeth
cn: Elspeth Tirel
uid: etirel
mail: [email protected]
memberOf: cn=sysadmins,ou=groups,dc=example,dc=org
userPassword: etirel!
dn: uid=gjura,ou=users,dc=example,dc=org
changeType: add
objectClass: inetOrgPerson
description: Fiercely loyal, unyielding, just and charismatic.
sn: Jura
givenName: Gideon
cn: Gideon Jura
uid: gjura
mail: [email protected]
memberOf: cn=developers,ou=groups,dc=example,dc=org
userPassword: gjura!
dn: uid=jbeleren,ou=users,dc=example,dc=org
changeType: add
objectClass: inetOrgPerson
description: Brilliant, curious, and always in control.
sn: Beleren
givenName: Jace
cn: Jace Beleren
uid: jbeleren
mail: [email protected]
memberOf: cn=developers,ou=groups,dc=example,dc=org
userPassword: jbeleren!
$ docker run -d --name ldap-server --network hashicorp -v $PWD/seed.ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom/seed.ldif osixia/openldap:1.2.0 --copy-service
$ docker exec ldap-server ldapsearch -x -H ldap://localhost -b dc=example,dc=org -D "cn=admin,dc=example,dc=org" -w admin
$ vault auth enable ldap
Success! Enabled ldap auth method at: ldap/
$ vault write auth/ldap/config \
url="ldap://ldap-server" \
userdn="ou=users,dc=example,dc=org" \
userattr="uid" \
groupdn="ou=groups,dc=example,dc=org" \
groupfilter="(&(objectClass=groupOfUniqueNames)(uniqueMember={{.UserDN}}))" \
groupattr="cn" \
binddn="cn=admin,dc=example,dc=org" \
bindpass="admin"
Success! Data written to: auth/ldap/config
Next we'll be creating two Vault policies, consul-devs
and consul-sysadmins
to control what type of authentication token can be requested.
Create a file called consul-devs.json
with the following contents
path "consul/creds/developers" {
capabilities = ["read"]
}
Create a file called consul-sysadmins.json
with the following contents
path "consul/creds/sysadmins" {
capabilities = ["read"]
}
Add the policies in Vault
$ vault policy write consul-devs /tmp/consul-devs.json
Success! Uploaded policy: consul-devs
$ vault policy write consul-sysadmins /tmp/consul-devs.json
Success! Uploaded policy: consul-sysadmins
$ vault write auth/ldap/groups/developers policies=consul-devs
Success! Data written to: auth/ldap/groups/developers
$ vault write auth/ldap/groups/sysadmins policies=consul-sysadmins
Success! Data written to: auth/ldap/groups/sysadmins
Login as a user with developer membership
$ vault login -method=ldap username=gjura password=gjura!
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token 955524f4-b3b8-b9da-03ab-f5789f462c2b
token_accessor 65269a37-f826-0abb-f691-f6c281fa316e
token_duration 768h
token_renewable true
token_policies [consul-devs default]
token_meta_username gjura
A few things to note;
- Vault login tokens are also controlled by a lease to force users to reauthenticate periodically.
- The
token
is the primary authentication token for the user, and is used to perform Vault actions allowed by theirtoken_policies
. - The
token_accessor
is a special authentication token that only allows limited actions; 1) looking up token properties and 2) revoking the token. This is useful for services leveraging Vault to simply revoke user tokens without requiring high level Vault privilegs.
Test the consul-devs
policy by requesting a developers and sysadmins Consul authentication token
$ vault read consul/creds/developers
Key Value
--- -----
lease_id consul/creds/developers/afbb7a27-efc6-5199-9f15-f3df274c82ab
lease_duration 768h
lease_renewable true
token afd34e9a-ac88-58ca-45c2-8dd1172daf40
$ vault read consul/creds/sysadmins
Error reading consul/creds/sysadmins: Error making API request.
URL: GET http://127.0.0.1:8200/v1/consul/creds/sysadmins
Code: 403. Errors:
* permission denied
Verify that the generated developer token has the correct Consul ACL applied; read access, but no write access.
$ curl -s "$CONSUL_HTTP_ADDR/v1/kv/proxies/?token=afd34e9a-ac88-58ca-45c2-8dd1172daf40&recurse=true"
[
{
"LockIndex": 0,
"Key": "proxies/webproxy01",
"Flags": 0,
"Value": "aHR0cDovL3d3dy5nb29nbGUuY2E=",
"CreateIndex": 322,
"ModifyIndex": 322
}
]
$ curl -s -XPUT --data 'http://www.bing.com' "$CONSUL_HTTP_ADDR/v1/kv/proxies/webproxy02?token=afd34e9a-ac88-58ca-45c2-8dd1172daf40"
Permission denied
Perform the same tests to verify the consul-sysadmins
policy after logging in as a user that is a member of the sysadmins
group.