A working PAM environment built from free components: a credential vault that stores and rotates privileged accounts under a policy, and a session proxy that brokers and records every SSH and RDP session to two isolated target machines.
An organization has servers that a handful of people need administrative access to. That creates two separate problems, and most teams only solve the first one.
Problem one is the credential. The root password is in a password manager, a runbook, a config file, and three people's heads. Nobody knows when it last changed. When an admin leaves, the honest answer to "what did they have access to" is a spreadsheet somebody stopped updating in 2023. An application needs that same password, so it sits in a connection string in source control.
Problem two is the session. Even if the credential is controlled, once an admin is on the box there is no record of what they did. When something breaks at 3 a.m. and the change log is empty, the investigation starts with asking people what they remember.
Auditors ask about both. SOX, PCI DSS, HIPAA, and cyber insurance renewals all want the same two things: proof that privileged credentials are controlled and rotated, and proof that privileged sessions are recorded and attributable.
This lab builds both halves and produces evidence for each.
flowchart LR
subgraph host["Windows host"]
admin["Operator<br/>(browser)"]
end
subgraph vault["Vault stack :8080"]
nginx["nginx<br/>TLS proxy"]
conjur["Conjur OSS<br/>Digital Vault"]
pg1[("PostgreSQL<br/>encrypted store")]
nginx --> conjur --> pg1
end
subgraph scripts["Python components"]
ccp["ccp_retriever.py<br/>reads only"]
cpm["cpm_rotator.py<br/>reads and rotates"]
end
subgraph proxy["Proxy stack :8081"]
guac["Guacamole<br/>web app"]
guacd["guacd<br/>session daemon"]
pg2[("PostgreSQL<br/>connections, history")]
rec[/"recordings<br/>volume"/]
guac --> guacd
guac --> pg2
guacd -->|writes| rec
rec -->|read only| guac
end
subgraph targets["Isolated network 192.168.148.0/24 (no gateway)"]
linux["LabLinux<br/>192.168.148.10<br/>SSH"]
win["LabWindows<br/>192.168.148.20<br/>RDP"]
end
admin --> guac
guacd --> linux
guacd --> win
ccp --> conjur
cpm --> conjur
| Lab component | CyberArk equivalent | Role |
|---|---|---|
| Conjur OSS | Digital Vault | Encrypted credential storage |
pam-policy.yml |
Safe and Safe permissions | Access control as code |
| Conjur variable | Privileged account | A stored credential |
| Conjur host | Application or machine identity | Non-human account |
ccp_retriever.py |
Central Credential Provider | Runtime credential retrieval |
cpm_rotator.py |
Central Policy Manager | Automated rotation |
| Apache Guacamole | Privileged Session Manager | Session brokering and recording |
These are analogies, not equivalences. See Honest limits.
Access control is code, not a checkbox. pam-policy.yml defines two machine identities against
the same two credentials. cpm-service gets read, execute, update. app-server gets
read, execute. One word of difference.
The boundary holds under test. Running the rotation script as app-server authenticates
successfully, reads the current credential successfully, and is refused at the write. The boundary
is not access, it is the update privilege, and it is evaluated at the moment of the call rather
than at login.
Rotation propagates without coordination. The rotator generates a new 20-character credential and writes it to the vault. The retriever, a separate process authenticating on its own, returns the new value. Nothing told it the password changed.
The targets cannot reach the internet. Not "traffic is blocked." There is no default route at all, so the kernel has nowhere to send a packet destined off-subnet.
Every session is recorded and attributable. Each session writes to its own directory named for its history UUID, which is how a recording ties back to a specific user, connection, and timestamp. Playback shows the session alongside a timestamped keystroke log and an activity histogram.
policy/pam-policy.yml Access control policy, the Safe defined as code
scripts/ccp_retriever.py Runtime credential retrieval
scripts/cpm_rotator.py Credential rotation
docker/guacamole/docker-compose.yml Session proxy stack
RUNBOOK.md Full build, start to finish
BUILD-LOG.md What broke and what fixed it
HOW-I-USED-AI.md Where AI was used and where it was not
docs/images/ Evidence captures
docs/media/ Session playback
RUNBOOK.md is the complete build. Every command, every capture, and every place the
source material was wrong for this hardware.
BUILD-LOG.md is the failures. A hypervisor conflict inherited from an earlier
project, a data key broken by Windows line endings, a schema silently corrupted by PowerShell
redirection, and recordings that wrote perfectly while the web app was locked out of its own
directory.
Every one of these is something you would not do in production. They are listed because a lab that hides its shortcuts is not evidence of anything.
- The two halves are not integrated. Guacamole stores target credentials in its own database rather than pulling them from Conjur at connect time. This is the most significant gap between this lab and a real PAM deployment, and it is the obvious v2.
guacadmin/guacadminleft as the default admin.- Database passwords in plaintext in
docker-compose.yml. Ignore server certificateon the RDP connection.- Both Python scripts talk plain HTTP to
localhost:8080, bypassing the nginx TLS proxy. - Recording directories made world-readable with
chmod o+rX. The correct fix is a shared group between the two containers. - Single-node everything. No HA, no replication, no backup, no HSM. A real Digital Vault is a hardened appliance; this is one container.
- The rotator changes the vault value only. A real CPM also changes the account on the target system through a plugin framework. That framework is most of what CyberArk sells.
CyberArk PAS binaries are behind a customer portal. This is not CyberArk. It is the same architecture built from components that behave the same way mechanically, which is a different and smaller claim.
Full instructions in RUNBOOK.md. Requirements: Docker Desktop, VirtualBox 7,
Python 3.11+, roughly 40 GB of disk, and a machine that can run two VMs alongside Docker.
Note that Docker Desktop and VirtualBox both want the hypervisor layer. Phase 0 of the runbook measures whether they coexist on your hardware before anything gets built, because finding out later means rebuilding.
Built following NextWork lab material, with divergences documented in the runbook where the source was wrong for this hardware or omitted a check worth keeping.



