Hands on lab work at the meeting point of privileged access management and cloud infrastructure. Each lab stands up a real environment, runs real tooling against it, and documents what actually happened, including the parts that broke.
Stack: CyberArk/Idira, Linux (RHEL 9), AWS, Azure
Building infrastructure is the easy half. These are the defects that only appeared once the environments were stood up and the deployed objects were read back from the API rather than trusted from the config.
A machine role that no machine could ever assume. The terraform-aws-modules/iam module defaults role_requires_mfa to true. The DB broker is assumed by a service, and a service cannot present MFA, so the role deployed looking perfect, passed terraform validate, passed Checkov, and would have failed the first time anything tried to use it. Visible only by reading the deployed trust policy. — lab 06
A CloudTrail bucket policy that trusted every AWS account. Neither statement carried aws:SourceArn. The principal is the CloudTrail service, which is not scoped to an account, so the policy trusted CloudTrail globally rather than this account's trail — the cross-account confused-deputy pattern AWS has documented since 2022. Encryption and public access were both fine; the trust boundary was the thing nobody was checking. — lab 09
A Key Vault hardened against access that recorded nobody who accessed it. Soft delete, purge protection, network ACLs, RBAC — all correct. No diagnostic setting, so no AuditEvent. A vault nobody can reach is half a control; the other half is knowing who reached it. — lab 10
An RBAC linter that reported a clean cluster. Run against a live kind cluster holding four deliberately cluster-admin-equivalent roles, it printed No risky RBAC found and exited 0. kubectl -o json wraps everything in a single kind: List document, and the top-level kind check returned nothing. Every unit test passed the entire time, because every unit test fed it a hand-written manifest — the one shape it never meets in production. — lab 08
Three credentials in plaintext in terraform.tfstate. Minimal config, no AWS provider, nothing referencing the values. They landed in state anyway, including the full 40-character secret key. The lab's central claim, measured rather than asserted. — lab 01
Each one is written up in that lab's LAB-NOTES.md with the error output, the cause, and the fix.
Every lab has been run, not just built. Each one has a findings/ folder recording what actually happened, and the Verification column below says what was executed and against what.
Most ran without any cloud account: Docker for Conjur and Kubernetes, LocalStack for AWS IAM and S3, a Rocky container for OpenSCAP, Microsoft's Kusto container for KQL, and a stub tenant for psPAS. Where a claim genuinely needs a real subscription, that is stated in the lab rather than glossed.
| # | Lab | What it proves | Verification |
|---|---|---|---|
| 01 | Conjur secrets into a Terraform/AWS pipeline | No credentials on disk or in state. Summon vs the data source leak, measured | Docker run, 4 bugs fixed, credential leak measured |
| 02 | Shadow admin discovery across AWS and Azure | Escalation path analysis, scored against ground truth | 9 tests; paths planted on LocalStack, ground truth verified |
| 03 | Self building hardened RHEL 9 lab | STIG remediation as code, measured before and after | 8 tests; real OpenSCAP scan, remediate, rescan |
| 04 | Privileged access detections for Microsoft Sentinel | PAM knowledge as working detection logic | 24 tests; all 4 detections executed against Kusto |
| 05 | AWS Secrets Manager as a PAM control plane | Rotation, three layer access, honest CyberArk comparison | Applied on LocalStack, CMK and retrieval verified |
| 06 | IAM least privilege and break glass roles | Minimal roles proven with Access Analyzer and Checkov | Applied on LocalStack, controls read back from IAM |
| 07 | IaC security gate | Blocks secrets and misconfig. Self test proves bad fails, good passes | 3 scanners run for real, bad fails, good passes |
| 08 | Conjur secrets on Kubernetes plus KubiScan | An offline RBAC linter, run against a real cluster. KubiScan cross-check still pending | 15 tests; real kind cluster, 15 RBAC findings |
| 09 | AWS multi account security baseline | A baseline that passes its own audit. Prowler scoring needs a real account | 14 tests; trail bucket applied and audited |
| 10 | Azure landing zone guardrails | Guardrail definitions unit tested, Bicep verified offline. Deny path needs Azure | 15 tests; Bicep verified with PSRule, 36 rules |
| 11 | CyberArk/Idira PAM automation with psPAS | Account lifecycle plus drift reconciliation as idempotent runbooks | 29 tests; reconciler run against a stub tenant |
| 12 | Network isolation as an access control | The layer under IAM: can this host even reach the endpoint | 15 tests; verified on real AWS, negative control run live |
| 13 | Highly available application platform | Two AZs, self healing, and Terraform state a team can share | 12 tests; broke it on real AWS, 11 of 12 requests survived |
| 14 | Reusable secure VPC Terraform module | The secure network is the default; the insecure one costs you an argument | 9 tests, no AWS account needed; each one proven by a deliberate break |
Eight times while building these labs, a tool reported success while reading nothing: gitleaks after a version bump, checkov twice on nested ARM, the RBAC linter on live kubectl output, PSRule twice, a pytest skip guard declared where pytest never looks, and lab 14's own test suite reporting 9 passed in an environment where eight of them had silently skipped. None announced itself.
Two more of the same shape appeared in the checks written for those bugs, which is the part worth sitting with.
So every check here has to count findings rather than trust an exit code, and has to have been observed failing before it counts as evidence. POSITIVE-CONTROLS.md lists each one and exactly what breaks it.
See REPO-COVERAGE.md for which upstream tool each lab depends on and under what license.
On status: every lab has run and every lab has findings/. CI is green across all 11.
What each lab still cannot prove is stated in its own README and notes rather than averaged away here. The largest remaining gap is shared rather than per-lab: LocalStack creates IAM objects faithfully and evaluates no policy at request time, so every "this control actually blocks the request" claim across labs 02, 05, 06 and 09 is configured-and-verified, not observed-blocking. One free-tier AWS account closes all four. Labs 05, 09 and 10 additionally need a real subscription for CloudTrail data events, GuardDuty and Azure Policy deny.
The running the labs did produce found real bugs, including several in the labs' own tooling: an RBAC linter that reported a clean cluster while four cluster-admin-equivalent roles sat in it, a Key Vault hardened against access but keeping no record of who accessed it, a CloudTrail bucket policy trusting the CloudTrail service globally rather than its own trail, and a module default that silently required MFA on a machine role that can never present it.
Lab 03 is the Linux one and it is not a Docker exercise. OpenSCAP against the DISA STIG profile for RHEL 9, scan then oscap --remediate then rescan, with the delta computed by a scorer in scripts/.
Baseline : 62 / 71 passed (87.3%)
Hardened : 68 / 71 passed (95.8%)
Delta : +6 controls (+8.5 pp)
The interesting part is the denominator. Of the profile's 1532 rules, 412 came back notapplicable because that run was in a container: no kernel of its own, no bootloader, no GRUB, no auditd, no physical console. So the honest reading is +8.5 points on the 71-rule userspace subset, not a STIG compliance figure — which is exactly why the scorer excludes skips from the denominator. Counting them would have reported roughly 4% and looked catastrophic instead of narrow.
Also confirmed a distro trap the playbook had wrong: the datastream on Rocky is ssg-rl9-ds.xml, not ssg-almalinux9-ds.xml. Wrong path means zero rules evaluated, which presents as a clean pass.
Every lab follows the same shape, and the shape is the point:
lab-name/
README.md what it is, how to run it, what it proves
LAB-NOTES.md the running log: errors, dead ends, fixes
tests/ offline tests for the core logic (most labs)
terraform/ or ansible, bicep, policy: the environment as code
scripts/ bootstrap, teardown, and the scoring logic
findings/ output, analysis, screenshots (from the live run)
Two things make these more than a folder of Terraform.
The tested core. Wherever a lab has real logic (a SkyArk scorer, a SCAP delta, an RBAC linter, a drift reconciler, a policy validator), that logic is unit tested offline and runs in CI. If the number a lab reports is wrong, a test goes red. That is the difference between "I ran a tool" and "I built the thing that checks the tool."
The lab notes. A dated log of "this failed, here is the error, here is why, here is the fix" reads as real experience because it is. Several of these labs found genuine bugs during the build (a Terraform dependency cycle, a policy that would have protected nothing, a security gate that failed its own scan), and the notes record them.
These labs build on open source tooling. The environments, test methods, analysis, and findings are mine. The upstream tools are not, and each lab's README credits them. Nothing upstream is copied into these repos.
On the name: Palo Alto Networks acquired CyberArk and rebranded the platform Idira. Repos and docs are mid migration. Labs here use whichever name the tooling itself uses at the pinned version.