Move the burden of Vault authentication and token renewal out of your application code with Vault Agent. We'll cover the sidecar daemon concept, Auto-Auth, and Template & Sink for automatically rendering config files containing secrets.

After covering in episode 12 how lease, TTL, renewal, and revocation work — complete with the reasons every dynamic secret and token in Vault has a limited lifespan — and in episode 13 how to securely deliver secrets via response wrapping, this episode covers the topic backend developers and DevOps have been most waiting for: how can an application consume secrets from Vault without having to program all the authentication and renewal logic into its code.
This is the crucial question: if our application must call auth/approle/login, store the token, then handle renew every few minutes and revoke when the process dies — then every application in the company must rewrite the same security code. That's boilerplate that's error-prone, leak-prone, and complicates audits. HashiCorp's solution to this problem is Vault Agent: a companion process that takes over all the heavy lifting of authentication and token lifecycle, so application code can stay clean and not even need to know that Vault exists. Let's dissect it thoroughly.
Imagine your application like an employee who needs to enter the office building. Without Vault Agent, that employee must carry their own access card, check its expiry date, and renew it themselves before it expires. Now imagine a hundred employees with hundreds of cards, each with different TTLs — who guarantees they're all renewed on time? Well, Vault Agent is the security officer at the door who handles all the cards for the employees.
Technically, the main benefits of Vault Agent are:
VAULT_TOKEN or login logic..env, config.json, application.yml) that Vault Agent fills from Vault data.Important
This is the indirect integration pattern: the application doesn't need to call the Vault API at all. Secrets "come by themselves" to the application via files. Conversely, direct integration using the official SDK (hvac, node-vault, vault/api) will be covered in depth in episode 16.
Vault Agent is a process that runs alongside the application (sidecar). On a traditional VM server it runs as a systemd service; in Kubernetes it runs as a sidecar container inside the same pod (we'll cover that in episode 17). It has three main responsibilities:
Its workflow looks roughly like this:
+-----------+ 1. auto-auth (AppRole/AWS/K8s) +----------------+
| | ---------------------------------------> | |
| Vault | <--------------------------------------- | Vault Agent |
| Server | 2. token + lease | (sidecar) |
| | | |
| | <--------------------------------------- | 3. template: |
| | 4. secret data | secret "..." |
+-----------+ | |
| 5. render to |
| destination |
+----------------+What's worth emphasizing: the application only interacts with the generated files, not with Vault. If Vault is down or the token is being rotated, the application is unaffected because it reads the files already on disk.
The auto_auth block in the Vault Agent configuration is the "heart" of automatic login. Inside it we define:
method — which auth method is used to log in (for example approle, aws, azure, gcp, kubernetes, jwt).sink — where the login token is written. A file sink writes the token to a file, while aead writes the token in encrypted form.For an AppRole login, Vault Agent needs role_id and secret_id. Both are usually stored in files with strict permissions:
pid_file = "/var/run/vault-agent.pid"
vault {
address = "http://127.0.0.1:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/vault-agent/role-id"
secret_id_file_path = "/etc/vault-agent/secret-id"
}
}
sink "file" {
config = {
path = "/etc/vault-agent/token"
}
}
}Tip
The role_id value is static and safe to store as a file. Meanwhile secret_id is confidential and ideally rotated periodically — one common pattern is generating the secret_id via response wrapping (episode 13) and then having a bootstrap script write it to secret_id_file_path. That way no secret_id lingers on disk for long.
Beyond AppRole, Auto-Auth also supports cloud-based authentication: for example the aws method which logs in using an IAM instance role, or the kubernetes method which logs in using a ServiceAccount JWT. The concept is the same — Vault Agent handles everything, and the application code doesn't change at all. A comparison of the configurations for both methods:
auto_auth {
method "kubernetes" {
mount_path = "auth/kubernetes"
config = {
role = "web"
token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"
}
}
}Note
Both methods work exactly the same from Vault Agent's point of view: log in → get a token → the token is renewed automatically → written to the sink. The only difference is who proves the machine's identity. In Kubernetes, identity is proven by the ServiceAccount JWT; in AWS, by the IAM role instance metadata. We'll dive into Kubernetes Auth details in episode 17.
The most interesting part of Vault Agent is templates. Templates use Consul Template syntax — a Go templating language equipped with the secret, withSecret, and related functions. The concept: you write a file pattern, then Vault Agent fills in the marked secret parts.
Suppose a web app's secret is stored in KV v2 at the path secret/data/myapp containing the keys DB_HOST and DB_PASSWORD. The template to produce a .env file is:
APP_ENV=production
APP_PORT=8080
DB_HOST={{ with secret "secret/data/myapp" }}{{ .Data.data.DB_HOST }}{{ end }}
DB_PASSWORD={{ with secret "secret/data/myapp" }}{{ .Data.data.DB_PASSWORD }}{{ end }}Note the data access: for KV v2, the actual data lives under data.data, so the variable is {{ .Data.data.DB_HOST }}. For KV v1 it's just {{ .Data.DB_HOST }}. Using the wrong Data level is one of the most common pitfalls — we'll cover it in the common mistakes section.
The template block in the agent configuration connects the template with the destination file:
pid_file = "/var/run/vault-agent.pid"
vault {
address = "http://127.0.0.1:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/vault-agent/role-id"
secret_id_file_path = "/etc/vault-agent/secret-id"
}
}
sink "file" {
config = {
path = "/etc/vault-agent/token"
}
}
}
template {
source = "/etc/vault-agent/templates/app.env.tpl"
destination = "/var/lib/myapp/.env"
perms = 0600
}Warning
The perms attribute is very important. You don't want the rendered .env file to be readable by all users on the server (0644). Use 0600 or 0640 with an application-specific group. Once a secret file is readable by an unauthorized user, that's already a leak incident.
Running Vault Agent is quite simple — just point to the config file:
vault agent -config=/etc/vault-agent/config.hclThe -config flag can be repeated to load several files at once — useful for splitting the configuration per-section (e.g. base.hcl, auto-auth.hcl, templates.hcl) so it's easy to review:
vault agent -config=/etc/vault-agent/base.hcl \
-config=/etc/vault-agent/auto-auth.hcl \
-config=/etc/vault-agent/templates.hclTip
When the Vault Agent process receives the SIGHUP signal, it reloads the configuration without needing a restart — this is what ExecReload=/bin/kill -HUP $MAINPID in a systemd unit exploits. This is useful when you add a new template: just systemctl reload vault-agent and the template activates immediately without downtime.
For production environments, we run it as a systemd service so it's always alive and auto-restarts on crash. Example service unit:
[Unit]
Description=Vault Agent - Secret Rendering Sidecar
Documentation=https://developer.hashicorp.com/vault/docs/agent
Requires=network-online.target
After=network-online.target
[Service]
User=vault-agent
Group=vault-agent
ProtectSystem=strict
ExecStart=/usr/bin/vault agent -config=/etc/vault-agent/config.hcl
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now vault-agent
sudo systemctl status vault-agentAfter the agent runs, the .env file gets rendered. Because the template contains static secrets from KV, Vault Agent renders the file the first time it successfully logs in, and re-renders it every time the secret in Vault changes:
APP_ENV=production
APP_PORT=8080
DB_HOST=postgres.internal.local
DB_PASSWORD=a9f3!sVx#2kQNote
Note how rendering works: Vault Agent doesn't just write the file — it writes to a temporary file then does an atomic rename to the destination. This prevents the application from reading a half-written file. An application reading the file during the rename always sees a complete version.
To design deployments correctly, you need to understand what happens to the token at each phase of the Vault Agent process:
token_ttl. Templates are continuously watched; as soon as secret data changes, the rendered file is updated. This is what makes automatic rotation work without the application getting involved.SIGHUP makes the agent reload the configuration (adding/changing templates) without breaking the active token.systemctl stop), the agent performs a token revoke to Vault. This is crucial: the token isn't left floating alive after the process dies, closing off the possibility of misuse.sudo systemctl stop vault-agent
# Agent log: [INFO] (auth.approle) successfully revoked tokenImportant
Because the revoke happens on a clean termination signal, avoid killing the agent with kill -9 without reason. SIGKILL doesn't give the agent a chance to clean up the token — the token will stay alive until its TTL expires. Use systemctl stop or SIGTERM under normal circumstances.
When to use Vault Agent and when to use the SDK directly in the application? Both are valid, but they have different characteristics:
| Aspect | Vault Agent (Indirect) | In-App SDK (Direct) |
|---|---|---|
| Application's knowledge of Vault | None needed at all | The app must know the address & auth method |
| Token / renewal management | Automatic via the agent | Must be programmed in the app |
| Programming language | Agnostic (reads files) | Depends on the SDK per language |
| Dynamic secrets (e.g. DB creds) | Not fully supported | Fully supported (per-request) |
| Secret access latency | Low (read local file) | Depends on HTTP round-trips |
| Secret rotation | File re-rendered automatically | Needs polling/renew logic in code |
| Best for | Microservices, containers, static servers | Apps needing dynamic secrets per request |
The simple intuition: if the app only needs static secrets like database connection credentials that rarely change, Vault Agent is far simpler and safer. But if the app needs dynamic credentials generated per request — for example new DB credentials each time it opens a connection — then direct SDK integration is the right choice. Both topics will be explored in depth in episodes 15 and 16.
| Mistake | Symptom | Solution |
|---|---|---|
Wrong data level in the template ({{ .Data.DB_HOST }} for KV v2) | Rendered value is empty | For KV v2 use {{ .Data.data.DB_HOST }} |
| Sink / destination file with loose permissions | Secrets readable by other users | Set perms = 0600 and restrict the service user/group |
role_id_file_path / secret_id_file_path don't exist | Agent login fails: role_id is not a valid path | Ensure the files exist before agent start (bootstrap script) |
| Forgetting to enable the sink | Token not written to file so templates fail | Add the sink "file" block |
| Wrong template path / typo | Agent runs but the file isn't rendered | Check agent logs; test with vault agent -render |
Storing secret_id with an unlimited TTL | Misuse risk if it leaks | Use secret_id_ttl and response wrapping |
The rendered .env file gets committed to Git | Secret leak into the repository | Add the destination to .gitignore and never treat the template as the real value |
Caution
Never debug by disabling perms or moving the destination to a public directory just to "see it quickly." This habit is a time bomb: once in production, one small mistake turns directly into a secret leak.
In this episode 14 we've covered why Vault Agent is needed, how it works as a sidecar daemon, the Auto-Auth concept that automates login and token lifecycle, and Template & Sink for rendering configuration files (.env, config.json) containing secrets from Vault. We also saw how to run it with systemd and understood the comparison between indirect integration (agent) vs direct (SDK).
The core lesson of this episode: application code that doesn't need to know about Vault is the safest code — the less security logic spread across applications, the smaller the attack surface.
In episode 15 we'll go deeper: Vault Agent Caching to reduce API load and latency, and Advanced Templates using advanced Consul Template features like loops and conditionals. Keep your enthusiasm up!