Lakshya

Software Engineering · Chapter 22 of 31

Getting a password out of a vault, in code

Vault and CyberArk from the application developer's side — the circular problem underneath, and the code that solves it.

18 min read2 diagramsAll 31 chapters

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

THE CIRCLE EVERY SECRETS TOOL EXISTS TO BREAK 1. You move the database password out of the config file and into a vault. Good. 2. Now your program has to ask the vault for it — and the vault will only answer someone it recognises. 3. So your program needs a password to get the password. If that one also lives in a config file, you have solved nothing. You have added a network call. People call that first credential “secret zero” — the one you need before you can ask for any other. THE ANSWER IS NEVER “HIDE SECRET ZERO BETTER” It is: use something the machine ALREADY HAS and never had to be handed. · A server in the cloud already has an identity the cloud provider gave it when it booted.· A container in Kubernetes already has a signed file the cluster put there for it.· A program on a known machine already has a user account, a file path and a fixed binary the operating system can vouch for. The vault checks that with the platform — the cloud, the cluster, the operating system — and only then hands over the real secret. SO THE THING YOUR CODE CARRIES AT REST IS NOT A PASSWORD AT ALL It carries a claim about where it is running. That claim is worthless if copied to a laptop, which is the entire point. Everything else in this chapter — every login method in either product — is a variation on that one move.
Understand this one picture and the rest of the chapter is detail. Every login method in either product is a way of breaking that circle.

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.

MethodWhat proves who you areUse it when
KubernetesThe signed file the cluster already placed inside your containerYou 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 bootYou run on cloud compute. Nothing to store at all
AppRoleTwo halves: a RoleID, which is safe to bake into the image, and a SecretID, which is notThere 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 themYou have a single sign-on system, or any issuer that publishes its signing keys
TokenA Vault token you pasted inBootstrapping 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:

pythonhvac · python-hvac.org — approle and kv_v2 pages
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:

pythonhvac — kubernetes auth page, quoted from the docs
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:

pythonhvac — database secrets engine
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.

gogithub.com/hashicorp/vault-examples — token-renewal example, trimmed of error handling
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.

“WE USE CYBERARK” DOES NOT TELL YOU WHICH PRODUCT — AND THEY WORK DIFFERENTLY PAM — PRIVILEGED ACCESS MANAGER The vault itself. Where the passwords actually live, and where a human administrator’s session gets recorded on video. Built for PEOPLE first. It also rotates passwords on accounts that already exist — a domain admin, a mainframe login. This is usually what an enterprise bought first. CONJUR / SECRETS MANAGER A separate product with its own store and its own policy language, built for APPLICATIONS from the start. Closest in spirit to HashiCorp Vault. Comes in three editions: Open Source, Self-Hosted, and SaaS. The last two were renamed from “Conjur Enterprise” and “Conjur Cloud”. CREDENTIAL PROVIDERS — CP AND CCP. NOT A THIRD VAULT. This is the layer an application uses to read out of the PAM vault above, instead of a human doing it. CP runs locally on your server and keeps an encrypted cache. CCP is a shared web service you call over the network. The giveaway that it is not its own store: its documentation says folders are only supported in PAM. AND THE NAMES ARE MOVING UNDER YOUR FEET Palo Alto Networks completed its acquisition of CyberArk on 11 February 2026, and introduced Idira on 12 May 2026. The documentation site currently reads “Idira Docs” at the top while the page footers still say CyberArk. Expect more change.
Work out which of these three you are actually being pointed at before writing any code — the integration is completely different for each.

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:

httpdocs.cyberark.com — calling the web service using REST
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:

shelldocs.cyberark.com — CLI application password SDK syntax
# 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,UserName

Conjur, 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:

httpdocs.cyberark.com — conjur API key authentication, retrieve a 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:

pythonbuilt from the documented request shapes above — CyberArk also publishes conjur-api-python
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,
).text

In 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 VaultCyberArk
Creates brand-new credentials on demandCentral to how it works, across databases, clouds, certificates and SSHSupported narrowly. The core model is stored values that get rotated
Rotates passwords on accounts that already existNot the main ideaCentral — a domain admin account, a mainframe login, an existing service account
Records a human's admin sessionNo equivalentYes, and it is a large part of why it was bought
Typical arrivalBottom-up, from platform teams, because it is API-first and cheap against KubernetesTop-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

What is 'secret zero'?The credential you need before you can ask for any other. If it lives in a config file you have solved nothing — you have added a network call in front of the same problem.
How do you break the circle?Prove who you are with something the platform already gave the machine — a cloud identity, a Kubernetes token, an OS user and binary path — rather than with a string you were handed.
Why are AppRole's two halves delivered differently?The RoleID grants nothing alone so it can ship in config. The SecretID is the password half and must never sit at rest — it travels in a single-use sealed envelope, so an interception is detectable.
What is a dynamic secret and what does it cost you?Vault creates a brand-new username and password per request, with an expiry. The cost is that your code must renew before it lapses and treat expiry as normal.
What is the most common production bug here?Logging in once at start-up and never again. It works in testing and fails in production at an hour old.
Is CyberArk one product?No — PAM is the vault, Credential Providers are how applications read from it, and Conjur is a separate cloud-native product. The integration differs completely.
In CCP, what do AppID, Safe and Object do?They choose which password you get. They do not prove you are allowed to ask — that is separate, using the calling machine, OS user, binary path or hash, or a client certificate.
Why is Conjur's eight-minute token a good thing?It makes 'log in once at start-up' structurally impossible, so it prevents the most common bug in this area by design.
Why do enterprises run both Vault and CyberArk?Different populations. CyberArk arrived top-down for human privileged access and legacy accounts; Vault bottom-up for cloud-native workloads. Consolidating rarely justifies the audit disruption.
← Which language, and what the interview actually tests in eachGit and GitHub, actually explained →