Identity & Access · Chapter 14 of 16
Getting a password out of a vault, in code
You have been told to fetch it from Vault or CyberArk and given no further instructions. This is the rest of that sentence.
Somebody has told you to “get the password from Vault” or “pull it from CyberArk”, and stopped there. This chapter is the rest of that sentence.
Why not just put it in a file
A password in a configuration file is exactly as protected as the least protected copy of that file — a laptop, an old branch in version control, a backup, a build log. And anyone who reads it has access that does not expire and that nobody can see them using.
An environment variable is only slightly better. It keeps the password off disk, which is real. But it is still one fixed string that never expires, it is visible to every line of code in the program and to every program that one starts, and it turns up in the two places people forget: crash dumps, and the file the operating system exposes listing a running process's environment. Vault and CyberArk exist to remove the “fixed and never expires” part — not to move the same string somewhere smarter.
The circular problem, which nobody names for you
Say it in one line
You cannot protect the first credential by hiding it. You have to stop having one — by proving who you are with something the platform already knows about the machine, rather than with a string you were given.
That is the whole idea. Everything below is how each product implements it.
HashiCorp Vault — how a program logs in
Vault calls these auth methods. You pick one based on where your code runs.
| Method | What proves who you are | Use it when |
|---|---|---|
| Kubernetes | The signed file the cluster already placed inside your container | You run in Kubernetes. Easiest and safest — the identity is already there |
| Cloud IAM (AWS, Azure, GCP) identity and access management | The identity the cloud gave the machine at boot | You run on cloud compute. Nothing to store at all |
| AppRole | Two halves: a RoleID, which is safe to bake into the image, and a SecretID, which is not | There is no platform identity to borrow — a plain server, or a build job |
| JWT / OIDC JSON Web Token / OpenID Connect | A short, signed token from an identity provider you already trust. Vault checks the signature against keys that provider publishes, so it never has to call them | You have a single sign-on system, or any issuer that publishes its signing keys |
| Token | A Vault token you pasted in | Bootstrapping and emergencies only. Anything else and you have recreated the problem |
A note on the two acronyms in that last row, because they turn up everywhere and are rarely spelled out. A JWT — JSON Web Token, said ‘jot’ — is a small bundle of facts about someone (who they are, who vouched for them, when it stops being valid) packed into a string and signed. The signature is the useful part: anyone holding the signer’s public key can check the token was not tampered with, without contacting the signer. Note it is signed, not encrypted — the contents are readable by anyone who has it, so nothing secret goes inside.
OIDC — OpenID Connect — is a thin standard layer on top of OAuth 2.0. OAuth answers “may this application act on my behalf?”; it never answered “who is this person?”, and everyone bolted that on themselves, inconsistently and often insecurely. OIDC standardised it by adding an ID token — which is a JWT — describing the user. Permission to act, versus proof of identity. “Sign in with Google” is OIDC. There is a full chapter on this: OAuth 2.0 and OIDC, properly.
The Kubernetes method is worth understanding because it shows the pattern clearly. Every container gets a signed file at a fixed path, put there by the cluster. Your code sends that file's contents to Vault. Vault does not check the signature itself — it asks Kubernetes “is this yours, and is it still valid?” So if the cluster revokes it, access stops, without Vault needing its own copy of that logic.
AppRole's two halves are delivered differently on purpose, and this is the part people get wrong.
The RoleID just says which role you want. On its own it grants nothing, so it can sit in your image or configuration.
The SecretID is the password half. It must never sit anywhere at rest. Vault's answer is response wrapping: instead of handing your workload the SecretID, something trusted — your deployment system — asks Vault to put it in a sealed envelope with a short expiry. Your workload opens the envelope, and only the first person to open it can.
That last property is the useful one. If someone intercepted it, your workload's open fails — so you find out. It is tamper-evident, not merely secret.
The bigger idea: stop sharing one password at all
Storing a password in Vault instead of a file is an improvement, but everyone still shares the same one. Dynamic secrets go further. You give Vault admin access to the database once. After that, when your application asks, Vault creates a brand-new username and password, just for that request, that nobody has ever used — and attaches an expiry, one hour by default.
What that changes: a leaked credential is useless within the hour, you can see in the database exactly which application did what, and there is no shared password to rotate because nothing is shared.
What it costs you is an obligation in your code. The credential comes with a lifetime, and Vault will delete that database user when it expires. So your program has to renew before then, or fetch a new one — and must treat “my database login suddenly stopped working” as a normal event to handle, not a bug to be surprised by.
The code
Logging in with AppRole and reading a stored value, in Python. The library is hvac, the community client Vault's own documentation points to:
import hvac
client = hvac.Client()
client.auth.approle.login(
role_id = "<the role id, safe to ship in config>",
secret_id = "<the secret id, never at rest — see response wrapping>",
)
response = client.secrets.kv.v2.read_secret_version(path="myapp/database")
data = response["data"]["data"] # note: nested twice. This trips everyone up
password = data["password"]In Kubernetes it is shorter, because the identity is already sitting on disk:
from hvac.api.auth_methods import Kubernetes
# the cluster put this file here; your code did not have to be given anything
jwt = open("/var/run/secrets/kubernetes.io/serviceaccount/token").read()
Kubernetes(client.adapter).login(role="my-app", jwt=jwt)Asking for a fresh database login instead of a stored one — the dynamic-secret case:
creds = client.secrets.database.generate_credentials(name="my-app-role")
username = creds["data"]["username"] # created just now, just for you
password = creds["data"]["password"]
lease_id = creds["lease_id"] # keep this — you need it to renew
# and later, before it expires:
client.sys.renew_lease(lease_id)The bug this chapter most wants you to avoid: logging in once at start-up and never again.
Your Vault login expires. So does every dynamic credential. Code that authenticates in its start-up function and then runs for a week will work perfectly in testing and fail in production at an hour old.
Go's official client ships a helper for exactly this — a lifetime watcher that renews in the background and tells you when renewal is no longer possible so you can log in again. Most other languages, including Python, expect you to run your own timer against the lifetime you were handed.
watcher, err := client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
Secret: token, Increment: 3600,
})
go watcher.Start()
defer watcher.Stop()
for {
select {
case err := <-watcher.DoneCh():
return nil // renewal is over — go and log in again
case renewal := <-watcher.RenewCh():
log.Printf("renewed: %#v", renewal)
}
}The option that is usually better than any of this
Everything above puts the vault login inside your application. Which means your application owns the choice of method, the renewal timer, and every bug in both.
There is a second way, and for most teams it is the right one: let the platform fetch the secret, and have your code just read a file.
- Vault Agent — a small process next to your application that logs in and writes the secret to a file. Your code opens the file.
- The Kubernetes injector — you add an annotation to your deployment, and the cluster attaches that agent for you automatically.
- The Secrets Store CSI driver — CSI is the Container Storage Interface, the standard way Kubernetes attaches storage; here the secret appears as a mounted volume, fetched using the pod's own identity.
- The Vault Secrets Operator — one controller for the whole cluster that copies Vault values into ordinary Kubernetes secrets. This is the direction HashiCorp is now steering new Kubernetes deployments, over the older injector.
Why this is better rather than just lazier: it removes the login problem from your application entirely. There is no client library to keep patched, no renewal timer to get wrong, and the secret never passes through your code's own network handling. Your program's job shrinks to read a file, which is very hard to get wrong.
CyberArk — and the names matter
The first thing to know is that CyberArk is not one product, and someone saying “we use CyberArk” has not told you how your code will talk to it.
Reading a password from the Central Credential Provider
This is the pattern most application developers meet. You make one web request and get the password back:
GET https://<your-ccp-host>/AIMWebService/api/Accounts
?AppID=AppBilling
&Safe=PasswordSafe
&Object=Application1
# a successful reply:
{
"Content": "<the password>",
"UserName": "<the username>",
"Address": "<the host it is for>",
"PasswordChangeInProcess": false
}The part that confuses people: those three parameters choose WHICH password. They are not what proves you are allowed to ask.
Proving that is a separate mechanism, and it is the same idea as the Vault section above — something the platform vouches for, not a string you hold:
Which machine you called from (address or hostname) · which operating-system user the program runs as · where the program lives on disk · a hash of the program file itself, so a swapped binary stops working · and for the networked service specifically, a client certificate presented over mutual TLS — ordinary HTTPS proves the server is who it claims; mutual TLS makes the caller prove it too, with its own certificate.
Certificates are the primary method for the shared web service, because it is reached across a network. The local provider leans on the operating-system facts instead, because it sits on the same machine and can trust them.
The local Credential Provider is the other shape: a service installed on your own server that registered with the vault out of band and keeps an encrypted local cache, so your calls are fast and survive a brief network outage. You call it through a command-line tool or a language library rather than over HTTP:
# Windows
CLIPasswordSDK.exe GetPassword /p AppDescs.AppID=AppBilling \
/p Query="Safe=PasswordSafe;Folder=Root;Object=Application1" /o Password
# Linux and UNIX
./clipasswordsdk GetPassword -p AppDescs.AppID=<app-id> \
-p Query="Safe=<safe>;Folder=<folder>;Object=<object>" \
-p RequiredProps=Address,UserNameConjur, and a deadline that shapes your code
Conjur works in two steps. First you exchange a long-lived key for a short-lived token, then you use that token to fetch the secret:
# step 1 — log in, and get back a token
POST /authn/<account>/<host-id>/authenticate
body: the API key header: Accept-Encoding: base64
# step 2 — use it
GET /secrets/<account>/variable/prod%2Fdb%2Fpassword
header: Authorization: Token token="<the token from step 1>"That token expires after eight minutes. Which is unusually short, and it is worth pausing on: it makes the “log in once at start-up” mistake structurally impossible. Vault's default of an hour lets you write that bug and not notice for a while. Conjur's eight minutes forces you to build renewal in from the first day. An awkward default that prevents a whole class of bug is a good default.
In Python, using the two calls above:
import requests
token = requests.post(
"https://conjur.internal/authn/myorg/host%2Fmyapp/authenticate",
data = CONJUR_API_KEY,
headers = {"Accept-Encoding": "base64"},
timeout = 5,
).text
secret = requests.get(
"https://conjur.internal/secrets/myorg/variable/prod%2Fdb%2Fpassword",
headers = {"Authorization": f'Token token="{token}"'},
timeout = 5,
).textIn Kubernetes, Conjur can use the same trick Vault does — take the signed file the cluster already put in your container and validate it against the cluster. CyberArk's own guidance is worth repeating: use the audience-bound token, not the default service account one, and never reuse the same signing keys across clusters, because then a token from one cluster is indistinguishable from another.
Why so many enterprises run both
| HashiCorp Vault | CyberArk | |
|---|---|---|
| Creates brand-new credentials on demand | Central to how it works, across databases, clouds, certificates and SSH | Supported narrowly. The core model is stored values that get rotated |
| Rotates passwords on accounts that already exist | Not the main idea | Central — a domain admin account, a mainframe login, an existing service account |
| Records a human's admin session | No equivalent | Yes, and it is a large part of why it was bought |
| Typical arrival | Bottom-up, from platform teams, because it is API-first and cheap against Kubernetes | Top-down, as the audited system of record for human privileged access |
Running both is usually not a mistake. CyberArk normally arrived first, as the audited answer for human administrators and long-lived accounts in Active Directory or on a mainframe — removing it is a compliance project, not a code change. Vault normally arrived later with the cloud teams, for workloads that did not exist when the first programme was designed. Each keeps the population it is good at, and consolidating rarely justifies the audit disruption.
Both products changed owner recently, and it shows in the documentation.
HashiCorp is now part of IBM — the official Vault code examples carry an IBM copyright header, though the library itself is unchanged.
CyberArk is now part of Palo Alto Networks, completed 11 February 2026, and a new platform name, Idira, followed on 12 May 2026. Today the documentation site header says “Idira Docs” while the page footers still say CyberArk.
Which is a good reason to check the current documentation rather than trusting any guide, including this one, on product names.
What to do, and what a reviewer should look for
- Fetch the secret where you use it, not once when the program starts.
- Renew before it expires, and treat expiry as a normal event with a code path, not a surprise.
- Fail closed. If the fetch fails, refuse to do the protected thing. Do not quietly fall back to an old cached value.
- Never log it — and remember that an error message and a web address are both places a secret can end up in a log without anyone meaning to.
- Keep it out of environment variables for anything long-lived. Read it into one narrowly scoped variable instead.
- Prefer the platform-fetches-it option where your deployment offers it.
- Ask for exactly the path you need. And note that permission to list a folder leaks the names of every secret in it, even without permission to read them.
Four questions for a code review: where does this token come from, and what happens the moment it expires? Can a secret reach a log line, an exception message or a web address? Does the permission attached to this application grant more than this one job needs? And if the vault is unreachable, does the application fail closed or carry on?
Check yourself