Use Conjur or CCP for app secrets instead of config files
For developers replacing plaintext secrets in app config with runtime retrieval from Conjur or Central Credential Provider. This walks you through wiring a sample app to fetch a secret, passing the right auth material, and verifying the app no longer depends on a local secret value.
TL;DR — Replace the secret value in your app config with a secret identifier, then fetch the real value at runtime from Conjur or Central Credential Provider using a machine identity. The most common failure is using the wrong secret path/object name or sending the wrong auth header/certificate, which shows up immediately as HTTP 401/403/404 from the secret endpoint. Reading time: ~5 min
Goal
When you finish, your application starts without a plaintext password/API key in its config file, retrieves that secret at runtime from Conjur or Central Credential Provider (CCP), and you can prove it by rotating the secret in the vault without editing the app config.
Prerequisites
- Access to Conjur or CCP with permission to read one existing secret
- A machine identity already approved for secret retrieval:
- Conjur: host/app identity and its API key or authn method details
- CCP: AppID, Safe, Folder, Object, and client certificate or other required auth material
- The exact secret identifier you will read:
- Conjur example:
data/vault/myapp/db-password - CCP example:
Safe=AppSafe,Folder=Root,Object=myapp-db-password
- Conjur example:
curl >= 7.76— check with:
curl --version
jq >= 1.6— check with:
jq --version
- Your app’s config file or environment-loading entry point
- The base URL for your secret service, for example:
- Conjur:
https://conjur.example.com - CCP:
https://ccp.example.com
- Conjur:
- If CCP uses mutual TLS, the client certificate and key files on disk, for example
/etc/ccp/client.crtand/etc/ccp/client.key
Steps
Step 1: Remove the plaintext secret from app config and replace it with a secret reference
Edit your app config so it stores a secret identifier, not the secret value.
{
"db": {
"host": "db.internal.example.com",
"user": "myapp",
"passwordFrom": {
"provider": "conjur",
"id": "data/vault/myapp/db-password"
}
}
}
Or for CCP:
{
"db": {
"host": "db.internal.example.com",
"user": "myapp",
"passwordFrom": {
"provider": "ccp",
"appid": "myapp-prod",
"safe": "AppSafe",
"folder": "Root",
"object": "myapp-db-password"
}
}
}
You should see that the config contains only identifiers like id, safe, or object, and no actual password string.
Step 2: Prove your identity can read the secret outside the app
For Conjur, authenticate and fetch a secret with curl.
export CONJUR_URL="https://conjur.example.com"
export CONJUR_ACCOUNT="myorg"
export CONJUR_LOGIN="host/myapp-prod"
export CONJUR_API_KEY="paste-the-host-api-key-here"
export CONJUR_SECRET_ID="data/vault/myapp/db-password"
TOKEN=$(curl -sS --fail \
--data-urlencode "login=${CONJUR_LOGIN}" \
--data-urlencode "password=${CONJUR_API_KEY}" \
"${CONJUR_URL}/authn/${CONJUR_ACCOUNT}/login" | \
xargs -0 printf "%s" | \
base64 | tr -d '\n')
curl -sS --fail \
-H "Authorization: Token token=\"${TOKEN}\"" \
"${CONJUR_URL}/secrets/${CONJUR_ACCOUNT}/variable/${CONJUR_SECRET_ID}"
For CCP, request the password using the exact query parameters and client certificate if required.
export CCP_URL="https://ccp.example.com"
export APPID="myapp-prod"
export SAFE="AppSafe"
export FOLDER="Root"
export OBJECT="myapp-db-password"
curl -sS --fail \
--cert /etc/ccp/client.crt \
--key /etc/ccp/client.key \
"${CCP_URL}/AIMWebService/api/Accounts?AppID=${APPID}&Safe=${SAFE}&Folder=${FOLDER}&Object=${OBJECT}" | jq .
You should see either the raw secret value (Conjur) or a JSON document containing the secret field/value (CCP).
Step 3: Capture the exact success and failure signals before wiring the app
Run one HEAD or verbose request so you know what healthy and unhealthy responses look like.
curl -sS -o /dev/null -D - \
--cert /etc/ccp/client.crt \
--key /etc/ccp/client.key \
"${CCP_URL}/AIMWebService/api/Accounts?AppID=${APPID}&Safe=${SAFE}&Folder=${FOLDER}&Object=${OBJECT}"
Example healthy output shape:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 248
Example bad certificate or missing auth output shape:
HTTP/1.1 401 Unauthorized
Content-Type: text/html; charset=utf-8
Content-Length: 1293
For Conjur, a bad token usually looks like:
HTTP/1.1 403 Forbidden
Content-Type: application/json
You should see 200 OK for the working request and a reproducible 401, 403, or 404 when you intentionally break auth or the secret path.
Step 4: Wire runtime retrieval into the app startup
If your app already reads environment variables, fetch the secret before launching the process and export it as DB_PASSWORD.
Conjur example:
#!/usr/bin/env bash
set -euo pipefail
CONJUR_URL="https://conjur.example.com"
CONJUR_ACCOUNT="myorg"
CONJUR_LOGIN="host/myapp-prod"
CONJUR_API_KEY_FILE="/run/secrets/conjur_api_key"
CONJUR_SECRET_ID="data/vault/myapp/db-password"
TOKEN=$(curl -sS --fail \
--data-urlencode "login=${CONJUR_LOGIN}" \
--data-urlencode "password=$(cat ${CONJUR_API_KEY_FILE})" \
"${CONJUR_URL}/authn/${CONJUR_ACCOUNT}/login" | \
xargs -0 printf "%s" | \
base64 | tr -d '\n')
export DB_PASSWORD=$(curl -sS --fail \
-H "Authorization: Token token=\"${TOKEN}\"" \
"${CONJUR_URL}/secrets/${CONJUR_ACCOUNT}/variable/${CONJUR_SECRET_ID}")
exec ./myapp
CCP example:
#!/usr/bin/env bash
set -euo pipefail
CCP_URL="https://ccp.example.com"
APPID="myapp-prod"
SAFE="AppSafe"
FOLDER="Root"
OBJECT="myapp-db-password"
export DB_PASSWORD=$(curl -sS --fail \
--cert /etc/ccp/client.crt \
--key /etc/ccp/client.key \
"${CCP_URL}/AIMWebService/api/Accounts?AppID=${APPID}&Safe=${SAFE}&Folder=${FOLDER}&Object=${OBJECT}" | jq -r '.Content // .content // .Password // .password')
exec ./myapp
You should see the app start normally, and echo $? after exit should be 0 for a clean run.
⚠️ If you replace a working plaintext secret in production before testing retrieval from the target host/container, the app can fail to start and cause downtime. Test the exact startup script on the same runtime first.
Step 5: Store the auth material outside the app config file too
Do not move the password out of config but leave the Conjur API key or CCP client key in the same repo. Put auth material in a file mounted by the runtime or in the platform’s secret store.
Linux file example:
install -m 0400 -o appuser -g appuser /dev/null /run/secrets/conjur_api_key
printf '%s' 'paste-the-host-api-key-here' > /run/secrets/conjur_api_key
chown appuser:appuser /run/secrets/conjur_api_key
chmod 0400 /run/secrets/conjur_api_key
You should see -r-------- permissions when you run:
ls -l /run/secrets/conjur_api_key
Verify it works
- Start the app with the runtime retrieval script.
./start-myapp.sh
- Confirm the app can use the secret end to end. For a DB-backed app, check the app health endpoint or logs.
curl -sS -i http://127.0.0.1:8080/health
Expected output shape:
HTTP/1.1 200 OK
Content-Type: application/json
{"status":"ok"}
-
Rotate the secret in Conjur or CCP, then restart the app without editing config. The app should still come up.
-
Grep the repo and deployed config for the old plaintext secret to confirm it is gone.
grep -R --line-number --fixed-strings 'old-password-value' . || true
Expected result: no matches.
Common pitfalls
Wrong Conjur variable ID or CCP object name
Mistake: using db-password when the actual path/object is data/vault/myapp/db-password or a different Object value.
Symptom: curl returns 404 Not Found or CCP returns an empty/no matching account response.
Fix: copy the exact secret identifier from the vault and retry the same curl command unchanged except for that field.
Using the wrong auth identity for the environment
Mistake: authenticating with a dev host/AppID against prod secrets.
Symptom: 401 Unauthorized or 403 Forbidden, even though the endpoint is reachable.
Fix: switch CONJUR_LOGIN or APPID to the identity approved for that environment and rerun the standalone fetch test.
Client certificate/key mismatch for CCP
Mistake: passing a certificate file that does not match the private key, or using an expired cert.
Symptom: TLS handshake errors such as curl: (58) could not load PEM client certificate or server-side 401 Unauthorized.
Fix: verify the cert/key pair and expiry, then rerun with the correct --cert and --key files.
Parsing the wrong JSON field from CCP
Mistake: assuming the password is always in .Content when your deployment returns a different field name/casing.
Symptom: app starts with an empty DB_PASSWORD and then fails DB login.
Fix: inspect the raw JSON with jq . and update the parser, for example .Content // .content // .Password // .password.
Leaving secrets in process arguments or logs
Mistake: using a command that echoes the secret or starts the app with --db-password=....
Symptom: the secret appears in shell history, CI logs, or ps output.
Fix: export the secret as an environment variable or read it from stdin/file, and remove any set -x from startup scripts.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI