تخطَّ إلى المحتوى
العودة إلى أدلة الدارسين
HashiCorp VaultDevOpsSecurity & secrets3 مستويات98 قسمًايغطّي Vault 2.1دليل بالإنجليزية

The Complete HashiCorp Vault Guide

Manage secrets, encryption and dynamic credentials with HashiCorp Vault. Taught at three levels — Beginner, Mid-level and Senior — each with an in-depth guide, interview prep, and practical tips.

التوثيق الرسمي مسودّة بالذكاء الاصطناعي · مراجعة المجتمع جاريةساعدنا في مراجعته
15sections
69examples

This is part one of three. It covers everything you need to do real work with HashiCorp Vault, not a teaser. By the end you can start a server, store and version secrets, write a policy that gives a person or an application exactly the access it needs, log in as that person, hand out short-lived tokens, and read the error messages Vault gives you when something is wrong. Mid-level and Senior take the same topics further; nothing here is thrown away.

The guide targets Vault 2.1.1, released on 16 September 2026. The 2.0 release in April 2026 was the first major version since 1.0, and it changed a few details that older blog posts and tutorials still get wrong. Where that matters, the text says so and shows the current form.

Each section ends with a Try it task. Do them as you go. They take a few minutes each, and Vault only makes sense once you have watched a request get refused with "permission denied" and then fixed it yourself.

What Vault is, and the problem it solves

Vault is a server that stores, generates and hands out secrets, and decides who is allowed to see each one. A secret is anything that grants access or must stay private: a database password, a cloud access key, an API token for a payment provider, a TLS private key, an encryption key. Vault puts all of them behind one HTTP API, requires every caller to prove who they are, checks a written policy on every request, and records who asked for what.

To see why that matters, look at what teams did before.

The first habit is secrets in source code. A developer pastes a database password into a config file, commits it, and pushes. The password is now in every clone and in the history forever. Deleting the line in a later commit removes nothing, because Git remembers. Automated scanners crawl public repositories for exactly these strings within minutes of a push.

The second habit is secrets in environment variables and CI settings. That is better than source code, but the values are still long-lived, shared by everyone who can edit the pipeline, and rarely rotated. When an engineer leaves, nobody knows which of forty variables they had seen. When something leaks, nobody can tell which value leaked, from where, or when.

The third habit is a shared spreadsheet or chat message, which needs no further comment.

Underneath these habits sit four problems that Vault is designed to address together:

  1. Sprawl. Secrets live in dozens of places, so nobody can list them or change them in one go.
  2. Long life. A password created in 2021 and never changed is a standing invitation. The longer a secret lives, the more people have touched it and the more places it has been copied.
  3. No audit trail. When a secret is read from a file, nothing records the read. After an incident you cannot answer "who had access?".
  4. No fine-grained control. Anyone who can read the file can read every secret in it.

Vault's answer is to centralise secrets, put identity and policy in front of every read, record every request in an audit log, and, where it can, stop storing secrets at all. That last idea is the most interesting one. For many systems, Vault can create a credential on demand, for example a fresh database user that expires in an hour, so there is no long-lived password to leak. Later guides in this series cover that in depth; this guide concentrates on the foundations.

CLIENTa person, app or CI job
→
AUTH METHODproves who you are
→
TOKEN + POLICYwhat you may do
→
SECRETS ENGINEstores or generates

That diagram is the whole product. A client logs in through an auth method and receives a token. Policies attached to the token decide which paths the token may touch. Each path leads to a secrets engine, which does the real work. Everything Vault keeps on disk is encrypted. The rest of this guide unpacks each box.

A note on licensing, because you will meet the question at work. Since Vault 1.15 (August 2023) the source code is under the Business Source License 1.1 rather than the older MPL 2.0 license. The free edition you download is called Vault Community Edition, and that is what this guide uses. There is also OpenBao, a separate community fork of the last MPL-licensed code, governed by the Linux Foundation. It is a different product: do not assume its commands and features match Vault's. Finally, HashiCorp is now part of IBM, and you will see the product marketed as "IBM Vault". The command is still vault and the documentation still lives at developer.hashicorp.com/vault.

Try it
  1. Pick one project you know. List every secret it needs to run: database passwords, API keys, signing keys.
  2. For each one, write down where it lives today, who can read it, and when it was last changed.
  3. Mark every secret that has no answer for one of those three questions.
a list with several gaps. Each gap is a problem Vault exists to close, and the list is your shopping list for the rest of this guide.

The mental model: paths, tokens, policies and the seal

Vault has a small vocabulary. Learn these nouns once and every later feature is a variation on them.

Everything is a path

Vault is an HTTP API server, and every feature is reached at a URL of the form /v1/<path>. The CLI is a convenience wrapper that builds those requests for you. A secret stored at secret/myapp/config is really an HTTP request to /v1/secret/data/myapp/config (we will explain the extra data later). Creating a database user is a request to a path under database/. Even Vault's own administration is a path: sys/ is the system backend, and it is where you turn features on, read the health of the server and manage policies.

This design has a useful consequence. Because every action is a read or a write to a path, access control is also about paths. A policy does not say "this person is a database admin". It says "this token may read the path database/creds/app". You will find that once you think in paths, permissions become concrete and testable.

Secrets engines: what lives behind a path

A secrets engine is a component mounted at a path prefix that stores, generates or encrypts data. Mounting an engine at secret/ means every request under secret/ is handed to that engine. You can mount several engines, and you can mount the same kind of engine at different prefixes.

Engines fall into three families:

  • Static secrets. You put a value in, you get the same value out. The KV (key-value) engine is the main example and is what you will use first.
  • Dynamic secrets. Vault creates a credential when asked and deletes it when its lease ends. The database, AWS, GCP, Azure, Kubernetes, SSH and PKI (certificates) engines work this way.
  • Encryption as a service. The Transit engine encrypts and decrypts data for your application without ever giving the application the key.

Auth methods: how callers prove who they are

An auth method verifies an identity and maps it to a set of policies. Auth methods always live under auth/, for example auth/userpass/. A person might log in with a username and password, or through a company single sign-on system (OIDC, LDAP). An application might log in with approle, or with its Kubernetes service account, or with a cloud identity. The method you pick depends on who the caller is, and each method ends the same way: Vault returns a token.

Tokens: the credential you actually carry

The token is the core credential. Every request to Vault, except a few unauthenticated system endpoints, carries a token in the X-Vault-Token header (or as Authorization: Bearer <token>). When you log in, Vault gives you a token; when you use the CLI, it stores that token in a file, ~/.vault-token, and sends it for you.

Tokens you will see begin with a prefix that tells you the type: hvs. for a service token, hvb. for a batch token and hvr. for a recovery token. Before Vault 1.10 the prefixes were s., b. and r., so an old tutorial showing s.abc123 is not wrong, just old. Service tokens are the ordinary kind: stored by Vault, renewable, revocable. Batch tokens are lightweight and cannot be renewed or revoked, which suits very high-volume short jobs.

Every token has a TTL (time to live). When it expires, it stops working. The system default and the system maximum are both 768 hours, which is 32 days; you can tighten them per mount and per role. Tokens form a hierarchy: a token is a child of the token that created it, and revoking a parent revokes all of its children. A token with no parent is an orphan; tokens issued by logging in through an auth method are orphans.

A token also has an accessor, a reference you can use to look up, renew or revoke the token or check its capabilities without knowing the token itself. The accessor cannot be used to authenticate, so it is safe to log.

Policies: what a token may do

A policy is a small document, written in HCL (HashiCorp Configuration Language) or JSON, that lists paths and the capabilities allowed on each. The capabilities are create, read, update, patch, delete, list, sudo and deny. Policies are deny by default: a path not mentioned is forbidden. If two policies disagree, deny always wins.

Two policies exist from the start. default is attached to every token unless you opt out; it grants a few harmless things such as looking up your own token. You can edit it, but not delete it. root is immutable and grants everything.

The root token, and why you should not keep it

The root token carries the root policy and can do anything. You get one at initialisation and you should treat it like the master key to a building: use it once to set things up, then revoke it. Day-to-day work uses tokens with narrow policies. From Vault 2.0 onwards, even generating a replacement root token with vault operator generate-root requires an existing token as well as key shares, which is one more reason to plan your administrative accounts rather than lean on root.

Sealed and unsealed

This is the concept newcomers find strangest. When a Vault server starts, it can reach its storage but cannot read it, because the data is encrypted and the key is not in memory. The server is sealed. Until someone unseals it, it answers almost nothing, and every data request returns an error that says Vault is sealed.

Why design it this way? Because the storage (a disk, or a cloud volume) is treated as untrusted. If someone steals the disk, or a backup, they get ciphertext. The encryption happens in a layer Vault calls the barrier, which uses AES-256-GCM. The key hierarchy has three levels:

  1. Your data is encrypted with an encryption key (the keyring).
  2. The keyring is encrypted with the root key.
  3. The root key is protected by the unseal key, which by default is split into pieces using Shamir's Secret Sharing.

With the default setup, vault operator init creates five key shares and requires any three of them to unseal. You give the shares to different people, so no single person can unseal the server alone. Production systems usually avoid the manual step with auto-unseal, where a cloud key management service (AWS KMS, Azure Key Vault, GCP Cloud KMS and others) protects the root key. In that mode init issues recovery keys instead, which cannot decrypt anything and only authorise sensitive operations.

Sealed is not broken. A sealed Vault is working exactly as designed. After every restart of a production server with Shamir keys, it will be sealed until enough shares are supplied. If an alert says "Vault is sealed", the fix is to unseal it (or repair access to the KMS), not to reinstall.

Leases, mounts and audit

Three more nouns complete the vocabulary. A lease is metadata attached to a dynamic secret (and to service tokens): a lease ID, a TTL and a renewable flag. When the lease ends, Vault revokes the credential. You can revoke leases one at a time or by prefix. A mount is simply an engine or auth method attached to a path; you "enable" one to mount it. An audit device is a log sink (a file, syslog or a socket) that records every request and response, with sensitive values hashed so the log does not itself leak secrets.

Memorise the sentence: a client authenticates through an auth method, receives a token, and its policies decide which paths it may use. Nearly every Vault question, from "why can't I read this?" to "how do I give CI access?", is answered by asking which token, which policy and which path.
Try it
  1. Without looking back, draw the diagram with four boxes: client, auth method, token with policy, secrets engine.
  2. Label where "sealed" applies (it is about the server as a whole, not any one box).
  3. Explain in one sentence why the root token should be revoked after setup.
you can tell the story of one request from login to secret, and you notice that the seal sits underneath everything else.

Installing Vault and checking the setup

Install the single vault binary. The same file is both the server and the command line client.

On macOS, use the HashiCorp tap. The formula in Homebrew's core repository stopped receiving updates after the license change, so a plain brew install vault can give you something stale or nothing at all.

BASH
brew tap hashicorp/tap
brew install hashicorp/tap/vault
vault version

On Ubuntu or Debian, add HashiCorp's signed package repository and install:

BASH
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault

On Red Hat family systems the package repository is added with yum-config-manager (RHEL, CentOS, Amazon Linux) or fetched with wget (Fedora), and then installed with yum or dnf; the exact lines are on the official install page at developer.hashicorp.com/vault/install. On Windows, download the zip from releases.hashicorp.com, place vault.exe in a folder on your PATH, and note that Chocolatey and winget packages exist but are maintained by the community, not by HashiCorp. You can also download the zip for any platform from releases.hashicorp.com/vault/2.1.1/ and verify it against the published SHA256 checksums.

If you prefer not to install anything, Docker works for a disposable server:

BASH
docker run --rm -p 8200:8200 -e VAULT_DEV_ROOT_TOKEN_ID=root hashicorp/vault:2.1.1 server -dev -dev-listen-address=0.0.0.0:8200

Since Vault 2.0 the official container runs as the vault user, and since 2.0.2 the binary no longer has the cap_ipc_lock capability, so a container cannot lock memory. That matters for real deployments, where you set disable_mlock = true; a throwaway dev container does not care.

Verify the install:

BASH
vault version
TEXT
Vault v2.1.1 (abcdef123456), built 2026-09-16T10:00:00Z

The commit hash and build date on your machine will differ. What matters is the version number. Two optional extras are worth doing now. vault -h lists every command, and vault -autocomplete-install adds tab completion to your shell (restart the shell afterwards).

Add the flag -output-curl-string to almost any CLI command and Vault prints the equivalent curl command instead of running it. It is the fastest way to learn the HTTP API, and we will use it later in this guide.
Try it
  1. Install Vault with the method for your system.
  2. Run vault version and confirm it says 2.1.1, or the newest release you can get.
  3. Run vault -h and find three command names you have not heard of. Do not read about them yet.
a version line, and a help screen that looks long but is organised by topic. You will use about a dozen of those commands in this guide.

Starting a development server

Vault has a built-in development mode that gives you a working server in one command. It exists for learning and testing. It is not safe for real use, and the next paragraph explains why.

BASH
vault server -dev

In dev mode Vault stores everything in memory, so all data vanishes when you stop it. It starts already unsealed with a single unseal key. It listens on plain HTTP at 127.0.0.1:8200, with no TLS. It enables a KV version 2 engine at secret/. And it prints a root token in the terminal. Each of those conveniences would be a serious flaw in production, which is why the mode has "dev" in the name.

Leave that terminal running. The output includes lines like these:

TEXT
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variables:

    $ export VAULT_ADDR='http://127.0.0.1:8200'

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Root Token: hvs.xxxxxxxxxxxxxxxxxxxxxxxx

Open a second terminal for the client. The CLI needs two environment variables. VAULT_ADDR tells it where the server is, and this one is required because the CLI defaults to https://127.0.0.1:8200, while the dev server speaks plain HTTP. VAULT_TOKEN supplies the token:

BASH
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='hvs.xxxxxxxxxxxxxxxxxxxxxxxx'
vault status

Paste the real root token printed by your server. The status output looks like this:

TEXT
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         2.1.1
Storage Type    inmem
Cluster Name    vault-cluster-...
HA Enabled      false

Read it line by line. Initialized true means the storage has been set up. Sealed false means the server can read its data. Total Shares 1 and Threshold 1 show the dev-mode shortcut of a single unseal key. Storage Type inmem confirms the memory-only storage. HA Enabled false means this is a single node rather than a cluster.

If you prefer a fixed token so that you do not have to copy a new one on every start, pass it yourself:

BASH
vault server -dev -dev-root-token-id=root

Now the token is simply root. Convenient on a laptop, and exactly what you must never do anywhere else.

The other path is the web interface. Open http://127.0.0.1:8200 in a browser and sign in with the token. The UI is a pleasant way to browse, but the CLI is what you will use in scripts and what interviewers ask about, so this guide shows the CLI.

The HTTP/HTTPS mismatch. If you forget VAULT_ADDR you will see an error like http: server gave HTTP response to HTTPS client, or a warning that the address is unset and defaulting to https://127.0.0.1:8200. The client is speaking TLS to a server that does not. Export VAULT_ADDR with http:// and retry.
Try it
  1. Start vault server -dev -dev-root-token-id=root in one terminal.
  2. In another, export VAULT_ADDR and VAULT_TOKEN=root, then run vault status.
  3. Stop the server with Ctrl+C, start it again, and run vault status once more.
the second run is a brand-new Vault with an empty store. Nothing you saved earlier survives, which is what "in memory" means.

Your first secrets: the KV engine

The development server already has a KV (key-value) engine mounted at secret/. KV is the simplest engine and the one you will use most in the beginning. It stores named sets of key-value pairs at paths you choose.

There are two versions. KV version 1 stores one value per path with no history. KV version 2 keeps versions of each secret, lets you delete and recover them, and stores metadata. Version 2 is what the dev server mounts and what you should use by default. One detail trips up almost everyone, so here it is early: in version 2 the HTTP API inserts data/ (and metadata/) into the path. The CLI hides that for you, the policies you write do not.

Check what is mounted:

BASH
vault secrets list
TEXT
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_a1b2c3d4    per-token private secret storage
identity/     identity     identity_e5f6a7b8     identity store
secret/       kv           kv_c9d0e1f2           key/value secret storage
sys/          system       system_03a4b5c6       system endpoints used for control, policy and debugging

Write your first secret. The -mount flag names the engine, and the last argument is the path inside it:

BASH
vault kv put -mount=secret myapp/config username=app password=s3cr3t
TEXT
======= Secret Path =======
secret/data/myapp/config

======= Metadata =======
Key                Value
---                -----
created_time       2026-10-01T09:30:12.123456Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

The printed Secret Path is secret/data/myapp/config, which shows the data/ segment that the CLI added. Note version 1. Read it back:

BASH
vault kv get -mount=secret myapp/config
TEXT
======== Secret Path ========
secret/data/myapp/config

======= Metadata =======
Key                Value
---                -----
created_time       2026-10-01T09:30:12.123456Z
version            1

====== Data ======
Key         Value
---         -----
password    s3cr3t
username    app

To pull out a single value without the decoration, which is what scripts want, use -field:

BASH
vault kv get -mount=secret -field=password myapp/config
TEXT
s3cr3t

Now change a value. There is a trap here. kv put replaces the whole secret with exactly the keys you give it, so this command silently drops username:

BASH
vault kv put -mount=secret myapp/config password=new-password

The result is version 2 with only one key. To change one key and keep the rest, use kv patch:

BASH
vault kv patch -mount=secret myapp/config password=another-password

That produces version 3, changing only password. Because the earlier put had already dropped username, it is still gone, and patch cannot bring it back. The lesson is to reach for patch whenever you mean "update this one field", and to use put only when you intend to replace everything.

Each write made a new version, and you can read an older one:

BASH
vault kv get -mount=secret -version=1 myapp/config
vault kv metadata get -mount=secret myapp/config

The metadata command shows every version, when it was created and whether it was deleted or destroyed. KV version 2 keeps the latest 10 versions by default. You can change that per secret, and you can make old versions expire:

BASH
vault kv metadata put -mount=secret -max-versions=5 -delete-version-after=720h myapp/config

Listing works on a path prefix:

BASH
vault kv list -mount=secret myapp
TEXT
Keys
----
config

A trailing slash on a listed key means it is a folder rather than a secret.

Three ways to get rid of a secret

Version 2 distinguishes between removing and erasing:

BASH
vault kv delete -mount=secret myapp/config                       # soft delete the latest version
vault kv undelete -mount=secret -versions=3 myapp/config         # bring it back
vault kv destroy -mount=secret -versions=1 myapp/config          # permanent, cannot be undone
vault kv metadata delete -mount=secret myapp/config              # every version and the metadata

A soft delete marks a version as deleted but keeps the data, so undelete can restore it. Destroy wipes the data of the given versions for good. kv metadata delete removes the secret entirely. There is also vault kv rollback, which writes an old version's contents as a new version:

BASH
vault kv rollback -mount=secret -version=1 myapp/config
The older form vault kv get secret/myapp/config (without -mount) still works and adds data/ for you. The -mount=secret form is preferred in current documentation because it leaves no doubt about where the engine ends and your path begins.

If you want to use a KV engine at another path, mount it yourself. The type name kv-v2 gives a version 2 engine, and so does -version=2 kv; a plain kv without the version flag gives the older version 1 engine:

BASH
vault secrets enable -path=team-a kv-v2
vault kv put -mount=team-a payments/stripe api_key=sk_test_example
KV is encrypted storage, not a password manager for humans. Anyone with read on the path gets the plaintext value. Secrets put on the command line also land in your shell history. For real secrets, read the value from a file or from standard input (vault kv put -mount=secret myapp/config password=- reads the value from stdin) and keep the shell history disabled in shared environments.
Try it
  1. Write secret/myapp/config with two keys.
  2. Use kv put with one key, then look at the data. Did the second key survive?
  3. Use kv get -version=1 to see the original, then kv rollback to restore it.
  4. Soft-delete the latest version, read it (what do you get?), then undelete it.
you see that put replaces, patch merges, old versions stay readable until destroyed, and a soft delete is reversible.

Policies: deciding who may do what

So far you have acted as root, which bypasses every check. The real value of Vault appears the moment you stop doing that. A policy is how you describe the access a person or application should have, and it is usually short.

Here is a policy for an application called myapp. Save it as myapp.hcl:

myapp.hcl
path "secret/data/myapp/*" {
  capabilities = ["create", "read", "update", "patch", "delete"]
}

path "secret/metadata/myapp/*" {
  capabilities = ["list", "read"]
}

Read it as two rules. The first says: on any path beneath secret/data/myapp/, the token may create, read, update, patch and delete. The second says: on secret/metadata/myapp/..., it may list and read. Anything else is forbidden, because policies deny by default.

Why two paths? This is the KV version 2 detail promised earlier. The engine exposes the actual values under data/ and the bookkeeping (version list, key listing) under metadata/. A policy that mentions only secret/myapp/* matches nothing and gives a permanent "permission denied". Vault also has delete/, undelete/ and destroy/ paths for the matching operations, so grant those deliberately, not by accident.

Write the policy to Vault, and list what exists:

BASH
vault policy write myapp myapp.hcl
vault policy list
vault policy read myapp
TEXT
Success! Uploaded policy: myapp

The command vault policy fmt myapp.hcl tidies formatting, and vault policy delete myapp removes it.

Path matching rules

Paths in a policy follow a few rules that are worth knowing precisely:

  • An exact path, such as secret/data/myapp/config, matches that path only.
  • A trailing *, as in secret/data/myapp/*, matches everything beneath the prefix. The asterisk is only allowed at the end.
  • A + matches exactly one path segment, so secret/data/+/shared matches secret/data/teamA/shared and secret/data/teamB/shared, but not secret/data/teamA/x/shared.
  • When several rules match a path, Vault uses the most specific one. A deny capability always overrides everything else.
HCL
path "secret/data/+/shared" {
  capabilities = ["read"]
}

The capabilities, and how they map to HTTP

Capabilities correspond to HTTP verbs and to the CLI commands you run. read is an HTTP GET. create and update are POST or PUT, and Vault picks between them depending on whether the path already holds data. delete is DELETE. list is the LIST verb used by vault kv list. patch is an HTTP PATCH, used by vault kv patch. sudo is needed for a few protected administrative paths, and deny refuses outright.

A subtle example: reading a dynamic credential such as database/creds/app needs only read, even though that request creates a new database user. Vault models it as a read of a path that generates a value.

Testing a policy before you trust it

Policies are code, and you should test them. Create a token that carries only your policy and use it:

BASH
vault token create -policy=myapp -ttl=1h
TEXT
Key                  Value
---                  -----
token                hvs.CAESI...
token_accessor       8Gk2...
token_duration       1h
token_renewable      true
token_policies       ["default" "myapp"]
identity_policies    []
policies             ["default" "myapp"]

Notice that the token has both myapp and default. Now try it, using a variable to keep this token separate from your root token:

BASH
VAULT_TOKEN=hvs.CAESI... vault kv get -mount=secret myapp/config
VAULT_TOKEN=hvs.CAESI... vault kv get -mount=secret other/config

The first works. The second fails:

TEXT
Error reading secret/data/other/config: Error making API request.

URL: GET http://127.0.0.1:8200/v1/secret/data/other/config
Code: 403. Errors:

* 1 error occurred:
	* permission denied

Read that message carefully, because you will see it constantly. The URL shows the real path (secret/data/other/config), the Code: 403 is the HTTP status, and permission denied means the token authenticated fine but its policies do not allow this path. A missing or bad token gives a different text, which we will compare in the errors section.

Two more tools shorten the guessing. You can ask Vault what a token may do on a path, and you can print the policy a command would need:

BASH
vault token capabilities hvs.CAESI... secret/data/myapp/config
vault kv get -output-policy -mount=secret myapp/config

The first prints the capabilities (for example create, delete, patch, read, update). The second prints a ready-made policy stanza for the command, which is a clever way to start writing a policy: run the command you want to allow with -output-policy, and paste the result.

Policy traps that cost beginners hours. (1) Forgetting data/ in KV v2 paths. (2) Giving read on data/ but no list on metadata/, so kv list fails. (3) Duplicate keys in one policy file: since Vault 1.21, duplicate attributes in HCL are an error rather than a warning. (4) Editing the policy in a file and forgetting to run vault policy write again; Vault never reads your local file.
A policy change takes effect on the next request from tokens already issued. You do not need to log in again after updating a policy that a token already carries, but you do need to re-login to add a new policy to an existing token's list.
Try it
  1. Write myapp.hcl as above and upload it with vault policy write.
  2. Create a token with that policy and read myapp/config with it.
  3. Try kv list on myapp, then on other. Explain each result.
  4. Remove the metadata rule, re-upload the policy, and see which command breaks.
you see permission denied where you expect it and learn that a missing rule, not a wrong password, is the usual cause.

Authentication: how people and applications log in

Using the root token to do everything defeats the point. Real users and applications log in through an auth method. Each method proves an identity in its own way and ends by issuing a token that carries policies.

List what is enabled:

BASH
vault auth list
TEXT
Path      Type     Accessor               Description
----      ----     --------               -----------
token/    token    auth_token_a1b2c3d4    token based credentials

The token method is always present and cannot be removed. It is what you have been using.

Userpass: a good first method for learning

The userpass method stores usernames and passwords inside Vault. It is simple, which makes it ideal for practising, though most companies prefer single sign-on through OIDC or LDAP for people.

BASH
vault auth enable userpass
vault write auth/userpass/users/alice password=change-me-please token_policies=myapp

Two points about that command. First, the word write is generic. In Vault you configure almost everything by writing to a path, and the path here is auth/userpass/users/alice. Second, token_policies lists the policies the login token will carry. Now log in as Alice:

BASH
vault login -method=userpass username=alice
TEXT
Password (will be hidden):
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.CAESI...
token_accessor         9Lm3...
token_duration         768h
token_renewable        true
token_policies         ["default" "myapp"]
identity_policies      []
policies               ["default" "myapp"]
token_meta_username    alice

The CLI saved this token in ~/.vault-token. One surprise here: if VAULT_TOKEN is still exported as root, the environment variable takes priority over the saved token, and you are still root. Run unset VAULT_TOKEN to use the token you just earned. Then check who you are:

BASH
vault token lookup

The output lists the token's policies, TTL, creation time, and whether it is renewable. Alice can now read secret/data/myapp/config and nothing else. That is the security model in action.

The same login through the HTTP API, with no CLI involved, is a POST to the login path. The response contains the token at .auth.client_token:

BASH
curl --request POST --data '{"password": "change-me-please"}' \
  http://127.0.0.1:8200/v1/auth/userpass/login/alice

Logging in with a token you already have

Since tokens are the universal credential, vault login with no method takes a token and stores it:

BASH
vault login

You can also log in by passing the token value, but that exposes it in your shell history, so prefer the prompt. To discard the stored token, delete ~/.vault-token.

Other methods you will meet

You do not need these yet, but recognise the names. AppRole is for machines and automation: a role ID plus a secret ID. Kubernetes lets a pod log in with its service account. JWT/OIDC connects to single sign-on providers and to CI systems such as GitHub Actions and GitLab, so that a pipeline can log in with a short-lived identity token instead of a stored password. AWS, GCP and Azure methods let a workload log in with its cloud identity. LDAP, GitHub, Okta and TLS certificates cover other sources. The mid-level guide builds AppRole, Kubernetes and JWT step by step.

The principle behind all of them is the same. The best credential for a workload is one it already has for another reason, such as its Kubernetes service account or its cloud identity. A method that needs you to distribute a password to the client has just moved the secret-management problem one step, and later guides give it a name: the secret zero problem.

Do not turn userpass into your production answer. It is wonderful for a tutorial and acceptable for a small team. For a company, connect Vault to your identity provider so that leaving the company removes Vault access automatically. Also know that Vault locks out repeated failed logins for userpass, AppRole and LDAP by default, so a script with a bad password can lock an account.
Try it
  1. Enable userpass and create a user with the myapp policy.
  2. Run unset VAULT_TOKEN, then vault login -method=userpass username=alice.
  3. Run vault token lookup, then read myapp/config and other/config.
  4. Repeat the login with curl and extract the token with jq -r .auth.client_token.
you can get the same token through the CLI and the HTTP API, and you see that the policy, not the login, limits what Alice can reach.

Tokens, TTLs and leases in practice

Tokens are not forever, and that is deliberate. A leaked token that expires in an hour is a small problem. A leaked token that lives for a month is a large one. Understanding TTLs is the difference between a Vault that feels helpful and one that surprises you at 3 a.m.

Create a token yourself, with an explicit policy and lifetime:

BASH
vault token create -policy=myapp -ttl=1h
vault token create -policy=myapp -ttl=10m -use-limit=3

The second token expires in ten minutes and stops working after three uses. Short lifetimes and use limits are common defences for one-off jobs.

TTL, renewal and maximum TTL

A token has a TTL (how long until it expires) and a maximum TTL. While a token is renewable and under its maximum, you can extend it:

BASH
vault token renew
vault token renew -increment=30m hvs.CAESI...

Renewing resets the TTL but never beyond the maximum. When the maximum is reached, the token dies and the client must log in again. The system default TTL and maximum are both 768 hours; you can change them per mount, for example vault auth tune -default-lease-ttl=1h -max-lease-ttl=24h userpass/. A periodic token works differently: each renewal resets it to its period, so a long-running service can keep one alive indefinitely as long as it renews on time. An explicit_max_ttl is a hard cap that not even renewal can pass.

If you request a longer TTL than the maximum allows, Vault does not fail; it caps the value and prints a warning such as "TTL of ... exceeded the effective max_ttl ...; TTL value is capped accordingly". Read warnings like that, they tell you the value you asked for is not the value you got.

Looking at and revoking tokens

BASH
vault token lookup
vault token lookup hvs.CAESI...
vault token lookup -accessor 9Lm3...
vault token revoke hvs.CAESI...
vault token revoke -accessor 9Lm3...

The accessor-based versions are what you use when you know there is a token to cancel but you must not see the token itself, for example when going through an audit log. Revoking a token also revokes all child tokens and every lease it created, so revoke high in the hierarchy when dealing with an incident.

Leases: the same idea for dynamic secrets

When Vault creates a dynamic secret, it attaches a lease. Suppose you later enable the database engine and read a credential. The response contains a lease_id, a duration and a flag saying whether you can renew. The commands are symmetrical with tokens:

BASH
vault lease lookup <lease_id>
vault lease renew -increment=1h <lease_id>
vault lease revoke <lease_id>
vault lease revoke -prefix database/creds/app

The last command is the emergency button: it revokes every outstanding credential beneath a prefix, and Vault removes the matching database users. A team that can run that command in seconds has a much better incident story than one that must rotate a shared password across twenty services.

A quick look at dynamic secrets

You do not need a database to grasp the idea. The flow is: an administrator configures the engine with a connection and a role that holds a SQL template, and an application reads database/creds/<role>. Vault connects, creates a unique user with a short life, and returns the username and password. When the lease ends, Vault drops that user. No human ever sees or stores the password, and every application instance has a different one. The mid-level guide shows the full setup. For now, hold on to the contrast with KV, where you store a secret and it stays the same until somebody changes it.

Expired means gone. When a token or lease expires, the error is a generic one, typically permission denied (for tokens) or invalid lease ID or lease not found (for leases). If something worked yesterday and not today, check the TTL first with vault token lookup before you suspect the policy.
Try it
  1. Create a token with -ttl=2m -use-limit=2 and the myapp policy.
  2. Use it twice to read a secret. Try a third time and read the error.
  3. Create a short-lived token, run vault token lookup on it twice a minute apart and watch ttl fall.
  4. Create a parent token and a child; revoke the parent and confirm the child is dead.
you see that tokens are temporary by design, that use limits are enforced, and that revocation cascades down the tree.

Talking to Vault over HTTP

Every CLI command is an HTTP request in disguise. Knowing this demystifies Vault, and it is how your applications will talk to it, using a client library or plain HTTP.

The API lives at /v1/. A token goes in the X-Vault-Token header. To read the secret from earlier:

BASH
curl --header "X-Vault-Token: $VAULT_TOKEN" $VAULT_ADDR/v1/secret/data/myapp/config

The JSON response has the structure that catches people out:

JSON
{
  "request_id": "0f3a...",
  "lease_id": "",
  "renewable": false,
  "lease_duration": 0,
  "data": {
    "data": {
      "password": "s3cr3t",
      "username": "app"
    },
    "metadata": {
      "created_time": "2026-10-01T09:30:12.123456Z",
      "version": 1
    }
  }
}

There are two data keys. The outer one is Vault's standard response envelope, used by every engine. The inner one is KV version 2's wrapper around your key-value pairs, alongside metadata. With jq you pull the password with .data.data.password. KV version 1 has no inner wrapper.

To write, send a POST with the same inner data wrapper:

BASH
curl --header "X-Vault-Token: $VAULT_TOKEN" --request POST \
  --data '{"data": {"username": "app", "password": "s3cr3t"}}' \
  $VAULT_ADDR/v1/secret/data/myapp/config

Other endpoints you will want early are the health check and seal status, neither of which needs a token:

BASH
curl $VAULT_ADDR/v1/sys/health
curl $VAULT_ADDR/v1/sys/seal-status

The health endpoint's status code carries meaning, which is what load balancers and monitors rely on: 200 means the node is active and unsealed, 429 an unsealed standby, 501 not initialised, and 503 sealed.

Remember the trick from earlier: any CLI command plus -output-curl-string prints the equivalent request rather than running it.

BASH
vault kv get -mount=secret -output-curl-string myapp/config
TEXT
curl -H "X-Vault-Request: true" -H "X-Vault-Token: $(vault print token)" http://127.0.0.1:8200/v1/secret/data/myapp/config

Since Vault 2.0, request paths must be clean. A path containing //, /./ or /../ is rejected. If your script builds URLs by joining strings, such as adding a trailing slash to a mount and then a leading slash to a path, it will break on 2.x with a clear refusal. Strip the stray slashes.

Use the -format=json flag (or VAULT_FORMAT=json) for scripts that parse CLI output with jq, and -field=name when you want one raw value. Never scrape the default table output; its layout is not a contract.
Try it
  1. Fetch myapp/config with curl and print only the password with jq.
  2. Write a new version through curl and check kv metadata get shows version 4 (or higher).
  3. Call /v1/sys/health and read the HTTP status with curl -i.
  4. Use -output-curl-string on vault policy list and run the printed command.
you can reach the same data through the CLI and through raw HTTP, and you know where the double data key comes from.

Audit logs: proving who did what

A secrets server that cannot tell you who read a secret is only half a security tool. Audit devices record every request and response. You enable one much like an auth method:

BASH
vault audit enable file file_path=/tmp/vault-audit.log
vault audit list -detailed

On a dev server, /tmp is fine. In real deployments the log goes to a protected directory, or to syslog, or to a socket that feeds a central logging system. Each line is a JSON object describing one request or response. The entries contain the path, the operation, the client's identity and the result. Secret values and tokens are not written in plain text: Vault replaces them with an HMAC-SHA256 hash, so you can correlate "the same value was used twice" without the log becoming a second copy of your secrets.

Do a read and look at the log:

BASH
vault kv get -mount=secret myapp/config
tail -n 2 /tmp/vault-audit.log | jq .

You will see a request entry and a response entry. The request shows "operation": "read" and "path": "secret/data/myapp/config", plus the display_name of the token holder (for example userpass-alice), and the values in the response are hashed strings beginning with hmac-sha256:.

There is a rule to learn now, because it explains a famous class of outages. If at least one audit device is enabled and none of them can write, Vault stops answering requests. It fails closed: no audit record means no request is served. A full disk that holds the audit log can therefore take down every application that depends on Vault. This is why production guides say to run at least two audit devices, to watch the disk and to alert on audit failures.

Enable audit before you need it. Nothing is recorded retroactively. On a new production server, turn on an audit device before you give anyone access, and confirm that entries appear.
Try it
  1. Enable a file audit device on your dev server.
  2. Do one kv get as alice and one as root.
  3. Find both requests in the log with jq and compare their display_name fields.
  4. Look up the password value in the log and confirm it is hashed.
you can see who did what, and you confirm that the log itself contains no readable secret.

Configuration: from dev mode to a real server

A development server needs no configuration file. A real server reads one. Even if you do not run production yourself, you will read these files in your job, so know what the pieces are.

A minimal single-node configuration using Integrated Storage (Raft) looks like this:

vault.hcl
ui            = true
disable_mlock = true
api_addr      = "https://vault.example.com:8200"
cluster_addr  = "https://vault.example.com:8201"

storage "raft" {
  path    = "/opt/vault/data"
  node_id = "vault-1"
}

listener "tcp" {
  address       = "0.0.0.0:8200"
  tls_cert_file = "/opt/vault/tls/tls.crt"
  tls_key_file  = "/opt/vault/tls/tls.key"
}

Go through it piece by piece.

  • ui = true serves the web interface on the same port as the API.
  • disable_mlock controls whether Vault asks the operating system to keep its memory out of swap. With Integrated Storage, the setting has no default since Vault 1.20, so the server refuses to start until you set it explicitly to true or false. In containers it is normally true, and you then disable swap on the host or encrypt it.
  • api_addr is the address that clients should use, and standby nodes use it to redirect clients to the active node. cluster_addr is the address nodes use to talk to each other, on port 8201. Raft requires it.
  • storage "raft" puts the encrypted data on local disk and lets several Vault nodes form a cluster. path is where the data lives and node_id names this node. Every production cluster needs three or five nodes, and the operator guide later in the series covers it.
  • listener "tcp" is the network endpoint. The certificate and key files give it TLS. The setting tls_disable = true exists, but it belongs only in a dev setting.
  • An optional seal "awskms" { ... } (or azurekeyvault, gcpckms, ocikms, transit) block enables auto-unseal.

Run it, point the CLI at it, and initialise:

BASH
vault server -config=/etc/vault.d/vault.hcl
export VAULT_ADDR='https://vault.example.com:8200'
vault operator init
TEXT
Unseal Key 1: 8kQ...
Unseal Key 2: 3vP...
Unseal Key 3: Zx1...
Unseal Key 4: mT9...
Unseal Key 5: Lw7...

Initial Root Token: hvs.xxxxxxxxxxxxxxxxxxxxxxxx

Vault initialized with 5 key shares and a key threshold of 3. Please securely
distribute the key shares printed above. When the Vault is re-sealed,
restarted, or stopped, you must supply at least 3 of these keys to unseal it
before it can start servicing requests.

That output appears exactly once. Vault does not keep the unseal keys or show them again. Give each share to a different trusted person, and store the initial root token only long enough to set up real administrators. Then unseal by running the next command three times, each time with a different share:

BASH
vault operator unseal

After the third share, vault status shows Sealed false. On a server with auto-unseal you skip this step, because init produces recovery keys and the server unseals itself on start.

If Vault does not start, or you want to check a config file before using it, vault operator diagnose -config=/etc/vault.d/vault.hcl reports problems such as unreadable certificates, a missing cluster_addr or a storage path it cannot write.

The installer packages on Linux create a vault system user, a systemd service and a data directory. The service runs vault server against /etc/vault.d/vault.hcl, and you manage it with sudo systemctl start vault and sudo journalctl -u vault. Two security habits from the official hardening guide are worth memorising at this stage: run Vault as an unprivileged user, and never leave the root token around.

If you run Vault on Kubernetes, the official Helm chart deploys it. The default chart values start a single node with file storage, which HashiCorp itself says is not production-ready; a real install turns on high availability with Raft (server.ha.enabled=true and server.ha.raft.enabled=true). The chart version at the time of writing deploys a slightly older Vault than 2.1.1 unless you pin server.image.tag.

Environment variables configure the client, and you will set them in every terminal:

Variable What it does
VAULT_ADDR The server URL the CLI talks to
VAULT_TOKEN The token to use, taking priority over ~/.vault-token
VAULT_FORMAT Output format: table, json or yaml
VAULT_CACERT Path to a CA certificate to trust for TLS
VAULT_SKIP_VERIFY Turns off TLS verification; for throwaway dev work only
VAULT_NAMESPACE Namespace to use on Enterprise and HCP editions
Never ship VAULT_SKIP_VERIFY=true. It removes the protection that TLS gives, so anyone on the network path can impersonate your Vault. If you see a certificate error, the right fix is to give the client the CA with VAULT_CACERT.
Try it
  1. Write the vault.hcl above with a local path you own and tls_disable = true in the listener (local learning only; remove the certificate lines).
  2. Run vault operator diagnose -config=vault.hcl and read its checks.
  3. Start the server with that file, run vault operator init -key-shares=3 -key-threshold=2, and unseal with two shares.
  4. Stop the server, start it again, and confirm it comes back sealed.
your data survives a restart this time, but Vault returns sealed and waits for shares, which is the whole point of the barrier.

Reading the errors Vault gives you

Most beginner frustration with Vault comes from five or six messages. Each one has a cause that you can identify quickly once you know what the message is telling you.

Message What it means What to do
http: server gave HTTP response to HTTPS client The CLI is speaking HTTPS to a plain HTTP server export VAULT_ADDR='http://127.0.0.1:8200'
connection refused on port 8200 Vault is not running, or listens elsewhere Start it; check the listener address
Code: 503 ... Vault is sealed The server needs unsealing vault operator unseal, or fix access to the KMS
Code: 403 ... permission denied The token is valid but the policy does not allow the path vault token capabilities, then fix the policy
Code: 403 ... missing client token No token was sent vault login, or set VAULT_TOKEN
no handler for route Wrong path, or nothing is mounted there vault secrets list; check spelling
x509: certificate signed by unknown authority The client does not trust the server's CA Set VAULT_CACERT
Vault is not initialized (health returns 501) Fresh storage vault operator init

The distinctions matter. permission denied and missing client token both use HTTP 403, yet they point in opposite directions. The first says you are known but not allowed; the second says you never identified yourself. no handler for route is the same story at another level: the path did not reach any engine at all, so the problem is not policy but a typo or an engine that was never enabled.

Follow a fixed order when a read fails, and you will rarely be stuck for long:

  1. Is the server reachable, and does vault status say Sealed false?
  2. Is the token what you think it is? Run vault token lookup and check policies and TTL. Check that VAULT_TOKEN is not overriding the token you logged in with.
  3. Is the path right? Run vault secrets list and check that the mount exists and that the KV version matches your path style.
  4. What does the policy allow? Use vault token capabilities <path>, and use vault read sys/internal/ui/resultant-acl to see the effective policy of your current token.
  5. Still unclear? Use -output-curl-string to see the real HTTP request, and read the audit log.

One error worth singling out is this one from the KV CLI: preflight capability check returned 403, please ensure client's policies grant access to path "secret/". The CLI asks Vault which KV version a mount uses, and it needs read on sys/internal/ui/mounts/secret to do so. The default policy normally covers this. If a custom setup leaves it out, either add that read or call the explicit API path.

Do not "fix" permission errors by attaching the root policy. The temptation is strong at 6 p.m. on a Friday. The right fix is to add the one missing path to the policy. A token with root rights that ends up in a pipeline is the exact outcome Vault exists to prevent.
Try it
  1. Cause each of these on purpose: a wrong VAULT_ADDR scheme, a misspelled mount, an expired or empty VAULT_TOKEN, and a path outside your policy.
  2. For each, write the message, the layer that failed (network, mount, identity, policy) and the one command that diagnosed it.
a four-row table in your own words. Once each message maps to a layer, most Vault problems take a minute.

Putting it all together

Here is a small end-to-end project that uses every idea in this guide. Suppose a small team builds a service called orders and needs a database password and an API key. You are the Vault administrator, and two people will use the secrets: a developer, Dina, who may read and update them, and a read-only auditor, Omar.

Start a dev server in one terminal with vault server -dev -dev-root-token-id=root. In another, set up the client:

BASH
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='root'
vault audit enable file file_path=/tmp/vault-audit.log
vault secrets enable -path=orders kv-v2

Store the secrets under a path that reflects the service:

BASH
vault kv put -mount=orders prod/database username=orders_app password=Pa55-from-admin
vault kv put -mount=orders prod/payments api_key=sk_test_example

Write two policies, one for each role:

orders-dev.hcl
path "orders/data/prod/*" {
  capabilities = ["create", "read", "update", "patch"]
}

path "orders/metadata/prod/*" {
  capabilities = ["list", "read"]
}
orders-audit.hcl
path "orders/data/prod/*" {
  capabilities = ["read"]
}

path "orders/metadata/prod/*" {
  capabilities = ["list", "read"]
}

Upload the policies and create both users:

BASH
vault policy write orders-dev orders-dev.hcl
vault policy write orders-audit orders-audit.hcl
vault auth enable userpass
vault write auth/userpass/users/dina password=dina-demo-pass token_policies=orders-dev token_ttl=1h
vault write auth/userpass/users/omar password=omar-demo-pass token_policies=orders-audit token_ttl=1h

Now act as each person. Log in as Dina, rotate the API key with patch, and list the secrets:

BASH
unset VAULT_TOKEN
vault login -method=userpass username=dina
vault kv patch -mount=orders prod/payments api_key=sk_test_rotated
vault kv list -mount=orders prod

Then log in as Omar. He can read, but any write is refused:

BASH
vault login -method=userpass username=omar
vault kv get -mount=orders -field=api_key prod/payments
vault kv put -mount=orders prod/payments api_key=nope

The last command fails with permission denied, and that is the right result. Finally, look at the audit log to see the trail and to confirm it recorded Omar's refused write with his identity:

BASH
grep '"operation":"update"' /tmp/vault-audit.log | jq -r '.auth.display_name + "  " + .request.path + "  " + (.error // "ok")' | tail -n 3

Finish with cleanup that shows the lifecycle: look at Dina's token, revoke it, and watch her next request fail.

BASH
vault login -method=userpass username=dina
vault token revoke -self
vault kv list -mount=orders prod

You have just run the full loop. Secrets were stored in a central place, two identities received least-privilege access, every action was logged, and a token was revoked on demand. Replace the two human users with AppRole or Kubernetes login for applications, and the shape of a real deployment appears.

Try it
  1. Run the whole project from an empty dev server, without copying: write each command yourself.
  2. Add a third user who may read only prod/payments, not prod/database.
  3. Change Omar's policy to deny orders/data/prod/database explicitly and confirm the deny wins over a broader read rule.
a working access model you built and tested yourself. You can now explain, with evidence, who can read what.

What you can now do, and what comes next

You can now explain what Vault is and the problem it solves, and trace a request from login through token and policy to a secrets engine. You can install it, run a development server, and tell dev mode apart from a real deployment. You can store, version, patch, delete and restore secrets in KV version 2, and you know why its paths contain data/. You can write a policy, test it with a real token, and read permission denied as a statement about policy rather than about passwords. You can log in through userpass, control token lifetimes, renew and revoke tokens, call the HTTP API directly, turn on an audit log, and read a basic server configuration.

Some facts from this guide are worth keeping in your head as a short list. The CLI defaults to HTTPS, so VAULT_ADDR matters. kv put replaces and kv patch merges. KV version 2 policies need data/ and metadata/. Root tokens are for setup, not for daily use. A sealed server is a healthy server waiting for keys. And if audit devices are enabled but cannot write, Vault stops serving requests.

What comes next depends on where you work:

  • Mid-level covers machine authentication (AppRole, Kubernetes, JWT for CI), dynamic database credentials, Transit encryption, PKI certificates, response wrapping, and Vault Agent and the Vault Secrets Operator for delivering secrets to applications.
  • Senior covers running Vault as a platform: Raft clusters, auto-unseal, upgrades, backup and restore, monitoring, multi-tenancy and incident playbooks.
  • Neighbouring guides in this catalogue fit naturally. Vault is often deployed with Kubernetes and configured as code with Terraform, and the applications that use it often run in Docker containers.

For teams in the Gulf and Egypt, one practical point recurs in security reviews: where secrets and their encryption keys physically live. A self-managed Vault lets you keep the cluster, the audit logs and the auto-unseal key in a cloud region inside your data-residency boundary, which is often the reason a regulated employer prefers it to a managed secrets product in another jurisdiction. Raise that question early when you design a deployment.

Two last reminders about the ecosystem. Older tutorials may show Vault 1.x details (tokens starting s., unauthenticated rekey endpoints, or the HCP Vault Secrets service, which has reached end of life). And the quickest way to stay correct is to read the release notes at the start of any new project.

Sources