Secrets Management

Secrets as a Service: Dynamic Secrets

Vault can generate secrets on-demand for some systems. For example, when an app needs to access an Amazon S3 bucket, it asks Vault for AWS credentials. Vault will generate an AWS credential granting permissions to access the S3 bucket. In addition, Vault will automatically revoke this credential after the TTL is expired.

Challenge

Data protection is a top priority which means that the database credential rotation is a critical part of any data protection initiative. Each role has a different set of permissions granted to access the database. When a system is attacked by hackers, continuous credential rotation becomes necessary and needs to be automated.

Solution

Applications ask Vault for database credential rather than setting them as environment variables. The administrator specifies the TTL of the database credentials to enforce its validity so that they are automatically revoked when they are no longer used.

Dynamic Secret Workflow

Each app instance can get unique credentials that they don't have to share. By making those credentials to be short-lived, you reduced the change of the secret to being compromised. If an app was compromised, the credentials used by the app can be revoked rather than changing more global set of credentials.

Personas

The end-to-end scenario described in this guide involves two personas:

  • admin with privileged permissions to configure secret engines
  • apps read the secrets from Vault

Prerequisites

To perform the tasks described in this guide, you need to have:

  • A Vault environment of version 1.2 or later. Refer to the Getting Started guide to install Vault. Make sure that your Vault server has been initialized and unsealed.

  • A PostgreSQL environment you can connect, or Docker to run a PostgreSQL in a container.

Policy requirements

To perform all tasks demonstrated in this guide, your policy must include the following permissions:

# Mount secret engines
path "sys/mounts/*" {
  capabilities = [ "create", "read", "update", "delete", "list" ]
}

# Configure the database secret engine and create roles
path "database/*" {
  capabilities = [ "create", "read", "update", "delete", "list" ]
}

# Write ACL policies
path "sys/policies/acl/*" {
  capabilities = [ "create", "read", "update", "delete", "list" ]
}

# Manage tokens for verification
path "auth/token/create" {
  capabilities = [ "create", "read", "update", "delete", "list", "sudo" ]
}

If you are not familiar with policies, complete the policies guide.

Steps

In this guide, you are going to configure PostgreSQL secret engine, and create a read-only database role. The Vault generated PostgreSQL credentials will only have read permission.

  1. Enable the database secret engine
  2. Configure PostgreSQL secret engine
  3. Create a role
  4. Request PostgreSQL credentials
  5. Validation

Step 1 through 3 need to be performed by an admin user. Step 4 describes the commands that an app runs to get a database credentials from Vault.

Step 1: Enable the database secret engine

(Persona: admin)

First step is to enable the database secrets engine at a desired path.


CLI command / API call using cURL / Web UI

CLI command

Execute the following command to enable the database secrets engine at database/ path.

$ vault secrets enable database

If you want to enable the secrets engine at a different path, use the -path parameter to specify your desired path. Otherwise, the secrets engine gets enabled at a path named after its type. Since the type is database in this example, its path becomes database in absence of -path parameter.

API call using cURL

Mount database secret engine using /sys/mounts endpoint:

curl --header "X-Vault-Token: <TOKEN>" \
       --request POST \
       --data <PARAMETERS> \
       <VAULT_ADDRESS>/v1/sys/mounts/<PATH>

Where <TOKEN> is your valid token, and <PARAMETERS> holds configuration parameters of the secret engine.

Example:

The following example mounts database secret engine at sys/mounts/database path, and passed the secret engine type ("database") in the request payload.

$ curl --header "X-Vault-Token: ..." \
       --request POST \
       --data '{"type":"database"}' \
       https://127.0.0.1:8200/v1/sys/mounts/database

Web UI

  1. Open a web browser and launch the Vault UI (e.g. http://127.0.0.1:8200/ui) and then login.

  2. Select Enable new engine.

  3. Select Databases from the list, and then click Next. Enabling
database

  4. Click Enable Engine to complete. This sets the path to be database.

Step 2: Configure PostgreSQL secret engine

(Persona: admin)

The PostgreSQL secret engine needs to be configured with valid credentials. It is very common to give Vault the superuser credentials and let Vault manage the auditing and lifecycle credentials; it's much better than having one person manage the credentials.

For the purpose of this tutorial, let's run PostgreSQL Docker image in a container.

Execute the following command to start a postgres instance which listens to port 5432, and the superuser (root) password is set to rootpassword.

# For the purpose of this demo, keeping it simple
$ docker run --name postgres -e POSTGRES_USER=root \
         -e POSTGRES_PASSWORD=rootpassword \
         -d -p 5432:5432 postgres

# Verify that the postgres container is running
$ docker ps
CONTAINER ID        IMAGE            ...         PORTS                    NAMES
befcf913da91        postgres         ...         0.0.0.0:5432->5432/tcp   postgres

The following command configures the database secret engine using postgresql-database-plugin. The allowed role is readonly which you will create in Step 3.


CLI command / API call using cURL

CLI command

Example:

$ vault write database/config/postgresql
        plugin_name=postgresql-database-plugin \
        allowed_roles=readonly \
        connection_url=postgresql://root:rootpassword@localhost:5432/postgres?sslmode=disable

API call using cURL

Example:

# Create an HTTP request payload
$ tee payload.json <<EOF
{
    "plugin_name": "postgresql-database-plugin",
    "allowed_roles": "readonly",
    "connection_url": "postgresql://root:rootpassword@localhost:5432/postgres?sslmode=disable"
}
EOF

# Invoke the database/config/postgresql endpoint
$ curl --header "X-Vault-Token: ..." --request POST --data @payload.json \
       http://127.0.0.1:8200/v1/database/config/postgresql

Step 3: Create a role

(Persona: admin)

In Step 2, you configured the PostgreSQL secret engine by passing readonly role as an allowed member. The next step is to define the readonly role. A role is a logical name that maps to a policy used to generate credentials.

Example: readonly.sql

CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";

The values within the {{<value>}} will be filled in by Vault. Notice that VALID_UNTIL clause. This tells PostgreSQL to revoke the credentials even if Vault is offline or unable to communicate with it.


CLI command / API call using cURL

CLI command

Example:

$ vault write database/roles/readonly db_name=postgresql \
        creation_statements=@readonly.sql \
        default_ttl=1h max_ttl=24h

The above command creates a role named readonly with default TTL of 1 hour, and max TTL of the credential is set to 24 hours. The readonly.sql statement is passed as the role creation statement.

API call using cURL

Example:

# Create the HTTP request payload
$ tee payload.json <<EOF
{
    "db_name": "postgres",
    "creation_statements": ["CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
  GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"],
    "default_ttl": "1h",
    "max_ttl": "24h"
}
EOF

# Invoke the database/roles API endpoint
$ curl --header "X-Vault-Token: ..." \
       --request POST --data @payload.json \
       http://127.0.0.1:8200/v1/database/roles/readonly

The db_name, creation_statements, default_ttl, and max_ttl are set in the role-payload.json.

Step 4: Request PostgreSQL credentials

(Persona: apps)

Now, you are switching to apps persona. To get a new set of PostgreSQL credentials, the client app needs to be able to read from the readonly role endpoint. Therefore the app's token must have a policy granting the read permission.

apps-policy.hcl

# Get credentials from the database secret engine
path "database/creds/readonly" {
  capabilities = [ "read" ]
}

CLI command / API call using cURL / Web UI

CLI command

First create an apps policy, and generate a token so that you can authenticate as an apps persona.

Example:

# Create "apps" policy
$ vault policy write apps apps-policy.hcl
Policy 'apps' written.

# Create a new token with app policy
$ vault token create -policy="apps"

Key                  Value
---                  -----
token                s.NN5Izfj9ok3VuZiaP9N9QJ1V
token_accessor       l1QxSKs80HfrzR1gfy5zm7ay
token_duration       768h
token_renewable      true
token_policies       ["apps" "default"]
identity_policies    []
policies             ["apps" "default"]

Use the returned token to perform the remaining.

# Invoke the vault command with apps token
$ VAULT_TOKEN=s.NN5Izfj9ok3VuZiaP9N9QJ1V vault read database/creds/readonly

Key                Value
---                -----
lease_id           database/creds/readonly/fyF5xDomnKeCHNZNQgStwBKD
lease_duration     1h
lease_renewable    true
password           A1a-ckirtymYaXACpIHn
username           v-token-readonly-6iRIcGv8tLpu816oblPY-1556567086

Re-run the command and notice that Vault returns a different set of credentials each time. This means that each app instance can acquire a unique set of DB credentials.

API call using cURL

First create an apps policy, and generate a token so that you can authenticate as an app persona.

# Create the HTTP request payload
$ tee payload.json <<EOF
{
  "policy": "path \"database/creds/readonly\" {capabilities = [ \"read\" ]}"
}
EOF

# Create "apps" policy
$ curl --header "X-Vault-Token: ..." --request PUT \
       --data @payload.json \
       http://127.0.0.1:8200/v1/sys/policies/acl/apps

# Generate a new token with apps policy
$ curl --header "X-Vault-Token: ..." --request POST \
       --data '{"policies": ["apps"]}' \
       http://127.0.0.1:8200/v1/auth/token/create | jq
{
   ...
   "auth": {
     "client_token": "s.qbloIGiHddxc3chdtE2TrGX7",
     "accessor": "8zrb4SIVJ4DMZUsfr6uRsqdu",
     "policies": [
       "apps",
       "default"
     ],
     "token_policies": [
       "apps",
       "default"
     ],
     ...
   }
}

Be sure to use the returned token to perform the remaining.

$ curl --header "X-Vault-Token: s.qbloIGiHddxc3chdtE2TrGX7" \
       --request GET \
       http://127.0.0.1:8200/v1/database/creds/readonly | jq
{
   ...
   "data": {
     "password": "A1a-cxHgJAjdnyDrpBiT",
     "username": "v-token-readonly-4pBp6GCvhixU6PPlbgRw-1556567165"
   },
   ...
}

Re-run the command and notice that Vault returns a different set of credentials each time. This means that each app instance can acquire a unique set of DB credentials.

Web UI

  1. Click the Policies tab, and then select Create ACL policy.

  2. Toggle Upload file sliding switch, and click Choose a file to select your apps-policy.hcl file you authored. This loads the policy and sets the Name to be apps.

  3. Click Create Policy to complete.

  4. Click the Vault CLI shell icon (>_) to open a command shell. Execute vault write auth/token/create policies=apps in the CLI shell to create a new token: Create token

  5. Copy the generated client token value.

  6. Sign out of the Vault UI.

  7. Now, sign into the Vault using the newly generated token you just copied.

  8. Click the Vault CLI shell icon (>_) to open a command shell. Execute vault read database/creds/readonly in the CLI shell. DB credentials

Validation

  1. Generate a new set of credentials.

    $ vault read database/creds/readonly
    
    Key                Value
    ---                -----
    lease_id           database/creds/readonly/IQKUMCTg3M5QTRZ0abmLKjTX
    lease_duration     1h
    lease_renewable    true
    password           A1a-T7Ezuy261IBew8H9
    username           v-token-readonly-47vOtpF7pZq79Xajx7yq-1556567237
    

    The generated username is v-token-readonly-47vOtpF7pZq79Xajx7yq-1556567237.

  2. Connect to the postgres as an admin user, and run the following psql commands.

    $ psql -U postgres
    
    postgres > \du
                                                          List of roles
                       Role name                     |                         Attributes                         | Member of
    --------------------------------------------------+------------------------------------------------------------+-----------
    postgres                                         | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
    v-token-readonly-47vOtpF7pZq79Xajx7yq-1556567237 | Password valid until 2018-01-11 00:37:14+00                | {}
    
    postgres > \q
    

    The \du command lists all users. You should be able to verify that the username generated by Vault exists.

  3. Renew the lease for this credential by passing its lease_id.

    $ vault lease renew database/creds/readonly/IQKUMCTg3M5QTRZ0abmLKjTX
    
    Key                Value
    ---                -----
    lease_id           database/creds/readonly/IQKUMCTg3M5QTRZ0abmLKjTX
    lease_duration     1h
    lease_renewable    true
    
  4. Revoke the generated credentials.

    $ vault lease revoke database/creds/readonly/3e8174da-6ca0-143b-aa8c-4c238aa02809
    

    Now, when you check the list of users in PostgreSQL, none of the Vault generated user name exists.

Next Step

There are some tools available to help integrate your applications with Vault's database secrets engine. Using those tools, the existing applications may require minimum to no code change to work with Vault.

Refer to the following guides:

Help and Reference