If you’ve heard about HashiCorp Vault but never actually sat down and used it hands-on, this guide is for you.

Vault is one of those tools where the concept sounds complicated — secrets management, authentication backends, policies — but once you run your first commands and see it working, everything clicks into place.

In this tutorial, we’re going to skip the theory-heavy intros and get straight into practical usage. By the time you’re done, you’ll have a fully working Vault setup where a real user logs in with a username and password, gets restricted by a policy, and can create, read, update, and delete secrets — all from the CLI.


What You’ll Learn

By the end of this tutorial, you’ll know how to:

  • Start a local Vault dev server and understand its structure
  • Enable and use the KV (Key-Value) secrets engine
  • Enable Userpass authentication and create users
  • Write and apply Vault policies to control access
  • Perform full CRUD operations on secrets using the Vault CLI
  • Log in as a non-root user and verify policy enforcement

Prerequisites

Before we dive in, make sure you have the following:

  • Vault CLI installed — grab it from the official HashiCorp releases page
  • A terminal (Linux, macOS, or WSL on Windows all work)
  • Basic familiarity with the command line

To verify Vault is installed:

1
vault version

You should see something like:

1
Vault v1.17.0 (...)

The Big Picture — What We’re Building

Here’s a quick diagram of what we’ll set up:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌─────────────────────────────────────────────────────┐
│ Vault Server (dev) │
│ │
│ ┌────────────────┐ ┌──────────────────────────┐ │
│ │ Userpass Auth │ │ KV Secrets Engine │ │
│ │ (auth/userpass│ │ (secret/myapp/*) │ │
│ │ /devuser) │ │ │ │
│ └───────┬────────┘ └──────────────────────────┘ │
│ │ ▲ │
│ ▼ │ │
│ ┌───────────────┐ ┌─────────┴──────────────┐ │
│ │ Token Issued │─────▶│ Policy: myapp-policy │ │
│ └───────────────┘ │ (read/write/delete on │ │
│ │ secret/data/myapp/*) │ │
│ └────────────────────────┘ │
└─────────────────────────────────────────────────────┘

A user (devuser) logs in via Userpass auth, receives a token governed by myapp-policy, and that policy controls exactly what secrets paths they can touch.


Step 1: Start the Vault Dev Server

Vault ships with a built-in dev mode that runs entirely in memory — perfect for learning and local testing. It auto-unseals and provides a root token.

1
vault server -dev

You’ll see a wall of output. Pay attention to these two important lines:

1
2
Unseal Key: <some-long-base64-string>
Root Token: hvs.xxxxxxxxxxxxxxxxxxxx

Keep this terminal open. The dev server runs in the foreground.

Now open a second terminal and set your environment variables so the CLI knows where to connect:

1
2
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='hvs.xxxxxxxxxxxxxxxxxxxx' # paste your root token here

Verify the connection:

1
vault status

Expected output:

1
2
3
4
5
6
7
8
9
Key             Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.17.0
...

Sealed: false means Vault is running and ready.

Important: The dev server stores nothing to disk. When you stop it, everything is wiped. It is not for production use.


Step 2: Enable the KV Secrets Engine

Vault organises secrets under secrets engines — think of them as mount points, each with their own behaviour. The KV (Key-Value) engine is the simplest: store arbitrary key/value pairs at a given path.

In dev mode, a KV v2 engine is already mounted at secret/. Let’s verify:

1
vault secrets list

Output:

1
2
3
4
5
6
Path          Type         Accessor              Description
---- ---- -------- -----------
cubbyhole/ cubbyhole cubbyhole_xxxxxxxx per-token private secret storage
identity/ identity identity_xxxxxxxx identity store
secret/ kv kv_xxxxxxxx key/value secret storage
sys/ system system_xxxxxxxx system endpoints used for control, policy and debugging

The secret/ path is already there. If you’re working on a real (non-dev) Vault server, you’d enable it manually:

1
2
# Only needed on non-dev servers
vault secrets enable -path=secret kv-v2

Step 3: Create Your First Secret

Let’s write a secret before we set up users and policies, just to get comfortable with the syntax.

With KV v2, the CLI command is:

1
2
3
4
vault kv put secret/myapp/config \
db_host="db.internal.example.com" \
db_port="5432" \
db_name="myappdb"

Output:

1
2
3
4
5
6
7
8
9
10
11
======= Secret Path =======
secret/data/myapp/config

======= Metadata =======
Key Value
--- -----
created_time 2026-06-24T10:00:00.000000000Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1

Notice the secret was stored at secret/data/myapp/config — KV v2 automatically adds /data/ to the path. This matters when writing policies (more on that in Step 5).

Read it back to confirm:

1
vault kv get secret/myapp/config

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
======== Secret Path ========
secret/data/myapp/config

======= Metadata =======
Key Value
--- -----
created_time 2026-06-24T10:00:00.000000000Z
...
version 1

====== Data ======
Key Value
--- -----
db_host db.internal.example.com
db_name myappdb
db_port 5432

Step 4: Enable Userpass Authentication

By default, Vault only has the token auth method enabled. For human users, Userpass is the simplest: a plain username + password login.

Enable it:

1
vault auth enable userpass

Output:

1
Success! Enabled userpass auth method at: userpass/

Now create a user. We’ll call them devuser with password devpassword123:

1
2
3
vault write auth/userpass/users/devuser \
password="devpassword123" \
policies="myapp-policy"

Output:

1
Success! Data written to: auth/userpass/users/devuser

We’ve assigned myapp-policy to this user — that policy doesn’t exist yet. We’ll write it in the next step.

Note: In a real environment, always use strong, randomly generated passwords and store them securely. Never hardcode credentials in scripts.

Verify the user was created:

1
vault read auth/userpass/users/devuser

Output:

1
2
3
4
5
6
7
8
9
10
11
Key                        Value
--- -----
policies [myapp-policy]
token_bound_cidrs []
token_explicit_max_ttl 0s
token_max_ttl 0s
token_no_default_policy false
token_num_uses 0
token_period 0s
token_ttl 0s
token_type default

Step 5: Write the Vault Policy

A policy in Vault is an HCL (HashiCorp Configuration Language) document that says: on these paths, allow these operations. Without a policy granting access, everything is denied by default.

We want devuser to be able to:

  • Create and update secrets under secret/myapp/*
  • Read secrets under secret/myapp/*
  • Delete secrets under secret/myapp/*
  • List what secrets exist at secret/myapp/

Create the policy file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cat > myapp-policy.hcl << 'EOF'
# Allow full CRUD on myapp secrets
path "secret/data/myapp/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

# Allow listing the myapp metadata path
path "secret/metadata/myapp/*" {
capabilities = ["list", "read", "delete"]
}

# Allow listing the top-level myapp path
path "secret/metadata/myapp" {
capabilities = ["list"]
}
EOF

Why secret/data/ and secret/metadata/?
KV v2 splits every secret into two internal sub-paths: /data/ holds the actual values, and /metadata/ holds versioning information. Your policy must cover both if you want full control.

Now upload this policy to Vault:

1
vault policy write myapp-policy myapp-policy.hcl

Output:

1
Success! Uploaded policy: myapp-policy

Confirm it was stored:

1
vault policy read myapp-policy

You’ll see the policy contents printed back.


Step 6: Log In as devuser

Now let’s switch from using the root token to actually logging in as devuser. This is the realistic scenario — real users shouldn’t ever touch the root token.

1
2
3
vault login -method=userpass \
username=devuser \
password=devpassword123

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 hvs.CAESIxxxxxxxxxxxxxx
token_accessor xxxxxx
token_duration 768h
token_renewable true
token_policies ["default" "myapp-policy"]
identity_policies []
policies ["default" "myapp-policy"]
token_meta_username devuser

Notice token_policies includes myapp-policy — that’s the policy we attached to this user. The CLI automatically saved this token, so all subsequent commands run with devuser‘s permissions.


Step 7: CRUD Operations as devuser

Now the fun part. Let’s run through Create, Read, Update, and Delete operations as devuser, and verify that the policy correctly controls access.

Create

Write a new secret:

1
2
3
4
vault kv put secret/myapp/database \
username="appuser" \
password="supersecretdbpassword" \
host="postgres.internal:5432"

Output:

1
2
3
4
5
6
7
8
9
======= Secret Path =======
secret/data/myapp/database

======= Metadata =======
Key Value
--- -----
created_time 2026-06-24T10:05:00.000000000Z
...
version 1

Read

Read the secret back:

1
vault kv get secret/myapp/database

Output:

1
2
3
4
5
6
====== Data ======
Key Value
--- -----
host postgres.internal:5432
password supersecretdbpassword
username appuser

To get a specific field only:

1
vault kv get -field=password secret/myapp/database

Output:

1
supersecretdbpassword

This is useful in scripts where you only need one value.

Update

Updating a KV v2 secret creates a new version automatically — old versions are kept by default:

1
2
3
4
vault kv put secret/myapp/database \
username="appuser" \
password="newrotatedpassword" \
host="postgres.internal:5432"

Output:

1
2
3
4
5
======= Metadata =======
Key Value
--- -----
...
version 2

Check the version history:

1
vault kv metadata get secret/myapp/database

Output:

1
2
3
4
5
6
7
8
========== Metadata ==========
Key Value
--- -----
cas_required false
created_time 2026-06-24T10:05:00.000000000Z
current_version 2
...
versions map[1:... 2:...]

Retrieve a specific version:

1
vault kv get -version=1 secret/myapp/database

This returns the original password from version 1 — that’s the versioning power of KV v2.

Patch (Partial Update)

If you only want to update one field without rewriting the whole secret, use patch:

1
2
vault kv patch secret/myapp/database \
password="anotherrotatedpassword"

Output:

1
2
3
4
5
======= Metadata =======
Key Value
--- -----
...
version 3

Delete

Delete the current version of a secret:

1
vault kv delete secret/myapp/database

Output:

1
Success! Data deleted (if it existed) at: secret/data/myapp/database

This is a soft delete in KV v2. The data is marked as deleted but not destroyed — you can still access old versions or undelete it. To permanently destroy a version:

1
vault kv destroy -versions=1,2 secret/myapp/database

To delete all versions and the metadata:

1
vault kv metadata delete secret/myapp/database

Step 8: Verify Policy Enforcement

Let’s confirm the policy actually blocks devuser from accessing paths they shouldn’t.

Try reading a secret outside the allowed path:

1
vault kv get secret/someother/path

Output:

1
2
3
4
5
6
7
Error reading secret/data/someother/path: Error making API request.

URL: GET http://127.0.0.1:8200/v1/secret/data/someother/path
Code: 403. Errors:

* 1 error occurred:
* permission denied

403 permission denied — exactly as expected. The policy does its job.

Now try listing all secrets (not just under myapp/):

1
vault kv list secret/

Output:

1
2
3
4
5
6
Error listing secret/metadata/: Error making API request.

URL: GET http://127.0.0.1:8200/v1/secret/metadata?list=true
Code: 403. Errors:

* permission denied

devuser can only see what’s inside secret/myapp/, nothing else. Security working correctly.

List what’s allowed

1
vault kv list secret/myapp/

Output:

1
2
3
Keys
----
config

Only config shows up (we deleted database earlier). The user has full visibility within their allowed scope and zero visibility outside it.


Step 9: Useful Commands Cheat Sheet

Here’s a quick reference table for all the commands we covered:

Action Command
Start dev server vault server -dev
Check status vault status
Enable secrets engine vault secrets enable -path=secret kv-v2
Write a secret vault kv put secret/path key=value
Read a secret vault kv get secret/path
Read one field vault kv get -field=key secret/path
Update a secret vault kv put secret/path key=newvalue
Patch one field vault kv patch secret/path key=newvalue
Delete current version vault kv delete secret/path
Destroy versions vault kv destroy -versions=N secret/path
Delete all metadata vault kv metadata delete secret/path
List secrets vault kv list secret/path
Enable userpass auth vault auth enable userpass
Create a user vault write auth/userpass/users/NAME password=X policies=Y
Log in as user vault login -method=userpass username=X password=Y
Write a policy vault policy write NAME FILE.hcl
Read a policy vault policy read NAME
List policies vault policy list

Things to Keep in Mind

  • Never run the dev server in production. It stores nothing to disk, has no TLS, and restarts with a new root token each time. Use a properly initialized, TLS-enabled Vault cluster for production.

  • KV v2 vs KV v1 paths matter. When writing policies, KV v2 stores data under /data/ and metadata under /metadata/. Forgetting this is the most common policy mistake beginners make.

  • Rotate the root token. In production, generate a root token only when needed for emergency operations, then revoke it afterwards. Use more narrowly scoped tokens for day-to-day tasks.

  • Token TTLs matter. The devuser token in this example has a 768-hour TTL. In production, set much shorter TTLs and use token renewal or short-lived dynamic secrets where possible.

  • Audit logging. In production, always enable an audit log (vault audit enable file file_path=/var/log/vault_audit.log). Every request and response is logged, which is essential for compliance and debugging.


Conclusion

You’ve just walked through the full Vault workflow end to end:

  • Started a dev Vault server and connected the CLI to it
  • Enabled the KV v2 secrets engine and understood why the /data/ and /metadata/ path split exists
  • Enabled Userpass authentication and created a real user
  • Wrote a policy using HCL that grants precisely scoped access to one secrets path
  • Performed full CRUD — create, read, update (including patch), and delete — on KV secrets
  • Logged in as a non-root user and confirmed that the policy correctly blocks access outside the allowed scope

This is the foundation that every Vault deployment builds on. From here you can explore dynamic secrets (where Vault generates short-lived credentials for databases, AWS, and more), PKI secrets for certificates, and Transit for encryption-as-a-service.

But it all starts with understanding exactly what we covered here: secrets engines, auth methods, and policies. Master those three concepts and you’ve unlocked most of what makes Vault powerful.

Happy Coding


Author: Codingtricks
Tags: HashiCorp Vault DevOps Security Secrets Management CLI
Category: DevOps