Build a Free CyberArk PAM Lab

Deploy a local CyberArk Conjur vault and simulate credential retrieval and rotation.

Introduction

30 Second Summary

Every organization has critical systems protected by passwords that hold the keys to their kingdom. When those passwords are hardcoded, shared in spreadsheets, or never rotated, a single breach can cascade into total compromise.

In this project (Part 1), you will build a local Privileged Access Management lab using CyberArk Conjur OSS that stores, retrieves, and rotates privileged credentials automatically. Everything runs in Docker containers on your Windows machine. No VirtualBox and no virtual machines are needed here. The two target VMs are built later in Part 2.

What You'll Build

A local PAM lab running entirely in Docker that mirrors the core architecture of CyberArk Privileged Access Manager, complete with policy-defined access control and automated credential management scripts.

By the end of this project, you'll have:

  • A Conjur OSS vault running in Docker that simulates the CyberArk Digital Vault for storing encrypted credentials.
  • A YAML policy file defining Safes, privileged accounts, and role-based access control permissions.
  • Python automation scripts that retrieve and rotate passwords via REST API, simulating the CyberArk CCP and CPM components.
  • Secret Mission: Add multi-account rotation with timestamped audit logging for interview demonstration.

Are there any prerequisites?

No prior experience with CyberArk or secrets management is needed. You only need to install three tools: Docker Desktop, Python, and VS Code. Step 1 walks you through each installation.

This is Part 1 (vault and automation scripts). Part 2 is a separate project that adds an Apache Guacamole session proxy with two target VMs for recording privileged sessions.

Install Your Tools

Before you can deploy a vault, you need three tools on your Windows machine: a container runtime to host Conjur, a scripting language for your automation scripts, and a code editor to write them in.

In this step, you will install Docker Desktop, Python, and VS Code. These are the only tools you need for Part 1. No virtual machines and no VirtualBox.

Where are the target VMs?

The two target virtual machines (a Linux server and a Windows server) are built later in Part 2. Part 2 is a separate project that adds an Apache Guacamole session proxy alongside this vault stack.

For now, everything runs in Docker containers on your Windows host. No VirtualBox installation is needed in this part.

In this step, get ready to:
  • Install Docker Desktop, Python, and VS Code.
  • Create the project folder and reserve port 8080 for Conjur.
  • Verify all three tools are working.
Install Docker Desktop

Docker Desktop provides the container engine that runs your Conjur vault. All the vault components (the Conjur server, PostgreSQL database, nginx proxy, and CLI client) run as containers.

  • Open your browser and go to https://www.docker.com/products/docker-desktop/.
  • Click Download for Windows.
  • Run the installer and accept the default options.
  • When the installer finishes, restart your computer if prompted.
  • After restart, Docker Desktop launches automatically. Wait for the whale icon in your taskbar to stop animating (this means the engine is ready).

Verify Docker is working by opening PowerShell and running a version check.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Check your Docker version by running this command:
docker --version

✔️ I see a version number

Docker is installed and ready. You should see something like Docker version 27.x.x. Move on to the next tool.

ⓧ Command not found or error

Make sure Docker Desktop is running (look for the whale icon in your taskbar). If you just installed it, close and reopen PowerShell so it picks up the new PATH.

If you still see an error, restart your computer and try again after Docker Desktop finishes starting.

Docker Desktop won't start?

On Windows 10/11, Docker Desktop requires WSL 2 or Hyper-V enabled. The installer normally handles this, but if you see an error about virtualization, open Turn Windows features on or off from the Start menu and enable Windows Subsystem for Linux.

help me fix Docker Desktop not starting on Windows

Install Python

Python powers the CCP retrieval and CPM rotation scripts you will build later. You need version 3.11 or higher.

  • Open your browser and go to https://www.python.org/downloads/.
  • Click Download Python 3.x.x (the latest stable version).
  • Run the installer.
  • On the first screen, check the box that says Add python.exe to PATH before clicking Install Now.

Do not skip the PATH checkbox

If you miss the Add python.exe to PATH checkbox, PowerShell will not find the python command later. If you already ran the installer without it, re-run the installer, choose Modify, and check the PATH option.

  • Close and reopen PowerShell (so it picks up the new PATH).
  • Check your Python version by running this command:
python --version

✔️ I see version 3.11 or higher

Python is ready. Move on to the next tool.

ⓧ I see a version lower than 3.11

Download the latest Python from the link above and install it. The new installer will update your existing installation. Make sure to check Add python.exe to PATH during the install.

ⓧ Command not found

Python is not installed or not on your PATH. Download and install it from the link above. Make sure to check Add python.exe to PATH on the first screen of the installer. After installing, close and reopen PowerShell before retrying.

Install VS Code

VS Code is where you will write the PAM policy file and both Python scripts.

  • Open your browser and go to https://code.visualstudio.com/.
  • Click Download for Windows.
  • Run the installer and accept the default options.
  • When it finishes, open VS Code to confirm it launches.
Create the project folder and note the port reservation

All project files live in a single folder on your C: drive. You will also reserve port 8080 for the Conjur vault so there are no conflicts later.

  • Create the project folder by running this command in PowerShell:
mkdir C:\pam-lab

Why port 8080?

The Conjur quickstart exposes its REST API on host port 8080. Your Python scripts will call http://localhost:8080 to authenticate and retrieve secrets.

Part 2 (the Guacamole session proxy) uses a different port to avoid a conflict, so both stacks can run side by side.

  • Confirm the folder exists by running:
ls C:\pam-lab

You should see an empty directory listed (or a message that the folder has no contents). This confirms the folder was created.

Your tools are installed and your project folder is ready. Next up, you will deploy the Conjur vault that stores all your privileged credentials.

Deploy Conjur OSS as Your Digital Vault

Your tools are installed and your project folder is ready. Now it's time to deploy the heart of any PAM system: the secrets vault.

CyberArk's Digital Vault is where all privileged credentials live, encrypted and access-controlled. You'll deploy Conjur OSS in Docker to simulate this vault locally, giving you a real vault to store and retrieve secrets from.

In this step, get ready to:
  • Clone the official Conjur quickstart repository and start the vault containers.
  • Create the admin account and connect the CLI client.
  • Log in as admin and verify the vault is operational.
Clone the official Conjur quickstart and start the vault

CyberArk maintains an official quickstart repository that bundles all the containers you need into a single Docker Compose setup. This includes the Conjur server, a PostgreSQL database, an nginx proxy, and a CLI client.

  • In your PowerShell window, navigate to your project folder and clone the quickstart by running these commands:
cd C:\pam-lab
git clone https://github.com/cyberark/conjur-quickstart.git
cd conjur-quickstart
docker compose pull

What do these commands do?

  • git clone downloads the official CyberArk quickstart repository to your machine.
  • cd conjur-quickstart moves you into the cloned folder.
  • docker compose pull downloads all the Docker images the vault needs. This may take a few minutes depending on your internet speed.

🙋‍♀️ Seeing 'error during connect' or 'docker daemon is not running'?

Make sure Docker Desktop is running. Look for the whale icon in your taskbar. If it's not there, open Docker Desktop and wait for the engine to fully start before retrying.

help me fix Docker connection errors in PowerShell

Once the pull completes, you need to generate the master encryption key that protects all secrets stored in the vault.

  • Generate the master encryption key and load it into your PowerShell session by running these commands:
docker compose run --no-deps --rm conjur data-key generate > data_key
$env:CONJUR_DATA_KEY = Get-Content .\data_key -Raw

What is the master encryption key?

The master encryption key (stored in data_key) is what Conjur uses to encrypt every secret in the database. Without this key, the vault cannot decrypt anything.

In production CyberArk, this is equivalent to the Digital Vault's server key. The second command loads it into your PowerShell session so the Conjur container can read it at startup.

  • Start the Conjur environment by running:
docker compose up -d
  • Verify the containers are running by running:
docker ps

You should see multiple containers listed, including conjur_server, postgres_database, nginx_proxy, and conjur_client.

Containers not starting or exiting immediately?

If containers show as "Exited", check that the CONJUR_DATA_KEY environment variable is set. Run $env:CONJUR_DATA_KEY in PowerShell to confirm it shows a long string of characters.

If the variable is empty, re-run both the key generation and environment variable commands above, then run docker compose up -d again.

help me fix my Conjur containers that keep exiting

Create the admin account and connect the CLI client

With the vault running, you need to create an account inside it. This generates an admin API key that acts as the master credential for managing the vault.

  • Create the Conjur account by running this command:
docker compose exec conjur conjurctl account create myConjurAccount > admin_data

What does this command do?

This creates an account called myConjurAccount inside the Conjur vault. The output (saved to admin_data) contains the admin user's API key, which you'll need for all future authentication.

In CyberArk terms, this is like creating the initial Vault administrator account during a fresh installation.

  • View the admin API key by running:
Get-Content .\admin_data

Save your admin API key

The output contains a line with your API key. Copy this value and keep it somewhere safe. You will need it in the next commands and in later steps when building your Python scripts.

  • Connect the CLI client to the vault by running:
docker compose exec client conjur init oss -u https://proxy -a myConjurAccount --self-signed

What will this command ask you?

This command will prompt you twice:

  • It asks whether to trust the self-signed TLS certificate. Type y and press Enter.
  • It asks you to select the environment type. Select Conjur Open Source from the list.

🙋‍♀️ Getting a connection refused error?

The Conjur server may still be starting up. Wait 10-15 seconds after docker compose up -d and try the init command again.

help me connect the Conjur CLI client to my vault

Log in as admin and verify the vault is operational

The CLI client is connected but not yet authenticated. You need to log in with the admin API key to prove your identity to the vault.

  • Log in to the vault as admin by running this command, replacing the placeholder with your actual API key from admin_data:
docker compose exec client conjur login -i admin -p your-api-key-from-admin_data

You should see the message Logged in confirming successful authentication.

Seeing 'Unauthorized' or login failed?

Double-check that you copied the API key exactly from the admin_data file. The key is a long string with no spaces. Make sure you did not accidentally include extra whitespace or line breaks.

help me fix my Conjur admin login error

  • Verify the vault is operational by listing all resources:
docker compose exec client conjur list

You should see an empty JSON array [] returned. This confirms the vault is running and accepting commands. It's empty because you haven't loaded any policies or secrets yet.

Your vault is deployed and ready for credentials. In the next step, you'll load PAM policies that define Safes, privileged accounts, and role-based access permissions.

Load PAM Policies and Store Privileged Credentials

Your vault is running, but it is completely empty. Without a policy, Conjur has no concept of which credentials exist or who is allowed to access them.

In CyberArk production environments, administrators define Safes that group privileged accounts with RBAC permissions controlling who can read or rotate those passwords. In this step, you will create a YAML policy that mirrors that exact structure, load it into the vault, and store your first privileged credentials.

In this step, get ready to:
  • Create a YAML policy file defining Safes, accounts, and RBAC permissions.
  • Load the policy into Conjur and store initial credentials.
  • Verify secrets are stored and retrievable from the vault.
Create the PAM policy file

A Conjur policy is a YAML file that declares resources (variables, hosts, layers) and the permissions between them. Think of it as the blueprint that tells the vault what credentials exist and who gets access.

  • In VS Code, expand the conjur-quickstart folder in the Explorer sidebar.
  • Navigate to conf/policy/ inside conjur-quickstart.
  • Right-click the policy folder and select New File.
  • Name the file pam-policy.yml and press Enter.
  • Paste the following policy into the new file:
# PAM Lab Policy - Maps to CyberArk Safe + Account structure
# This policy defines a "Safe" with privileged accounts and service identities

# Define the privileged account variables (the "Safe" contents)
- !variable prod-servers/linux-root-password
- !variable prod-servers/windows-admin-password

# Define host identities (machine accounts that access the vault)
- !host cpm-service
- !host app-server

# Define a layer (equivalent to a CyberArk Safe)
- !layer prod-servers

# Grant layer membership
- !grant
  role: !layer prod-servers
  members:
    - !host app-server

# Permissions: CPM service can read and update passwords (rotate)
- !permit
  role: !host cpm-service
  privileges: [ read, execute, update ]
  resources:
    - !variable prod-servers/linux-root-password
    - !variable prod-servers/windows-admin-password

# Permissions: App servers can only read passwords (retrieve via CCP)
- !permit
  role: !layer prod-servers
  privileges: [ read, execute ]
  resources:
    - !variable prod-servers/linux-root-password
    - !variable prod-servers/windows-admin-password

What does this policy define?

This policy maps directly to how CyberArk structures access in production:

  • !variable declarations create the privileged account entries (the passwords stored in the Safe).
  • !host declarations create machine identities. cpm-service simulates the CPM that rotates passwords. app-server simulates an application that retrieves them.
  • !layer groups hosts together, equivalent to a CyberArk Safe membership.
  • !permit statements grant specific privileges. The CPM gets read, execute, and update (so it can rotate). App servers get read and execute only (retrieve, no changes).
  • Save pam-policy.yml by pressing Ctrl+S.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before continuing.

ⓧ I'd like to double check the full code

Here is the complete pam-policy.yml file for comparison:

# PAM Lab Policy - Maps to CyberArk Safe + Account structure
# This policy defines a "Safe" with privileged accounts and service identities

# Define the privileged account variables (the "Safe" contents)
- !variable prod-servers/linux-root-password
- !variable prod-servers/windows-admin-password

# Define host identities (machine accounts that access the vault)
- !host cpm-service
- !host app-server

# Define a layer (equivalent to a CyberArk Safe)
- !layer prod-servers

# Grant layer membership
- !grant
  role: !layer prod-servers
  members:
    - !host app-server

# Permissions: CPM service can read and update passwords (rotate)
- !permit
  role: !host cpm-service
  privileges: [ read, execute, update ]
  resources:
    - !variable prod-servers/linux-root-password
    - !variable prod-servers/windows-admin-password

# Permissions: App servers can only read passwords (retrieve via CCP)
- !permit
  role: !layer prod-servers
  privileges: [ read, execute ]
  resources:
    - !variable prod-servers/linux-root-password
    - !variable prod-servers/windows-admin-password
Load the policy and store initial secrets

Now that your policy file exists on disk, you need to load it into the Conjur server so it creates the resources and host identities.

  • Return to the PowerShell window where your Docker containers are running.
  • Make sure you are still in the C:\pam-lab\conjur-quickstart directory.
  • Load the policy into the Conjur root branch by running this command:
docker compose exec client conjur policy load -b root -f /policy/pam-policy.yml

What does this command do?

This tells the Conjur CLI client (running inside Docker) to load your YAML policy file at the root level of the policy tree. The -b root flag means "load into the root branch" and -f /policy/pam-policy.yml points to the file inside the container (the conf/policy/ folder is mounted at /policy/ inside the client container).

The output is a JSON object with a created_roles section containing the API keys for your newly created host identities (cpm-service and app-server). Save these API keys somewhere safe. You would need them if you wanted to authenticate as those hosts later.

  • You should see JSON output listing created_roles with API keys for cpm-service and app-server. Copy this output and save it in a text file for reference.

Seeing an error about the policy file?

  • Make sure the file is saved as pam-policy.yml (not .yaml) inside conjur-quickstart/conf/policy/.
  • Check for YAML indentation errors. The members: and resources: lines must be indented with exactly 2 spaces under their parent.
  • If you see created_roles: {} (empty), the policy was already loaded. This is fine because policy loads are idempotent.

Still stuck? Help me troubleshoot my policy load error.

Now store the initial privileged credentials in the vault. These simulate the passwords that a real CyberArk deployment would manage for production servers.

  • Store the Linux root password by running this command:
docker compose exec client conjur variable set -i prod-servers/linux-root-password -v "R00t!Initial#2026"
  • Store the Windows admin password by running this command:
docker compose exec client conjur variable set -i prod-servers/windows-admin-password -v "Admin!Initial#2026"

What is happening here?

The variable set command stores a secret value inside a variable that was defined by your policy. The -i flag specifies which variable to target, and -v provides the value. In a real CyberArk deployment, these would be actual root and admin passwords managed by the vault.

Verify secrets are stored and retrievable

Time to confirm that your vault is working end-to-end. You will retrieve a stored secret and list all resources to see your complete Safe structure.

  • Retrieve the Linux root password from the vault by running this command:
docker compose exec client conjur variable get -i prod-servers/linux-root-password
  • You should see R00t!Initial#2026 printed in your terminal. This confirms the vault is storing and returning your credential correctly.
  • List all resources in the vault by running this command:
docker compose exec client conjur list
  • You should see a JSON array listing your variables (prod-servers/linux-root-password, prod-servers/windows-admin-password), hosts (cpm-service, app-server), and the prod-servers layer.

Variable get returns an error?

  • Make sure you ran both variable set commands successfully before trying to retrieve.
  • Check the variable ID matches exactly: prod-servers/linux-root-password (case-sensitive, forward slash).
  • If you see a 404, the policy may not have loaded correctly. Run conjur list to check if the variables exist.

Need help? Help me debug why my variable retrieval is failing.

Your vault now has a fully loaded policy with privileged credentials stored securely. Next up, you will build a Python script that retrieves these secrets via REST API, simulating how CyberArk's Central Credential Provider works in production.

Build the CCP Retrieval Script

Your vault is loaded with privileged credentials and your RBAC policies control who can access them. Now it's time to build the script that retrieves those credentials programmatically.

In a real CyberArk environment, applications never hardcode passwords. Instead, they call the Central Credential Provider (CCP) REST API at runtime to fetch credentials from the vault. You'll build a Python script that does exactly this pattern.

In this step, get ready to:
  • Create a Python virtual environment and install the HTTP library.
  • Write a CCP retrieval script that authenticates and fetches secrets via REST API.
  • Run the script and verify it retrieves both privileged credentials from the vault.
Set up the Python environment

Python scripts need their own isolated environment so that packages don't conflict with other projects on your machine. A virtual environment keeps everything contained inside your project folder.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open a new PowerShell window.
  • Keep your existing PowerShell window running (the one managing Docker containers).
  • Navigate to your project folder and create the virtual environment by running these commands:
cd C:\pam-lab
python -m venv venv
  • Activate the virtual environment by running:
.\venv\Scripts\Activate.ps1

Your prompt should now show (venv) at the beginning, confirming the virtual environment is active.

Seeing a script execution error?

PowerShell may block the activation script by default. Run this command to allow it, then try the activation again:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

help me fix a PowerShell execution policy error when activating my Python venv

  • Install the HTTP library your script needs by running:
pip install requests==2.34.2

What is requests?

The requests library is Python's go-to tool for making HTTP calls. Your script will use it to talk to the Conjur REST API, the same way the real CyberArk CCP communicates with the Digital Vault.

You should see a success message ending with Successfully installed requests-2.34.2 and its dependencies.

Pip install failing?

Make sure you see (venv) in your prompt. If not, the virtual environment is not active and pip may install to the wrong location.

Also confirm you have an internet connection, since pip downloads packages from PyPI.

help me troubleshoot pip install failing in my Python virtual environment

Create the CCP retrieval script

The CCP retrieval script simulates how a real application would request credentials from CyberArk at runtime. It follows a two-step pattern: authenticate to get a token, then use that token to fetch secrets.

Why raw REST API instead of a Python SDK?

CyberArk's Python SDK is in early community status and subject to breaking changes. Using raw REST calls teaches you exactly how the CCP works at the protocol level.

This is more valuable for interviews because you can explain the authentication handshake step by step, not just say you called a library function.

  • In VS Code, create a new file called ccp_retriever.py in the C:\pam-lab folder (the project root, not inside conjur-quickstart).
  • Paste the following code into the file:
""" 
CCP Retriever - Simulates CyberArk Central Credential Provider (CCP)

This script demonstrates the same REST API pattern that CyberArk CCP uses:
1. Authenticate with an identity (API key) to get a short-lived token
2. Use that token to retrieve privileged credentials from the vault

Interview talking point: "The CCP provides a REST API that applications
call to retrieve credentials at runtime, eliminating hardcoded passwords."
"""

import os
import sys
import base64
import urllib.parse
import requests

# Configuration - maps to CyberArk connection parameters
CONJUR_URL = "http://localhost:8080"
CONJUR_ACCOUNT = "myConjurAccount"
CONJUR_LOGIN = "admin"

# The API key simulates a CyberArk application identity credential
API_KEY = os.environ.get("CONJUR_API_KEY")

if not API_KEY:
    print("ERROR: Set the CONJUR_API_KEY environment variable first.")
    print("  PowerShell: $env:CONJUR_API_KEY = 'your-key-here'")
    sys.exit(1)


def authenticate(conjur_url, account, login, api_key):
    """
    Authenticate to Conjur and get an access token.
    Maps to: CyberArk CCP authentication handshake.
    """
    login_encoded = urllib.parse.quote(login, safe="")
    url = f"{conjur_url}/authn/{account}/{login_encoded}/authenticate"

    response = requests.post(url, data=api_key, verify=False)
    response.raise_for_status()

    # The token must be base64-encoded for use in the Authorization header
    token_b64 = base64.b64encode(response.content).decode("utf-8")
    return token_b64


def retrieve_secret(conjur_url, account, token_b64, variable_id):
    """
    Retrieve a secret from the vault using the access token.
    Maps to: CyberArk CCP GetPassword API call.
    """
    variable_encoded = urllib.parse.quote(variable_id, safe="")
    url = f"{conjur_url}/secrets/{account}/variable/{variable_encoded}"

    headers = {"Authorization": f'Token token="{token_b64}"'}
    response = requests.get(url, headers=headers, verify=False)
    response.raise_for_status()

    return response.text


def main():
    print("=" * 60)
    print("CCP RETRIEVER - CyberArk Central Credential Provider Sim")
    print("=" * 60)
    print(f"\nVault URL: {CONJUR_URL}")
    print(f"Account:   {CONJUR_ACCOUNT}")
    print(f"Identity:  {CONJUR_LOGIN}")
    print()

    # Step 1: Authenticate (get short-lived token)
    print("[1] Authenticating to vault...")
    token = authenticate(CONJUR_URL, CONJUR_ACCOUNT, CONJUR_LOGIN, API_KEY)
    print("    SUCCESS - Received access token")

    # Step 2: Retrieve privileged credentials
    secrets_to_retrieve = [
        "prod-servers/linux-root-password",
        "prod-servers/windows-admin-password",
    ]

    print("\n[2] Retrieving privileged credentials from Safe...")
    for variable_id in secrets_to_retrieve:
        secret = retrieve_secret(CONJUR_URL, CONJUR_ACCOUNT, token, variable_id)
        print(f"    {variable_id}: {secret}")

    print("\n" + "=" * 60)
    print("INTERVIEW NOTE: This mirrors the CyberArk CCP REST API flow.")
    print("Applications never store passwords - they retrieve at runtime.")
    print("=" * 60)


if __name__ == "__main__":
    main()

What does this script do?

The script follows the exact same two-step flow that CyberArk's real CCP uses:

  • authenticate() sends the API key to the POST /authn/{account}/{login}/authenticate endpoint and receives a short-lived access token.
  • retrieve_secret() uses that token to call GET /secrets/{account}/variable/{id} and fetch the actual password value.
  • main() orchestrates both steps and prints the results, showing credentials are never stored in the script itself.
  • Save ccp_retriever.py.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

""" 
CCP Retriever - Simulates CyberArk Central Credential Provider (CCP)

This script demonstrates the same REST API pattern that CyberArk CCP uses:
1. Authenticate with an identity (API key) to get a short-lived token
2. Use that token to retrieve privileged credentials from the vault

Interview talking point: "The CCP provides a REST API that applications
call to retrieve credentials at runtime, eliminating hardcoded passwords."
"""

import os
import sys
import base64
import urllib.parse
import requests

# Configuration - maps to CyberArk connection parameters
CONJUR_URL = "http://localhost:8080"
CONJUR_ACCOUNT = "myConjurAccount"
CONJUR_LOGIN = "admin"

# The API key simulates a CyberArk application identity credential
API_KEY = os.environ.get("CONJUR_API_KEY")

if not API_KEY:
    print("ERROR: Set the CONJUR_API_KEY environment variable first.")
    print("  PowerShell: $env:CONJUR_API_KEY = 'your-key-here'")
    sys.exit(1)


def authenticate(conjur_url, account, login, api_key):
    """
    Authenticate to Conjur and get an access token.
    Maps to: CyberArk CCP authentication handshake.
    """
    login_encoded = urllib.parse.quote(login, safe="")
    url = f"{conjur_url}/authn/{account}/{login_encoded}/authenticate"

    response = requests.post(url, data=api_key, verify=False)
    response.raise_for_status()

    # The token must be base64-encoded for use in the Authorization header
    token_b64 = base64.b64encode(response.content).decode("utf-8")
    return token_b64


def retrieve_secret(conjur_url, account, token_b64, variable_id):
    """
    Retrieve a secret from the vault using the access token.
    Maps to: CyberArk CCP GetPassword API call.
    """
    variable_encoded = urllib.parse.quote(variable_id, safe="")
    url = f"{conjur_url}/secrets/{account}/variable/{variable_encoded}"

    headers = {"Authorization": f'Token token="{token_b64}"'}
    response = requests.get(url, headers=headers, verify=False)
    response.raise_for_status()

    return response.text


def main():
    print("=" * 60)
    print("CCP RETRIEVER - CyberArk Central Credential Provider Sim")
    print("=" * 60)
    print(f"\nVault URL: {CONJUR_URL}")
    print(f"Account:   {CONJUR_ACCOUNT}")
    print(f"Identity:  {CONJUR_LOGIN}")
    print()

    # Step 1: Authenticate (get short-lived token)
    print("[1] Authenticating to vault...")
    token = authenticate(CONJUR_URL, CONJUR_ACCOUNT, CONJUR_LOGIN, API_KEY)
    print("    SUCCESS - Received access token")

    # Step 2: Retrieve privileged credentials
    secrets_to_retrieve = [
        "prod-servers/linux-root-password",
        "prod-servers/windows-admin-password",
    ]

    print("\n[2] Retrieving privileged credentials from Safe...")
    for variable_id in secrets_to_retrieve:
        secret = retrieve_secret(CONJUR_URL, CONJUR_ACCOUNT, token, variable_id)
        print(f"    {variable_id}: {secret}")

    print("\n" + "=" * 60)
    print("INTERVIEW NOTE: This mirrors the CyberArk CCP REST API flow.")
    print("Applications never store passwords - they retrieve at runtime.")
    print("=" * 60)


if __name__ == "__main__":
    main()
Run and test the retrieval

The script reads the API key from an environment variable, just like a real CCP application would use its assigned identity credential. You need to set this before running the script.

  • Set the API key environment variable by running the following command (replace your-api-key-here with the admin API key from your admin_data file):
$env:CONJUR_API_KEY = "your-api-key-here"

Where do I find my API key?

In your other PowerShell window (the Docker one), you printed the contents of admin_data in Step 2. The API key is the long string on the line labelled API key.

You can also view it again by running Get-Content .\conjur-quickstart\admin_data in your new PowerShell window.

  • Run the CCP retrieval script:
python ccp_retriever.py

You should see output showing successful authentication followed by both credentials retrieved from the vault:

============================================================
CCP RETRIEVER - CyberArk Central Credential Provider Sim
============================================================

Vault URL: http://localhost:8080
Account:   myConjurAccount
Identity:  admin

[1] Authenticating to vault...
    SUCCESS - Received access token

[2] Retrieving privileged credentials from Safe...
    prod-servers/linux-root-password: R00t!Initial#2026
    prod-servers/windows-admin-password: Admin!Initial#2026

============================================================
INTERVIEW NOTE: This mirrors the CyberArk CCP REST API flow.
Applications never store passwords - they retrieve at runtime.
============================================================

Seeing a connection error?

Make sure your Docker containers are still running. Switch to your other PowerShell window and run docker ps to check. If the containers stopped, run docker compose up -d from the conjur-quickstart folder to restart them.

If you see an authentication error, double-check that you pasted the correct API key (no extra spaces or line breaks) when setting $env:CONJUR_API_KEY.

help me debug a connection or authentication error running my CCP retriever script against Conjur

Your CCP simulator is working. Applications can now retrieve credentials at runtime without ever storing passwords in code. Next up, you'll build the CPM rotation script that automatically changes passwords on a schedule.

Build the CPM Password Rotation Script

Your CCP retriever proves you can pull credentials out of the vault on demand. But credentials that never change are a ticking time bomb.

In this step, you will build a script that simulates the Central Policy Manager (CPM). The CPM automatically rotates passwords so that compromised credentials expire before attackers can use them.

In this step, get ready to:
  • Create a Python CPM rotation script that generates and pushes new passwords to the vault.
  • Run the rotation and verify the password changed in the vault.
Create the CPM rotation script

The rotation script follows the same authenticate-then-act pattern as your CCP retriever, but adds two new capabilities: writing secrets back to the vault and generating strong random passwords.

  • In VS Code, create a new file called cpm_rotator.py in your C:\pam-lab folder.
  • Add the following imports, configuration, and familiar helper functions to start the file:
"""CPM Rotator - Simulates CyberArk Central Policy Manager (CPM)

This script demonstrates automated password rotation:
1. Authenticate to the vault
2. Generate a new strong password
3. Update the credential in the vault
4. Verify the rotation succeeded

Interview talking point: "The CPM automatically rotates passwords based
on the Master Policy schedule, ensuring credentials never become stale
and reducing the window of exposure if a password is compromised."
"""

import os
import sys
import base64
import secrets
import string
import urllib.parse
from datetime import datetime
import requests

# Configuration
CONJUR_URL = "http://localhost:8080"
CONJUR_ACCOUNT = "myConjurAccount"
CONJUR_LOGIN = "admin"

API_KEY = os.environ.get("CONJUR_API_KEY")

if not API_KEY:
    print("ERROR: Set the CONJUR_API_KEY environment variable first.")
    print("  PowerShell: $env:CONJUR_API_KEY = 'your-key-here'")
    sys.exit(1)

# Target account to rotate (simulates CPM managing an account)
TARGET_VARIABLE = "prod-servers/linux-root-password"


def authenticate(conjur_url, account, login, api_key):
    """Authenticate to Conjur and get an access token."""
    login_encoded = urllib.parse.quote(login, safe="")
    url = f"{conjur_url}/authn/{account}/{login_encoded}/authenticate"

    response = requests.post(url, data=api_key, verify=False)
    response.raise_for_status()

    token_b64 = base64.b64encode(response.content).decode("utf-8")
    return token_b64


def retrieve_secret(conjur_url, account, token_b64, variable_id):
    """Retrieve current secret value."""
    variable_encoded = urllib.parse.quote(variable_id, safe="")
    url = f"{conjur_url}/secrets/{account}/variable/{variable_encoded}"

    headers = {"Authorization": f'Token token="{token_b64}"'}
    response = requests.get(url, headers=headers, verify=False)
    response.raise_for_status()

    return response.text

What does this code do?

The configuration section is identical to your CCP retriever. It connects to the same Conjur vault on port 8080 with the same admin identity.

The TARGET_VARIABLE constant defines which privileged account this CPM instance manages. In production CyberArk, each CPM is assigned specific accounts to rotate.

The authenticate and retrieve_secret functions are the same helpers from your CCP script. The CPM needs both capabilities because it reads the current password before rotating.

  • Below the retrieve_secret function, add these two new functions that handle the rotation logic:

def update_secret(conjur_url, account, token_b64, variable_id, new_value):
    """
    Update a secret in the vault with a new value.
    Maps to: CPM pushing the new rotated password back into the vault.
    """
    variable_encoded = urllib.parse.quote(variable_id, safe="")
    url = f"{conjur_url}/secrets/{account}/variable/{variable_encoded}"

    headers = {"Authorization": f'Token token="{token_b64}"'}
    response = requests.post(url, data=new_value, headers=headers, verify=False)
    response.raise_for_status()


def generate_password(length=20):
    """
    Generate a strong random password.
    Maps to: CPM password generation per the Master Policy complexity rules.
    """
    alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
    password = "".join(secrets.choice(alphabet) for _ in range(length))
    return password

What do these new functions do?

  • update_secret uses a POST request to write a new value into the vault. This is the same endpoint as retrieval but with POST instead of GET. It mirrors how the real CPM pushes rotated passwords back into the Digital Vault.
  • generate_password uses Python's secrets module for cryptographically secure random character selection. The 20-character length and mixed character set simulate the complexity rules defined in a CyberArk Master Policy.
  • Below generate_password, add the main function that orchestrates the full rotation workflow:

def main():
    print("=" * 60)
    print("CPM ROTATOR - CyberArk Central Policy Manager Sim")
    print("=" * 60)
    print(f"\nTimestamp:  {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"Target:    {TARGET_VARIABLE}")
    print()

    # Step 1: Authenticate
    print("[1] Authenticating as CPM service account...")
    token = authenticate(CONJUR_URL, CONJUR_ACCOUNT, CONJUR_LOGIN, API_KEY)
    print("    SUCCESS - Authenticated")

    # Step 2: Retrieve current password (for audit/comparison)
    print("\n[2] Retrieving current password...")
    old_password = retrieve_secret(CONJUR_URL, CONJUR_ACCOUNT, token, TARGET_VARIABLE)
    print(f"    Current: {old_password[:4]}{'*' * (len(old_password) - 4)}")

    # Step 3: Generate new password
    print("\n[3] Generating new password (Master Policy: 20 chars, complex)...")
    new_password = generate_password(20)
    print(f"    New:     {new_password[:4]}{'*' * 16}")

    # Step 4: Update the vault
    print("\n[4] Pushing new password to vault...")
    update_secret(CONJUR_URL, CONJUR_ACCOUNT, token, TARGET_VARIABLE, new_password)
    print("    SUCCESS - Vault updated")

    # Step 5: Verify rotation
    print("\n[5] Verifying rotation (re-retrieving from vault)...")
    verified = retrieve_secret(CONJUR_URL, CONJUR_ACCOUNT, token, TARGET_VARIABLE)
    if verified == new_password:
        print("    VERIFIED - Rotation successful")
    else:
        print("    ERROR - Verification failed!")
        sys.exit(1)

    print("\n" + "=" * 60)
    print("ROTATION COMPLETE")
    print(f"  Account:    {TARGET_VARIABLE}")
    print(f"  Old (mask): {old_password[:4]}...")
    print(f"  New (mask): {new_password[:4]}...")
    print(f"  Timestamp:  {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print("=" * 60)
    print("\nINTERVIEW NOTE: In production CyberArk, the CPM also changes")
    print("the password ON the target system (e.g., Active Directory),")
    print("then reconciles by verifying the change succeeded.")


if __name__ == "__main__":
    main()

What does the rotation workflow do?

The main function follows the same five-step process that a real CPM uses:

  • Authenticate to the vault to prove its identity.
  • Retrieve the current password so it can be compared after rotation.
  • Generate a new strong password meeting complexity requirements.
  • Push the new password into the vault, replacing the old one.
  • Verify the rotation by re-reading the vault and confirming the value matches.

The script masks passwords in the output (showing only the first 4 characters) so that credentials are never fully exposed in logs.

  • Save cpm_rotator.py.

File not saving?

  • Make sure the file is saved directly inside C:\pam-lab, not inside the conjur-quickstart subfolder.
  • Check that your filename is exactly cpm_rotator.py with no extra spaces or wrong extension.

help me fix my cpm_rotator.py file

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

"""CPM Rotator - Simulates CyberArk Central Policy Manager (CPM)

This script demonstrates automated password rotation:
1. Authenticate to the vault
2. Generate a new strong password
3. Update the credential in the vault
4. Verify the rotation succeeded

Interview talking point: "The CPM automatically rotates passwords based
on the Master Policy schedule, ensuring credentials never become stale
and reducing the window of exposure if a password is compromised."
"""

import os
import sys
import base64
import secrets
import string
import urllib.parse
from datetime import datetime
import requests

# Configuration
CONJUR_URL = "http://localhost:8080"
CONJUR_ACCOUNT = "myConjurAccount"
CONJUR_LOGIN = "admin"

API_KEY = os.environ.get("CONJUR_API_KEY")

if not API_KEY:
    print("ERROR: Set the CONJUR_API_KEY environment variable first.")
    print("  PowerShell: $env:CONJUR_API_KEY = 'your-key-here'")
    sys.exit(1)

# Target account to rotate (simulates CPM managing an account)
TARGET_VARIABLE = "prod-servers/linux-root-password"


def authenticate(conjur_url, account, login, api_key):
    """Authenticate to Conjur and get an access token."""
    login_encoded = urllib.parse.quote(login, safe="")
    url = f"{conjur_url}/authn/{account}/{login_encoded}/authenticate"

    response = requests.post(url, data=api_key, verify=False)
    response.raise_for_status()

    token_b64 = base64.b64encode(response.content).decode("utf-8")
    return token_b64


def retrieve_secret(conjur_url, account, token_b64, variable_id):
    """Retrieve current secret value."""
    variable_encoded = urllib.parse.quote(variable_id, safe="")
    url = f"{conjur_url}/secrets/{account}/variable/{variable_encoded}"

    headers = {"Authorization": f'Token token="{token_b64}"'}
    response = requests.get(url, headers=headers, verify=False)
    response.raise_for_status()

    return response.text


def update_secret(conjur_url, account, token_b64, variable_id, new_value):
    """
    Update a secret in the vault with a new value.
    Maps to: CPM pushing the new rotated password back into the vault.
    """
    variable_encoded = urllib.parse.quote(variable_id, safe="")
    url = f"{conjur_url}/secrets/{account}/variable/{variable_encoded}"

    headers = {"Authorization": f'Token token="{token_b64}"'}
    response = requests.post(url, data=new_value, headers=headers, verify=False)
    response.raise_for_status()


def generate_password(length=20):
    """
    Generate a strong random password.
    Maps to: CPM password generation per the Master Policy complexity rules.
    """
    alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
    password = "".join(secrets.choice(alphabet) for _ in range(length))
    return password


def main():
    print("=" * 60)
    print("CPM ROTATOR - CyberArk Central Policy Manager Sim")
    print("=" * 60)
    print(f"\nTimestamp:  {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"Target:    {TARGET_VARIABLE}")
    print()

    # Step 1: Authenticate
    print("[1] Authenticating as CPM service account...")
    token = authenticate(CONJUR_URL, CONJUR_ACCOUNT, CONJUR_LOGIN, API_KEY)
    print("    SUCCESS - Authenticated")

    # Step 2: Retrieve current password (for audit/comparison)
    print("\n[2] Retrieving current password...")
    old_password = retrieve_secret(CONJUR_URL, CONJUR_ACCOUNT, token, TARGET_VARIABLE)
    print(f"    Current: {old_password[:4]}{'*' * (len(old_password) - 4)}")

    # Step 3: Generate new password
    print("\n[3] Generating new password (Master Policy: 20 chars, complex)...")
    new_password = generate_password(20)
    print(f"    New:     {new_password[:4]}{'*' * 16}")

    # Step 4: Update the vault
    print("\n[4] Pushing new password to vault...")
    update_secret(CONJUR_URL, CONJUR_ACCOUNT, token, TARGET_VARIABLE, new_password)
    print("    SUCCESS - Vault updated")

    # Step 5: Verify rotation
    print("\n[5] Verifying rotation (re-retrieving from vault)...")
    verified = retrieve_secret(CONJUR_URL, CONJUR_ACCOUNT, token, TARGET_VARIABLE)
    if verified == new_password:
        print("    VERIFIED - Rotation successful")
    else:
        print("    ERROR - Verification failed!")
        sys.exit(1)

    print("\n" + "=" * 60)
    print("ROTATION COMPLETE")
    print(f"  Account:    {TARGET_VARIABLE}")
    print(f"  Old (mask): {old_password[:4]}...")
    print(f"  New (mask): {new_password[:4]}...")
    print(f"  Timestamp:  {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print("=" * 60)
    print("\nINTERVIEW NOTE: In production CyberArk, the CPM also changes")
    print("the password ON the target system (e.g., Active Directory),")
    print("then reconciles by verifying the change succeeded.")


if __name__ == "__main__":
    main()
Run the rotation and verify

Now you will execute the rotation script and then confirm the password actually changed in the vault by re-running your CCP retriever.

  • Run the CPM rotation script in your PowerShell window (the one with your virtual environment activated):
python cpm_rotator.py

What should I see?

The script prints a five-step progress log. You should see SUCCESS - Authenticated, then the current password masked with asterisks, a newly generated password (also masked), SUCCESS - Vault updated, and finally VERIFIED - Rotation successful.

The ROTATION COMPLETE summary at the bottom shows the masked old and new values with a timestamp.

  • Verify the password actually changed in the vault by running your CCP retriever again:
python ccp_retriever.py
  • Compare the prod-servers/linux-root-password value in the retriever output to what you saw before. It should be the new 20-character rotated password, not the original R00t!Initial#2026.

Why verify with a separate script?

This two-script verification mirrors production CyberArk workflows. The CPM rotates the password, and a separate application (using the CCP) retrieves it. If the retriever shows the new value, it proves the vault is the single source of truth and both components work together.

🙋‍♀️ Seeing a connection error?

  • Make sure your Docker containers are still running. Switch to the PowerShell window running Docker and run docker ps to confirm.
  • If the containers stopped, navigate to C:\pam-lab\conjur-quickstart and run docker compose up -d again.
  • Confirm your CONJUR_API_KEY environment variable is still set in this PowerShell session. If you opened a new window, you need to re-set it.

help me debug my CPM rotation script connection error

Leave these containers running

Part 2 of this lab adds an Apache Guacamole session proxy as a separate Docker Compose stack that runs alongside this one. Leave your Conjur containers up so both stacks coexist without rebuilding.

If you close your machine and the containers stop, you can restart them later by opening PowerShell, navigating to C:\pam-lab\conjur-quickstart, setting $env:CONJUR_DATA_KEY again, and running docker compose up -d.

Part 1 is complete. Your vault is running on port 8080, storing and rotating privileged credentials through Python scripts that mirror real CyberArk CCP and CPM workflows.

Secret mission

Add Multi-Account Rotation with Audit Logging

Your CPM rotator currently handles a single account. In production, CyberArk's CPM manages hundreds of privileged accounts and logs every rotation event for auditors. Extend your rotator to loop through multiple accounts and produce a timestamped audit trail.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally in Docker, so there are no ongoing cloud costs.

Resources you used:

  • Docker containers (conjur_server, postgres_database, nginx_proxy, conjur_client) running the Conjur vault.
  • Project folder at C:\pam-lab containing Python scripts, cloned repo, virtual environment, and log files.
  • Docker Desktop installed on your system.
  • Python 3.14 installed on your system.
  • VS Code installed on your system.

Keep everything running

No action needed. Choose this if you are still actively building on this lab or want to revisit it soon.

  • Your Docker containers will continue running and using system memory until you stop them or restart your computer.
  • Docker Desktop uses approximately 2-4 GB of RAM while containers are running.
  • Your Python scripts and interview guide remain in C:\pam-lab ready to run again.

Pause - I'll come back to this later

Shut down the Docker containers to free up memory while keeping all your files intact for later.

  • Stop the Conjur containers by running this in PowerShell:
cd C:\pam-lab\conjur-quickstart
docker compose down
  • This stops and removes the containers but preserves all your project files and Docker images.
  • To restart later, set the data key environment variable and bring the containers back up:
cd C:\pam-lab\conjur-quickstart
$env:CONJUR_DATA_KEY = Get-Content .\data_key -Raw
docker compose up -d
  • You will need to re-create the admin account and reload your policy after restarting, since the database volume is not persisted with this command.

Delete - I don't want to use this again

Remove all project resources completely and reclaim disk space.

  • Stop all containers and remove their volumes by running this in PowerShell:
cd C:\pam-lab\conjur-quickstart
docker compose down -v
  • Remove the downloaded Docker images to free disk space:
docker rmi cyberark/conjur cyberark/conjur-cli:9 postgres:15 nginx:latest cfmanteiga/alpine-bash-curl-jq
  • Delete the entire project folder:
cd C:\
rmdir /s /q C:\pam-lab
  • To uninstall Docker Desktop, open Settings, select Apps, then Installed apps. Find Docker Desktop and click Uninstall.
  • To uninstall Python, open Settings, select Apps, then Installed apps. Find Python 3.14 and click Uninstall.
  • To uninstall VS Code, open Settings, select Apps, then Installed apps. Find Microsoft Visual Studio Code and click Uninstall.

Nice Work!

Nice Work!

You just built a fully functional Privileged Access Management lab from scratch. You now have hands-on experience with the same secrets vaulting, credential retrieval, and automated rotation patterns that CyberArk Privileged Access Manager uses in production.

You've learned how to:

  • Deploy a Conjur OSS vault in Docker with YAML policies defining Safes, privileged accounts, and RBAC permissions.
  • Build a Python CCP simulator that authenticates to the vault and retrieves secrets via REST API.
  • Build a Python CPM simulator that generates strong passwords and rotates credentials automatically.
  • Secret Mission: Extended the CPM with multi-account rotation and timestamped audit logging for interview demonstration.

Ready to quiz yourself?