<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Tim Schindler's Blog]]></title><description><![CDATA[A CyberArk Community Most Valuable Professional (MVP), CyberArk Guardian, CDE-PAM, CDE-CPC, CDE-CLOUD-SEC, CDE-ACCESS, CDE-EPM, and CDE-CERT-SAAS.]]></description><link>https://timschindler.blog</link><generator>RSS for Node</generator><lastBuildDate>Fri, 14 Aug 2026 00:58:28 GMT</lastBuildDate><atom:link href="https://timschindler.blog/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building Idira (CyberArk) ISP  PowerShell tooling with Claude Code, Context7 MCP, and SecretStore]]></title><description><![CDATA[Coding agents write competent PowerShell against the CyberArk (now Idira) REST APIs but often fall short with Idira endpoints. Left to its own devices, the model will invent a token endpoint, treat th]]></description><link>https://timschindler.blog/building-idira-cyberark-isp-powershell-tooling-with-claude-code-context7-mcp-and-secretstore</link><guid isPermaLink="true">https://timschindler.blog/building-idira-cyberark-isp-powershell-tooling-with-claude-code-context7-mcp-and-secretstore</guid><category><![CDATA[cyberark]]></category><category><![CDATA[Idira]]></category><category><![CDATA[REST API]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[agentic-coding]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Model Context Protocol]]></category><category><![CDATA[Powershell]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Wed, 01 Jul 2026 07:37:40 GMT</pubDate><content:encoded><![CDATA[<p>Coding agents write competent PowerShell against the CyberArk (now <a href="https://www.paloaltonetworks.com/idira">Idira</a>) REST APIs but often fall short with Idira endpoints. Left to its own devices, the model will invent a token endpoint, treat the Identity tenant ID and the ISP subdomain as one string, and hardcode a client secret into the script the model just produced. Those are context failures, and no prompt fixes the gap one message at a time.</p>
<p>So we give the model the context up front. We will build one small PowerShell tool that authenticates to the Idira Identity Security Platform (ISP) and proves a token works, and the tool itself is almost incidental. What earns the reliability is the work we do around it: we feed the model the real API documentation with <a href="https://context7.com">Context7</a>, we write the platform's quirks into a <a href="https://code.claude.com/docs/en/memory"><code>CLAUDE.md</code></a> so the model stops guessing them, we give it the tenant's non-secret details as config, then we keep the service account secret where the model never gets the cleartext. The harness here is Claude Code, but the technique is about agentic coding in general, so the pieces port to whatever harness you run.</p>
<h2>Why letting the model guess is the wrong default</h2>
<p>The failure modes here are not subtle, but each one looks correct until something returns a 404 or a malformed request error. The model derives the Idira service host from the login domain, because for most SaaS products the two match.</p>
<p>For Idira ISP they do not. The model also typically reaches for the PAM self-hosted logon flow when the tenant is really ISP. PAM self-hosted's logon API endpoint is hosted by the PVWA, while ISP gets a platform token from Idira Identity with a client-credentials grant, so a script that worked against a PAM self-hosted instance fails when authenticating against an ISP tenant. The model writes the secret straight into the script, or into a <code>.env</code> that the model then reads back, which in an agentic loop puts the secret into the model's context, or the model's logs.</p>
<p>We will not fix any of this one prompt at a time. The way out is to make the right information cheap for the model to pull, and to keep the dangerous information out of the files and context the model touches -- and, where that is not enough, behind a hard stop it cannot cross.</p>
<h2>Feeding the model real docs with Context7</h2>
<p><a href="https://context7.com">Context7</a> is an up-to-date documentation source for coding agents: give it a library name and it returns that library's current docs as snippets the model can read on demand. It plugs into a harness as an MCP (<a href="https://modelcontextprotocol.io/docs/getting-started/intro">Model Context Protocol</a>) server or as a Skill; here we call it as an MCP server. For us the value is simple: the model stops guessing the ISP auth flow and reads the flow straight from the source documentation. Pointed at the Idira API docs, the library resolves cleanly:</p>
<pre><code class="language-text">Context7:resolve-library-id  "api-docs.cyberark.com"
 -&gt; /websites/api-docs_cyberark   (CyberArk API, 80 snippets, source reputation: High)
</code></pre>
<p>When we query that library for the <code>platformtoken</code> flow, we get the actual endpoint contract rather than a plausible reconstruction:</p>
<pre><code class="language-text">POST https://&lt;identity-tenant-id&gt;.id.cyberark.cloud/oauth2/platformtoken
content-type: application/x-www-form-urlencoded

grant_type=client_credentials
client_id=&lt;service-account login name&gt;
client_secret=&lt;service-account password&gt;

200 OK
{ "access_token": "eyJh...", "token_type": "Bearer", "expires_in": 900 }
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/61cf031d7cb96c3f9225f9db/7b607c1a-c078-4359-b59f-7631a1216de1.png" alt="" style="display:block;margin:0 auto" />

<p><em>The model pulling the platformtoken contract from Context7's</em> <code>/websites/api-docs_cyberark</code> <em>library rather than generating it.</em></p>
<p>The request body is an ordinary OAuth 2.0 client-credentials grant, the same call we would make with cURL. The only parts specific to ISP are the host we send the grant to and where the token works afterwards, and those are the two details the model gets wrong from memory. Pulling them from <a href="https://api-docs.cyberark.com/create-api-token/docs/create-api-token">api-docs.cyberark.com</a> removes the guesswork.</p>
<p>Context7 helps beyond the language we happen to be writing in. It also indexes the libraries next to our task:</p>
<pre><code class="language-text">/pspete/pspas               psPAS               (2305 snippets, High)
/pspete/identitycommand     IdentityCommand     (75 snippets,   High)
/cyberark/idsec-sdk-golang  CyberArk's official Go SDK
</code></pre>
<p>We write PowerShell here, but the model can learn from more than PowerShell, because the <code>platformtoken</code> call is the same HTTP whether we express the call in Go, in PowerShell, or as a cURL example sitting in the documentation.</p>
<p>The cleanest reference implementation of the ISP authenticator lives in the <a href="https://github.com/cyberark/idsec-sdk-golang"><code>idsec-sdk-golang</code></a> <code>NewIdsecISPAuth</code> flow, so we let the model read how that SDK models authentication and translate the pattern into the <code>Invoke-RestMethod</code> call we want. <a href="https://github.com/pspete/psPAS">psPAS</a> and <a href="https://github.com/pspete/IdentityCommand">IdentityCommand</a> serve the same purpose closer to home: proven patterns the model can lift instead of improvise.</p>
<p>None of this means we should pipe every library through Context7, since coverage varies and what comes back is only ever as good as the underlying documentation. We still give the model the real reference instead of hoping the model remembers, and Context7 makes that cheap.</p>
<h2>Encoding the idiosyncrasies in CLAUDE.md</h2>
<p>Context7 gives the model the facts. <code>CLAUDE.md</code> gives the model the rules that hold for this platform and this tenant, the ones no generic documentation will surface, because they are the assumptions a careful engineer would otherwise make and get wrong. Claude Code <a href="https://code.claude.com/docs/en/memory">reads <code>CLAUDE.md</code> from the project root at the start of every session</a>; this is an agentic-coding technique rather than a Claude Code one, so other harnesses read an equivalent file -- Codex and several others look for <code>AGENTS.md</code> -- and the rules port even where the filename does not.</p>
<p>The most valuable rule we can write is the one about hosts. The Identity tenant ID is not an Idira ISP's subdomain. The portal might be <code>acme.cyberark.cloud</code> and the Idira ISP's APIs <code>acme.dpa.cyberark.cloud</code>, both built from the same subdomain, but the Identity host that serves the token endpoint is something like <code>abc1234.id.cyberark.cloud</code>, built from a different identifier entirely. The model assumes one identifier covers all three hosts, and because the token host uses its own, deriving it from the subdomain produces a DNS failure long before we ever reach the credentials.</p>
<p>Here is the <code>CLAUDE.md</code> we hand the model:</p>
<pre><code class="language-markdown"># Idira (formerly CyberArk) ISP — project rules

## Branding
- CyberArk is now Idira. URLs and APIs are UNCHANGED. Do not rename `*.cyberark.cloud`, `api-docs.cyberark.com`, or the `cyberark/*` SDK repos.

## Hosts — two identifiers, three hostnames. Never derive one from another.
- IDENTITY_TENANT_ID and SUBDOMAIN are non-secret config; read them from .env. They are not secrets: do not hardcode them and do not put them in the vault.
- IDENTITY_TENANT_ID  -&gt; token endpoint host: &lt;IDENTITY_TENANT_ID&gt;.id.cyberark.cloud
- SUBDOMAIN           -&gt; service API hosts:   &lt;SUBDOMAIN&gt;.&lt;service&gt;.cyberark.cloud
- DO NOT assume IDENTITY_TENANT_ID == SUBDOMAIN. **They are ALWAYS DIFFERENT.** Treat both as required inputs; never infer the service host from the login domain.

## Authentication — client_credentials, service user
- This is an ISP (SaaS) tenant. Authenticate with the OAuth2 client_credentials grant. Pull the exact token endpoint, re quest body, and response shape from the api-docs.cyberark.com reference through Context7; do not write them from memory.
- The credential is a SERVICE USER (an OAuth confidential client): its login name is the client_id and its password is the client_secret. A regular, interactive Cloud Directory user authenticates through a different CyberArk Identity flow, so do not use that flow here.
- Tokens are short-lived. Re-authenticate on expiry; never cache a token for the life of a long-running job.

## API endpoints — read them from Context7, never from memory
- Look up EVERY Idira API endpoint you call (path, method, query string, and request/response shape) from api-docs.cyberark.com through Context7 — not just the token endpoint, every service API too.
- api-docs.cyberark.com does not cover every Idira service. When an endpoint is not there, pull it from another Context7 library that does — `psPAS`, `IdentityCommand`, or the `idsec-sdk-golang` SDK — instead of guessing it.
- The docs are not infallible: what Context7 returns can still be wrong or incomplete. Use the retrieved form, but flag any endpoint you cannot confirm for live verification against the tenant -- never commit an unverified path silently.

## Do NOT cross the streams
- DO NOT use the self-hosted Vault flow `/PasswordVault/API/auth/CyberArk/Logon`. This is a SaaS (ISP) tenant; the two flows are not interchangeable and will fail against the wrong base URL.

## Secrets
- The service-account client_id/secret come from the SecretManagement vault at runtime via Get-Secret. NEVER hardcode them, write them to a file, echo them, or commit them. Do not read the SecretStore vault file.
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/61cf031d7cb96c3f9225f9db/01084811-d2c2-4d51-9887-5929da4010cb.png" alt="" style="display:block;margin:0 auto" />

<p><em>The host rules and the ISP-vs-self-hosted guardrail are the two the model gets wrong most often.</em></p>
<p>Every block in there exists because I have watched a model make that mistake. The branding note stops the model from helpfully renaming working URLs, and the host rules are there because the Identity-tenant-ID-equals-Idira-subdomain assumption is the single most common way these scripts fail. The endpoints rule is there for the same reason: a model will otherwise reach for a plausible API path from memory and get it subtly wrong, and a wrong path only fails when the script runs, so every endpoint has to come from the docs through Context7 instead of from recall. The cross-the-streams rule stops the model from authenticating an ISP tenant as though the tenant were a PAM self-hosted deployment.</p>
<p>One caveat matters here: the official docs are careful to say that Claude Code treats <code>CLAUDE.md</code> as <a href="https://code.claude.com/docs/en/memory">context, not enforced configuration</a>, which means every rule in the file guides the model without binding it. The host rules make the model far less likely to conflate the two identifiers, but the rules are guidance, not a guarantee, and the same holds for the "do not read the vault file" line.</p>
<p>When we need a hard stop instead of a strong nudge, and for a credential store we should, the docs point to a <code>PreToolUse</code> hook that denies the matching tool call in code rather than by persuasion. A hook is only as tight as the patterns you give it: match every path to the credential, or the gap you leave is the bypass. Hooks are Claude Code's enforcement mechanism; other harnesses expose their own, or none, in which case the boundary has to live outside the harness. <code>CLAUDE.md</code> shapes the model's behavior, and a hook enforces the limit in code.</p>
<h2>Keeping the service-account secret out of the model's reach</h2>
<p>The credential we are protecting is an Idira Cloud Directory service user -- Identity's name for a service account -- configured as an OAuth confidential client. We create the service user in the Identity Administration portal, tick <strong>Is OAuth confidential client</strong>, and grant the least privilege the integration needs. The <a href="https://docs.cyberark.com/identity-administration/latest/en/content/ispss/ispss-add-service-user.htm">Add service users docs</a> walk through the steps, after which the service user's login name becomes the <code>client_id</code> and the password becomes the <code>client_secret</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61cf031d7cb96c3f9225f9db/7bc95e50-3e80-4dc1-a96e-989b462215e3.png" alt="" style="display:block;margin:0 auto" />

<p><em>The service user configured as an OAuth confidential client.</em></p>
<p>A <code>.env</code> file does not protect the secret here, and the reason is specific to agentic coding rather than plain hygiene: the model can read the file. Claude Code opens files and writes code that prints whatever the code loaded. A plaintext secret then sits squarely inside the model's reach.</p>
<blockquote>
<p>💡 The risk is not that the secret leaks to git, since we already gitignore <code>.env</code>. The risk is the model itself reading, printing, or committing the secret while doing exactly what we asked, landing the cleartext in its context or a transcript. So we keep the secret out of any file the model has reason to open, and we lean on the harness permission controls so the model will not read the credential store or run the authenticated command in a shell whose environment the model then dumps.</p>
</blockquote>
<p>I use one of two options here, depending on what the project already runs.</p>
<p>The first-party choice is PowerShell's own <a href="https://learn.microsoft.com/en-us/powershell/utility-modules/secretmanagement/overview">SecretManagement with the SecretStore extension</a> (two Microsoft modules, no third-party service). SecretStore keeps secrets in a local file that is <a href="https://learn.microsoft.com/en-us/powershell/utility-modules/secretmanagement/get-started/using-secretstore">encrypted with .NET cryptographic APIs and unlocked by a vault password</a>. The model can see the vault file, but the file is ciphertext, and the model never holds the password that unlocks the vault, which is the whole reason this arrangement survives an agent with read access to the project. We set this up once:</p>
<pre><code class="language-csharp">Install-Module Microsoft.PowerShell.SecretManagement -Scope CurrentUser
Install-Module Microsoft.PowerShell.SecretStore       -Scope CurrentUser

Register-SecretVault -Name idira -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault

# Stored once, interactively. The values never appear in the script.
Set-Secret -Vault idira -Name idira-client-id     -Secret 'svc-automation@cyberark.cloud'
Set-Secret -Vault idira -Name idira-client-secret -Secret (Read-Host -AsSecureString)
</code></pre>
<p>The alternative I reach for, when a project already runs it, is the <a href="https://www.1password.dev/cli/secret-references">1Password CLI</a>. With <code>op run</code>, we store <code>op://vault/item/field</code> references, and 1Password resolves them into a child process's environment for the life of that process alone. The real values stay <a href="https://www.1password.dev/cli/secret-references">out of your project files</a>, and <code>op run</code> redacts them on a best-effort basis when they surface in stdout — redaction, not containment: a child process can still read, transform, or log the resolved value. For an agentic workflow that is the better fit, because the secret stays out of any project file entirely.</p>
<p>Injecting the secret as an environment variable still does not isolate it from the model: on Linux, another process running as our user can usually read our processes' environment, and the model runs as our user. When the model's process must not reach the credential at all, use a <code>PreToolUse</code> hook, or move the credential behind a boundary the model's user cannot cross — a separate OS identity, a broker process, or a container it has no access to.</p>
<p>For the example below we use SecretStore: it is a Microsoft module that runs anywhere PowerShell 7 does and needs no account. It does prompt for a vault password to unlock.</p>
<h2>What the context changes</h2>
<p>With Context7 connected and the <code>CLAUDE.md</code> in the project root, we hand the model one plain instruction:</p>
<pre><code class="language-text">Write a PowerShell script that gets all the Secure Infrastructure Account strong accounts for databases. Make it AS SIMPLE AS POSSIBLE. Do not go overboard with multiple functions, parameterization, etc. The simpler the better.
</code></pre>
<p>The difference from a cold start shows up in the first draft.</p>
<p>Without the context, a model tends to produce something like this -- a composite of the failure modes I have watched, not one captured run:</p>
<pre><code class="language-csharp"># context-free draft -- hardcoded secret, self-hosted flow, hosts collapsed
$secret = 'svc-Pa55w0rd!'                                                       # hardcoded
$logon  = 'https://acme.cyberark.cloud/PasswordVault/API/auth/CyberArk/Logon'   # self-hosted flow
\(token  = Invoke-RestMethod -Method Post -Uri \)logon -Body @{ username = 'svc'; password = $secret }

# prove the token against a service -- but on the bare subdomain, not the .dpa service host
Invoke-RestMethod -Uri 'https://acme.cyberark.cloud/dpa/api/...' -Headers @{ Authorization = $token }
</code></pre>
<p>It hardcodes the secret, reaches for the PAM self-hosted <code>/PasswordVault</code> logon flow, and collapses the token host and the service host into the one subdomain it was handed. The script fails on the first call, and the error is a DNS or 404 that points at the wrong layer.</p>
<p>With the context, the model has what it needs before it writes a line: the platform-token contract as facts from Context7, the host and secret rules from <code>CLAUDE.md</code>, and the ISP tenant's subdomain and Identity tenant ID as non-secret config in a <code>.env</code>. The first draft already sends the grant to <code>platformtoken</code> on the <code>.id</code> host, proves it against the <code>.dpa</code> host built from the tenant subdomain, and reads the credentials from the vault instead of inventing them.</p>
<p>That draft is the script below. Run that prompt yourself and the exact lines will differ -- the model is non-deterministic, and more than one script is correct here -- but the parts the context pins down come out the same: the two hosts, the platform-token grant, and the secret read from the vault by name.</p>
<img src="https://cdn.hashnode.com/uploads/covers/61cf031d7cb96c3f9225f9db/a5725e2c-f95c-485f-a683-3f83ea213e6b.png" alt="" style="display:block;margin:0 auto" />

<p><em>The same request with the context in place: the model honors the host rules and reads the secret from the vault.</em></p>
<h2>The script</h2>
<p>The script gets a token from the Identity <code>platformtoken</code>, then proves it against a service on the tenant-subdomain host. We point the second call at a non-Identity service, <a href="https://api-docs.cyberark.com/secure-infra-access/docs/strong-account-apis-for-sia-databases">Secure Infrastructure Access</a> (SIA), so the call exercises the second host and confirms the model honored the <code>CLAUDE.md</code> host rule instead of reusing the token endpoint's host.</p>
<p>The ISP tenant subdomain and Identity tenant ID are configuration, not secrets, so they go in a plain <code>.env</code>. The tenant is the same on every run, which is why the script takes no parameters -- we write the two values down once and gitignore the file:</p>
<pre><code class="language-text"># .env -- tenant identifiers, not secrets
IDENTITY_TENANT_ID=abc1234
SUBDOMAIN=acme
</code></pre>
<p>The <code>client_id</code> and <code>client_secret</code> stay in the vault, looked up by name; the names are not sensitive either. With the config in <code>.env</code>, the secret in the vault, and the rules and contract already in place, the model has what it needs to write a script that authenticates and reaches both hosts:</p>
<pre><code class="language-powershell">#Requires -Modules Microsoft.PowerShell.SecretManagement

# Non-secret config from .env (IDENTITY_TENANT_ID and SUBDOMAIN are different hosts).
Get-Content .env | ForEach-Object {
    if (\(_ -match '^\s*([^#=]+?)\s*=\s*(.*)\)') { Set-Item "env:\((\)Matches[1].Trim())" $Matches[2].Trim() }
}

# Service-user (OAuth confidential client) credentials from the vault — never hardcoded.
$clientId     = Get-Secret -Name 'idira-client-id'     -AsPlainText
$clientSecret = Get-Secret -Name 'idira-client-secret' -AsPlainText

# OAuth2 client_credentials token from the identity tenant. Short-lived; fetched per run.
$token = (Invoke-RestMethod -Method Post `
    -Uri "https://$env:IDENTITY_TENANT_ID.id.cyberark.cloud/oauth2/platformtoken" `
    -ContentType 'application/x-www-form-urlencoded' `
    -Body @{ grant_type = 'client_credentials'; client_id = \(clientId; client_secret = \)clientSecret }
).access_token

# SIA (formerly DPA) database strong accounts. Verify this path against your tenant's
# SIA OpenAPI — api-docs.cyberark.com lists it inconsistently as /databases/strong-accounts.
(Invoke-RestMethod -Method Get `
    -Uri "https://$env:SUBDOMAIN.dpa.cyberark.cloud/databases/strong-accounts" `
    -Headers @{ Authorization = "Bearer $token" }
).accounts
</code></pre>
<p>The two <code>Invoke-RestMethod</code> calls deliberately hit two different hosts, <code>\(env:IDENTITY_TENANT_ID.id.cyberark.cloud</code> and <code>\)env:SUBDOMAIN.dpa.cyberark.cloud</code>, and that split is the point. Collapse the two hosts into one value, and the second call fails the moment we run it.</p>
<p><code>Get-Secret -AsPlainText</code> does put the secret into a variable in process memory, where it lives until the process exits. That is unavoidable, since the HTTP body needs the cleartext. We never write the secret to a file, never log the secret, never hand the secret to anywhere the model captures output. Keep it that way.</p>
<p><strong>Context7 changes the failure mode; it does not remove the need to verify</strong>. Pulling the endpoint from the docs stops the model inventing one from memory, but the docs themselves can be wrong, and we hit exactly that on the strong-accounts call: the model pulled <code>/databases/strong-accounts</code> from Context7 as the rule demands and flagged it right there in the script above, but that path did not match what works against the tenant, which is <code>/api/database-strong-accounts</code>. So whatever the model writes from a retrieved contract gets checked against the live tenant before we trust it. The docs raise the floor; they are not the last word.</p>
<h2>Limitations</h2>
<p>The script authenticates and proves the two-host split, but it is deliberately minimal, so a few limits come with it:</p>
<ul>
<li><p>We add no token caching or refresh here. For a long-running job, re-authenticate on expiry instead of holding a token past <code>expires_in</code>.</p>
</li>
<li><p>A service account that authenticates with a client secret has no MFA in front of it, so treat the service user's permissions as the blast radius and keep them minimal.</p>
</li>
<li><p>Runtime secret injection is not a sandbox; a <code>PreToolUse</code> hook (or an equivalent boundary outside the harness) is still required to keep the model's process from reaching the credential.</p>
</li>
<li><p>SecretStore unlocks with a vault password; for unattended runs you configure that password, which is one more secret to place carefully.</p>
</li>
<li><p>A real integration needs error handling the example leaves out.</p>
</li>
<li><p>Context7 can reproduce errors in the upstream docs, so verify the retrieved contract.</p>
</li>
</ul>
<h2>Make it a skill</h2>
<p>I built <a href="https://github.com/aaearon/grant-cli"><code>grant-cli</code></a>, a just-in-time (JIT) cloud-role-elevation tool that wraps the <a href="https://github.com/cyberark/idsec-sdk-golang"><code>idsec-sdk-golang</code></a> ISP authenticator, on this exact loop, and the pattern carries well beyond a single language or service.</p>
<p>The <code>CLAUDE.md</code> rules and the Context7 lookups are not specific to that tool or this script. The host rules and the service-user auth note hold on every ISP project we touch, and so does the instruction to pull the contract from Context7. That is the signal that a section of <code>CLAUDE.md</code> has outgrown being a fact and become a procedure worth <a href="https://code.claude.com/docs/en/skills">packaging as a skill</a>: a skill loads only when it is relevant and follows the <a href="https://agentskills.io">Agent Skills</a> standard across harnesses, so the tenant knowledge travels into the next tool instead of being retyped. Context7 itself already ships as a skill and the ISP rules can too.</p>
]]></content:encoded></item><item><title><![CDATA[Creating a CyberArk (now Idira) Central Policy Manager plugin for an API using the new REST API framework]]></title><description><![CDATA[CyberArk (now Idira) has released their new framework for creating Central Policy Manager (CPM) plugins that interact with REST APIs. Unlike WSChains, this framework is documented and supported by Cyb]]></description><link>https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-an-api-using-the-new-rest-api-framework</link><guid isPermaLink="true">https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-an-api-using-the-new-rest-api-framework</guid><category><![CDATA[cyberark]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[xml]]></category><category><![CDATA[central-policy-manager]]></category><category><![CDATA[REST API]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Mon, 17 Mar 2025 17:55:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/BfrQnKBulYQ/upload/4fa5585b00e0aa7ab24fdebc08a39e12.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>CyberArk (now Idira) has released their <a href="https://community.cyberark.com/marketplace/s/#a35Vy000000Lj3lIAC-a39Vy000002WA1VIAW">new framework for creating Central Policy Manager (CPM) plugins that interact with REST APIs</a>. Unlike <a href="https://timschindler.blog/exploring-cyberark-central-policy-managers-wschains">WSChains</a>, this framework is documented and supported by CyberArk, making it worthwhile to become familiar with it. CyberArk is already using it, as it is the framework on which the <a href="https://community.cyberark.com/marketplace/s/#a35Ht0000018s2cIAA-a39Ht000004GLKPIA4">new CyberArk Digital Vault platform</a> is built, among others.</p>
<p>Additionally, it offers an alternative to the Credential Management .NET SDK for storing credentials generated by target systems (or “access keys”), which is becoming increasingly common.</p>
<p>To complete our <a href="https://timschindler.blog/series/shopizer-admin-ext">CyberArk Shopizer Administrator extensions series</a>, we will use it to build a CPM plugin that manages Shopizer users through the Shopizer API.</p>
<h1>Developing the REST API plugin</h1>
<p>The <a href="https://docs.cyberark.com/pam-self-hosted/latest/en/content/plugins/cpm-plugins-rest-api.htm">documentation for the REST API framework</a> is quite comprehensive so there is no need to repeat it. After giving it a read-through and ensuring we’ve met the <a href="https://docs.cyberark.com/pam-self-hosted/latest/en/content/plugins/cpm-plugins-rest-api.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7CCreate%20CPM%20plugins%20for%20REST%20API%20applications%7C_____0#Prerequisites">prerequisites for the framework</a>, we can jump in.</p>
<h2>Creating a new platform</h2>
<p>After we <a href="https://docs.cyberark.com/pam-self-hosted/latest/en/content/plugins/cpm-plugins-rest-api.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7CCreate%20CPM%20plugins%20for%20REST%20API%20applications%7C_____0#CreateyournewplatformbasedontheRESTAPICPMPluginFramework">import the REST API framework CPM plugin</a> all that needs to be done is cloning it for use with our Shopizer platform and under <code>Additional Policy Settings</code> updating two parameters:</p>
<ol>
<li><p>Defining <code>8080</code> as the value for <code>Port</code></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742231581187/e869e7a8-b8ba-4982-8f41-405c362fbad0.png" alt="Screenshot of a software interface showing menu options on the left, including &quot;Automatic Password Management&quot; and &quot;Additional Policy Settings.&quot; On the right, a &quot;Properties&quot; table lists different settings with values, such as &quot;Port&quot; set to &quot;8080&quot; and &quot;UseSSL&quot; set to &quot;Yes.&quot;" style="display:block;margin:0 auto" />
</li>
<li><p>Changing the value for <code>ConfigurationFilePath</code> under to the filename that will contain the Shopizer configuration: <code>ShopizerAdministratorConfiguration.xml</code></p>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741625148660/18670746-09ce-4a4b-8225-e5f314dffef5.png" alt="Screenshot of a software interface showing the &quot;TimSchindler Shopizer Administrator Accounts via REST API.&quot; The left panel lists various account management options, including parameters like &quot;ConfigurationFilePath.&quot; The right panel displays properties with the &quot;Name&quot; and &quot;Value&quot; set as &quot;ConfigurationFilePath&quot; and &quot;ShopizerAdministratorConfiguration.xml.&quot;" style="display:block;margin:0 auto" />

<h2>Creating the parameters file for testing</h2>
<p>When developing the CPM plugin, we will test it by invoking it manually. Our <code>.ini</code> will be similar to <a href="https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-an-api-using-wschains#heading-preparing-our-parameters-file-for-testing">what we used for WSChains</a> with slight modifications:</p>
<pre><code class="language-ini">[targetaccount]
username=shopizeradministrator@timschindler.dev
newpassword=Password2
password=Password2
safename=Safename
foldername=Foldername
objectname=Objectname
PolicyID=PolicyID
; The machine where the Shopizer instance is running.
Address=192.168.178.61
; The port the Shopizer REST API is running on.
Port=8080

[extrapass3]
username=reconcile@timschindler.dev
password=Password1

[extrainfo]
; Path to the configuration file.
ConfigurationFilePath=ShopizerAdministratorConfiguration.xml
AddressValidationPattern=.*
Debug=Yes
ManagementType=password
</code></pre>
<p>After adding <code>ConfigurationFilePath</code> and <code>AddressValidationPattern</code> we can invoke it from the root of the Central Policy Manager’s installation folder with <code>.\bin\CANetPluginInvoker.exe shopizerrestplugin.ini &lt;verifypass/changepass/reconcilepass&gt; CyberArk.Extensions.Generic.Plugin.RestAPI.dll True</code></p>
<h1>Writing the configuration file</h1>
<p>Luckily, we've already done most of the hard work when developing the WSChains plug-in. We know the necessary API calls for each operation, their URLs, and the request and possible response bodies. We just need to format the data to match what the REST API framework expects. We can refer to the <a href="https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api">Shopizer API documentation</a> as needed.</p>
<p>To begin, we copy the sample configuration file provided with the framework, <code>SampleRestAPIPlatformConfiguration.xml</code>, and rename it to <code>ShopizerAdministratorConfiguration.xml</code>.</p>
<h2>Verify</h2>
<p>We’ll leverage the Parameters section for the sake of cleaner code, removing almost everything but opting to leave <code>AddressWithValidation</code> as it is.</p>
<p>We add the parameter <code>AddressWithValidationAndPort</code> that combines the <code>address</code> (host) and <code>port</code> properties along with the scheme, which is hard-coded to be <code>http</code> as the Shopizer instance we are developing against does not have a HTTPS listener.</p>
<pre><code class="language-xml">&lt;Parameters&gt;
	&lt;Parameter name="AddressWithValidation" validationPattern="{{AddressValidationPattern}}"&gt;{{Address}}&lt;/Parameter&gt;
	&lt;Parameter name="AddressWithValidationAndPort"&gt;http://{{AddressWithValidation}}:{{Port}}&lt;/Parameter&gt;
&lt;/Parameters&gt;
</code></pre>
<p>We only need a single <code>APICall</code> in the <code>APICalls</code> section:</p>
<pre><code class="language-xml">&lt;APICall&gt;
	&lt;Request name="LogonAsTargetAccount" method="POST"&gt;
		&lt;URL&gt;{{AddressWithValidationAndPort}}/api/v1/private/login&lt;/URL&gt;
		&lt;Headers&gt;
			&lt;Header key="Content-Type"&gt;application/json&lt;/Header&gt;
		&lt;/Headers&gt;
		&lt;Body&gt;
					{
					"username": "{{Username}}",
					"password": "{{Password|JsonEscape}}"
					}
		&lt;/Body&gt;
	&lt;/Request&gt;
	&lt;Responses&gt;
		&lt;Response name="SuccessfulLogonResponse" type="valid" format="json" statusCode="200"&gt;
			&lt;Parse&gt;
				&lt;ParseBody&gt;
					&lt;Parameter name="id" path="id" /&gt;
					&lt;Parameter name="token" path="token" secure="true"/&gt;
				&lt;/ParseBody&gt;
			&lt;/Parse&gt;
		&lt;/Response&gt;
		&lt;Response name="IncorrectPassword" type="error" statusCode="401"&gt;
			&lt;Parse&gt;
				&lt;ParseBody&gt;
					&lt;Parameter name="ErrorMessage" path="message"/&gt;
				&lt;/ParseBody&gt;
			&lt;/Parse&gt;
			&lt;ErrorMessage name="IncorrectPasswordMsg" returnCode="2001" description="{{ErrorMessage}}"/&gt;
		&lt;/Response&gt;
	&lt;/Responses&gt;
&lt;/APICall&gt;
</code></pre>
<p>The <code>Request</code> element does not have any magic to it. It is exactly what we would see if we were making the call using a tool such as cURL or PowerShell’s <code>Invoke-RestMethod</code>. The only part that is specific to the REST API framework is how we can JSON escape placeholders using <a href="https://docs.cyberark.com/pam-self-hosted/latest/en/content/plugins/cpm-plugins-rest-api-placeholders.htm#Placeholdermodifiers">placeholder modifiers</a>.</p>
<p>We have two <code>Response</code> child elements in the <code>Responses</code> element: one representing a successful logon response and a failed logon due to an incorrect password. In the successful logon response, we parse out the returned <code>id</code> and <code>token</code> as we can reuse the <code>Response</code> in other <code>APICall</code> elements. In the case of a failed login, we parse out the message so it can be displayed in the PVWA.</p>
<p>The last piece is defining the <code>Request</code> name in the <code>verifypass</code> chain in the <code>Chains</code> element. With that, our <code>ShopizerAdministratorConfiguration.xml</code> reads as below:</p>
<pre><code class="language-xml">&lt;RestPlugin&gt;

	&lt;Parameters&gt;
		&lt;Parameter name="AddressWithValidation" validationPattern="{{AddressValidationPattern}}"&gt;{{Address}}&lt;/Parameter&gt;
		&lt;Parameter name="AddressWithValidationAndPort"&gt;http://{{AddressWithValidation}}:{{Port}}&lt;/Parameter&gt;
	&lt;/Parameters&gt;

	&lt;ErrorMessages/&gt;

	&lt;APICalls&gt;
		&lt;APICall&gt;
			&lt;Request name="LogonAsTargetAccount" method="POST"&gt;
				&lt;URL&gt;{{AddressWithValidationAndPort}}/api/v1/private/login&lt;/URL&gt;
				&lt;Headers&gt;
					&lt;Header key="Content-Type"&gt;application/json&lt;/Header&gt;
				&lt;/Headers&gt;
				&lt;Body&gt;
					{
					"username": "{{Username}}",
					"password": "{{Password|JsonEscape}}"
					}
				&lt;/Body&gt;
			&lt;/Request&gt;
			&lt;Responses&gt;
				&lt;Response name="SuccessfulLogonResponse" type="valid" format="json" statusCode="200"&gt;
					&lt;Parse&gt;
						&lt;ParseBody&gt;
							&lt;Parameter name="id" path="id" /&gt;
							&lt;Parameter name="token" path="token" secure="true"/&gt;
						&lt;/ParseBody&gt;
					&lt;/Parse&gt;
				&lt;/Response&gt;
				&lt;Response name="IncorrectPassword" type="error" statusCode="401"&gt;
					&lt;Parse&gt;
						&lt;ParseBody&gt;
							&lt;Parameter name="ErrorMessage" path="message"/&gt;
						&lt;/ParseBody&gt;
					&lt;/Parse&gt;
					&lt;ErrorMessage name="IncorrectPasswordMsg" returnCode="2001" description="{{ErrorMessage}}"/&gt;
				&lt;/Response&gt;
			&lt;/Responses&gt;
		&lt;/APICall&gt;
	&lt;/APICalls&gt;

	&lt;Chains&gt;
		&lt;Chain name="verifypass"&gt;
			&lt;Request name="LogonAsTargetAccount" /&gt;
		&lt;/Chain&gt;
	&lt;/Chains&gt;

&lt;/RestPlugin&gt;
</code></pre>
<p>Even though we are not using <code>ErrorMessages</code>, we need to at least define <code>&lt;ErrorMessages/&gt;</code> as the REST API framework expects the element to be there.</p>
<p>Testing the plugin, we see positive progress:</p>
<pre><code class="language-powershell">PS &gt; .\bin\CANetPluginInvoker.exe shopizerrestplugin.ini verifypass CyberArk.Extensions.Generic.Plugin.RestAPI.dll True
The plugin ended successfully (RC = 0)
PS &gt;
</code></pre>
<h2>Change</h2>
<p>We need one more <code>APICall</code> that is used to change the password.</p>
<p>The additional <code>APICall</code> in the <code>APICalls</code> element for our change operation is:</p>
<pre><code class="language-xml">&lt;APICall&gt;
	&lt;Request name="ChangeAsTargetAccount" method="PATCH"&gt;
		&lt;URL&gt;{{AddressWithValidationAndPort}}/api/v1/private/user/{{id}}/password&lt;/URL&gt;
		&lt;Headers&gt;
			&lt;Header key="Content-Type"&gt;application/json&lt;/Header&gt;
			&lt;Header key="Authorization"&gt;Bearer {{token}}&lt;/Header&gt;
		&lt;/Headers&gt;
		&lt;Body&gt;
					{
					"password": "{{password|JsonEscape}}",
					"changePassword":"{{newpassword|JsonEscape}}"
					}
		&lt;/Body&gt;
	&lt;/Request&gt;
	&lt;Responses&gt;
		&lt;Response name="ValidChangeResponse" type="valid" statusCode="200"/&gt;
		&lt;Response name="InternalErrorResponse" type="error" statusCode="500"&gt;
			&lt;Parse&gt;
				&lt;ParseBody&gt;
					&lt;Parameter name="ErrorMessage" path="error"/&gt;
				&lt;/ParseBody&gt;
			&lt;/Parse&gt;
			&lt;ErrorMessage name="InternalErrorResponseMsg" returnCode="2002" description="{{ErrorMessage}}"/&gt;
		&lt;/Response&gt;
	&lt;/Responses&gt;
&lt;/APICall&gt;
</code></pre>
<p>The <code>Request</code> element is simple. We include the Bearer token from the <code>LogonAsTargetAccount</code> Request as an <code>Authorization</code> header.</p>
<p>A successful password change gives a status code <code>200</code> with no extra details to process. We have a response for when the API returns a <code>500</code> — this is the only code the Shopizer API seems to give when the new password doesn't meet complexity requirements.</p>
<p>Adding the below <code>Chain</code> element to <code>Chains</code> and a successful test has us moving on to the reconcile operation.</p>
<pre><code class="language-xml">&lt;Chain name="changepass"&gt;
	&lt;Request name="LogonAsTargetAccount" /&gt;
	&lt;Request name="ChangeAsTargetAccount" /&gt;
&lt;/Chain&gt;
</code></pre>
<pre><code class="language-powershell">PS &gt; .\bin\CANetPluginInvoker.exe shopizerrestplugin.ini changepass CyberArk.Extensions.Generic.Plugin.RestAPI.dll True
The plugin ended successfully (RC = 0)
PS &gt;
</code></pre>
<h2>Reconcile</h2>
<p>Compared to the verify and change operation, reconcile is more complicated. Our reconcile operation needs to:</p>
<ol>
<li><p>Log in as the reconciliation account.</p>
</li>
<li><p>Get the details of the target account.</p>
</li>
<li><p>Set the new password for the target account, using the details from step 2.</p>
</li>
</ol>
<p>We can create a new <code>APICall</code> element just for logging in as the reconciliation account, but since the request and responses would be the same as our current <code>APICall</code> for logging in as the target account during the verify operation, this would lead to a lot of repetition.</p>
<p>By using <a href="https://docs.cyberark.com/pam-self-hosted/latest/en/content/plugins/cpm-plugins-rest-api-placeholders.htm#Supportedimplementations">dynamic account types for placeholders</a>, we can modify the existing <code>APICall</code> to work for logging in as either the target account or the reconciliation account.</p>
<p>The <code>APICall</code> element is updated to:</p>
<pre><code class="language-xml">&lt;APICall&gt;
	&lt;Request name="Logon" method="POST"&gt;
		&lt;URL&gt;{{AddressWithValidationAndPort}}/api/v1/private/login&lt;/URL&gt;
		&lt;Headers&gt;
			&lt;Header key="Content-Type"&gt;application/json&lt;/Header&gt;
		&lt;/Headers&gt;
		&lt;Body&gt;
					{
					"username": "{{reconcileaccountORtargetaccount\username}}",
					"password": "{{reconcileaccountORtargetaccount\password|JsonEscape}}"
					}
				&lt;/Body&gt;
	&lt;/Request&gt;
	&lt;Responses&gt;
		&lt;Response name="SuccessfulLogonResponse" type="valid" format="json" statusCode="200"&gt;
			&lt;Parse&gt;
				&lt;ParseBody&gt;
					&lt;Parameter name="id" path="id" /&gt;
					&lt;Parameter name="token" path="token" secure="true"/&gt;
				&lt;/ParseBody&gt;
			&lt;/Parse&gt;
		&lt;/Response&gt;
		&lt;Response name="IncorrectPassword" type="error" statusCode="401"&gt;
			&lt;Parse&gt;
				&lt;ParseBody&gt;
					&lt;Parameter name="ErrorMessage" path="message"/&gt;
				&lt;/ParseBody&gt;
			&lt;/Parse&gt;
			&lt;ErrorMessage name="IncorrectPasswordMsg" returnCode="2001" description="{{ErrorMessage}}"/&gt;
		&lt;/Response&gt;
	&lt;/Responses&gt;
&lt;/APICall&gt;
</code></pre>
<p>Besides updating the <code>Body</code> element of the <code>Request</code>, we change the <code>name</code> attribute to <code>Logon</code> to show that it's not used for only logging in as the target account anymore. <mark class="bg-yellow-200 dark:bg-yellow-500/30">Be sure to update the </mark> <code>verifypass</code> <mark class="bg-yellow-200 dark:bg-yellow-500/30">and </mark> <code>changepass</code> <mark class="bg-yellow-200 dark:bg-yellow-500/30">chains with the new name of the </mark> <code>Request</code> <mark class="bg-yellow-200 dark:bg-yellow-500/30">element!</mark></p>
<p>How do dynamic account type placeholders work? The documentation states:</p>
<blockquote>
<p>The plugin searches for the ParameterName in the accounts declared from left to right and takes the first occurrence it finds. If the parameter is not found in any account, the replacement fails. For example, {{logonaccountORextrapass2ORtargetaccount\username}} tries to retrieve the username in the logon account. If the plugin cannot find it, it tries to retrieve the username from the extrapass2 account. If not found there as well, the plugin tries to retrieve the username from the target account. If the username parameter is not found in any of the declared accounts, the plugin fails.</p>
</blockquote>
<p>The CPM only gives the reconciliation account's details during a reconcile operation. Since we list <code>reconcileaccount</code> first in the dynamic type placeholder, the plugin uses the reconcile account details for the Logon <code>Request</code> during the reconcile operation and uses the target account details for verify and change operations.</p>
<p>We still need the <code>APICall</code> elements for retrieving the target account’s details and the actual password change.</p>
<p>To retrieve the target account’s details needed as part of the password change process:</p>
<pre><code class="language-xml">&lt;APICall&gt;
	&lt;Request name="GetUsersInformation" method="GET"&gt;
		&lt;URL&gt;{{AddressWithValidationAndPort}}/api/v1/private/users&lt;/URL&gt;
		&lt;Headers&gt;
			&lt;Header key="Content-Type"&gt;application/json&lt;/Header&gt;
			&lt;Header key="Authorization"&gt;Bearer {{token}}&lt;/Header&gt;
		&lt;/Headers&gt;
		&lt;Body&gt;&lt;/Body&gt;
	&lt;/Request&gt;
	&lt;Responses&gt;
		&lt;Response name="ValidGetUsersInformationResponse" type="valid" format="json" statusCode="200"&gt;
			&lt;Parse&gt;
				&lt;ParseBody&gt;
					&lt;Parameter name="targetaccount\id" path="..data[?(@.userName=='{{username}}')].id"/&gt;
					&lt;Parameter name="targetaccount\active" path="..data[?(@.userName=='{{username}}')].active"/&gt;
					&lt;Parameter name="targetaccount\defaultLanguage" path="..data[?(@.userName=='{{username}}')].defaultLanguage"/&gt;
					&lt;Parameter name="targetaccount\emailAddress" path="..data[?(@.userName=='{{username}}')].emailAddress"/&gt;
					&lt;Parameter name="targetaccount\firstName" path="..data[?(@.userName=='{{username}}')].firstName"/&gt;
					&lt;Parameter name="targetaccount\groups" path="..data[?(@.userName=='{{username}}')].groups"/&gt;
					&lt;Parameter name="targetaccount\lastName" path="..data[?(@.userName=='{{username}}')].lastName"/&gt;
					&lt;Parameter name="targetaccount\merchant" path="..data[?(@.userName=='{{username}}')].merchant"/&gt;
					&lt;Parameter name="targetaccount\username" path="..data[?(@.userName=='{{username}}')].userName"/&gt;
				&lt;/ParseBody&gt;
			&lt;/Parse&gt;
		&lt;/Response&gt;
		&lt;Response name="InternalErrorResponse"/&gt;
	&lt;/Responses&gt;
&lt;/APICall&gt;
</code></pre>
<p>The only thing we are doing for the first time is parsing the response using <a href="https://en.wikipedia.org/wiki/JSONPath">JSONPath</a>.</p>
<p>We used XPath before with the <a href="https://timschindler.blog/creating-a-cyberark-privileged-session-manager-connection-component-for-a-web-application#heading-identifying-elements-in-the-dom">Shopizer PSM connection component</a>, and JSONPath works in a similar way. We use it to extract user details from the response. Since the <code>APICall</code> element calls a URL that returns all Shopizer users, we use JSONPath to find the information we need for the target account, using the <code>username</code> placeholder in the expression.</p>
<p>In the final <code>APICall</code> element, we include the new password along with the details extracted from the previous <code>APICall</code>:</p>
<pre><code class="language-xml">&lt;APICall&gt;
	&lt;Request name="ChangeAsReconcileAccount" method="PUT"&gt;
		&lt;URL&gt;{{AddressWithValidationAndPort}}/api/v1/private/user/{{targetaccount\id}}&lt;/URL&gt;
		&lt;Headers&gt;
			&lt;Header key="Content-Type"&gt;application/json&lt;/Header&gt;
			&lt;Header key="Authorization"&gt;Bearer {{token}}&lt;/Header&gt;
		&lt;/Headers&gt;
		&lt;Body&gt;
					{
					"active": {{targetaccount\active}},
					"defaultLanguage": "{{targetaccount\defaultLanguage}}",
					"firstName": "{{targetaccount\firstName}}",
					"groups:": {{targetaccount\groups}},
					"lastName": "{{targetaccount\lastName}}",
					"password": "{{newpassword}}",
					"repeatPassword": "{{newpassword}}",
					"store": "{{targetaccount\merchant}}",
					"emailAddress": "{{targetaccount\emailAddress}}",
					"userName": "{{targetaccount\username}}"			
					}
				&lt;/Body&gt;
	&lt;/Request&gt;
	&lt;Responses&gt;
		&lt;Response name="ValidReconcileChangePasswordResponse" type="valid" statusCode="200"/&gt;
		&lt;Response name="InternalErrorResponse"/&gt;
	&lt;/Responses&gt;
&lt;/APICall&gt;
</code></pre>
<p>Finally, our <code>Chains</code> element looks like:</p>
<pre><code class="language-xml">&lt;Chains&gt;
	&lt;Chain name="verifypass"&gt;
		&lt;Request name="Logon" /&gt;
	&lt;/Chain&gt;
	&lt;Chain name="changepass"&gt;
		&lt;Request name="Logon" /&gt;
		&lt;Request name="ChangeAsTargetAccount" /&gt;
	&lt;/Chain&gt;
	&lt;Chain name="reconcilepass"&gt;
		&lt;Request name="Logon" /&gt;
		&lt;Request name="GetUsersInformation" /&gt;
		&lt;Request name="ChangeAsReconcileAccount" /&gt;
	&lt;/Chain&gt;
&lt;/Chains&gt;
</code></pre>
<div>
<div>💡</div>
<div>You can find the complete platform, including the configuration constructed above, in <a target="_self" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://github.com/aaearon/cyberark-shopizer-admin-extensions/blob/main/restapi/ShopizerAdministratorConfiguration.xml" style="pointer-events:none">GitHub</a>.</div>
</div>

<p>In the end, what matters is that the plugin works when invoked by the CPM so we would be remiss not to ensure it works as we expect. Invoking the reconcile in the PVWA versus invoking it manually we see success:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742231701332/3c3eccab-cffe-48d0-9dbe-5774fa06c03a.png" alt="The image shows an account overview interface for a user at &quot;shopizeradministrator@timschindler.dev&quot; on IP address 192.168.0.4. It indicates compliance status as compliant, with a reconciliation done by PasswordManager. The status was checked 0 days ago, and there are buttons for reconciling and changing settings. The latest activity is a password reconcile action by PasswordManager performed on March 17 at 6:12:21 PM." style="display:block;margin:0 auto" />

<h1>Conclusion</h1>
<p>Creating a CyberArk Central Policy Manager plugin using the new REST API framework offers a robust and supported method for managing credentials through REST APIs. This approach not only aligns with CyberArk's latest platform developments but also provides a documented alternative to older methods like WSChains.</p>
<p>By following the documentation and leveraging the flexibility of the REST API framework, we can efficiently build plugins tailored to specific applications, such as managing Shopizer administrator users.</p>
<p>See the below GitHub repository for all the platform files.</p>
<p><a href="https://github.com/aaearon/cyberark-shopizer-admin-extensions/tree/main/restapi">https://github.com/aaearon/cyberark-shopizer-admin-extensions</a></p>
]]></content:encoded></item><item><title><![CDATA[Creating a CyberArk (now Idira) Central Policy Manager plugin for an API using WSChains]]></title><description><![CDATA[In a previous article, we created a CyberArk (now Idira) Central Policy Manager plugin for the web application Shopizer using the Web Application CPM Plugin Framework and while it works, we had to emp]]></description><link>https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-an-api-using-wschains</link><guid isPermaLink="true">https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-an-api-using-wschains</guid><category><![CDATA[cyberark]]></category><category><![CDATA[wschains]]></category><category><![CDATA[central-policy-manager]]></category><category><![CDATA[Privileged access management]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[cybersecurity]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Fri, 02 Aug 2024 15:39:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722612647614/7b319541-46d1-4b37-ac78-6d6b3b8c2526.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-a-web-application">In a previous article</a>, we created a CyberArk (now Idira) Central Policy Manager plugin for the web application Shopizer using the <a href="https://cyberark-customers.force.com/mplace/s/#a352J000000WU3aQAG-a392J0000013WwCQAU">Web Application CPM Plugin Framework</a> and while it works, we had to employ an awkward workaround to get reconcile functioning. As Shopizer's web interfaces leverage its APIs, we should skip the middleman and have our CPM plugin interface those.</p>
<p><a href="https://timschindler.blog/exploring-cyberark-central-policy-managers-wschains">WSChains is an undocumented, unsupported, used-by-CyberArk-interally framework to build CPM plugins that interact with APIs.</a> Using our existing knowledge of Shopizer, examples of WSChains-based plugins from the CyberArk Marketplace, we will build a WSChains-based CPM plugin to manage Shopizer users.</p>
<div>
<div>💡</div>
<div>This is article is one in a <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://timschindler.blog/series/shopizer-admin-ext" style="pointer-events:none">series of articles around creating PSM connectors and CPM plugins</a>.</div>
</div>

<div>
<div>❗</div>
<div>Interested in seeing WSChains becoming a documented and supported option? <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://community.cyberark.com/s/article/Document-and-support-the-WSChains-CPM-plugin-84e7-e8d" style="pointer-events:none">Vote on this enhancement request</a>!</div>
</div>

<h1>Developing the WSChains plugin</h1>
<p>Armed with <a href="https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api">Shopizer REST API documentation</a> and a bit of knowledge about WSChains, we begin. We will re-use the Shopizer administrator users <a href="https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-a-web-application#heading-setting-the-stage">we previously created</a>.</p>
<h2>Creating a new web application platform</h2>
<p>We can clone the <a href="https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-a-web-application#heading-creating-a-new-web-application-platform">web application framework-based platform</a> and make some changes:</p>
<ul>
<li><p>Under Automatic Password Management -&gt; CPM Plug-in, change <code>DLLName</code> to <code>Cyberark.Extensions.Plugin.WSChains.dll</code>.</p>
</li>
<li><p>Under Automatic Password Management -&gt; Generate Password, update <code>PasswordForbiddenChars</code> to include <code>&lt;&gt;&amp;\"'</code>, which are characters that need to be escaped inside of XML documents.</p>
</li>
<li><p>Under Automatic Password Management -&gt; Additional Policy Settings, set <code>Port</code> to <code>8080</code> as this is the default port the Shopizer REST API listens on. <code>Debug</code> can be set to <code>Yes</code> but a majority of testing will be done outside of CyberArk.</p>
</li>
<li><p>Under Automatic Password Management -&gt; Additional Policy Settings -&gt; Parameters, we only need a single, additional parameter named <code>ChainsFilePath</code> with the value of <code>bin\ShopizerAdministratorChains.xml</code> -- the relative path to the XML <code>Chains</code> file that holds the operations of the CPM plugin.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722603063695/d3d2a04e-8f7e-4e68-8987-942e8f14fd3e.png" alt="" style="display:block;margin:0 auto" />

<div>
<div>❗</div>
<div>The WSChains DLL <code>Cyberark.Extensions.Plugin.WSChains.dll</code> is not available as part of the CPM's base installation. You need to acquire the DLL as part of a platform package from the CyberArk Marketplace or another source.</div>
</div>

<h2>Preparing our parameters file for testing</h2>
<p>We can test a WSChains plugin the same way we can test any other and we will do this to facilitate faster and easier development of our plugin. The <a href="https://docs.cyberark.com/pam-self-hosted/latest/en/Content/PASIMP/Plug-in-NetInvoker_Test.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7CCredentials%20Management%20.NET%20SDK%7C_____5#Parameterfile">Parameters file</a> looks like the following:</p>
<pre><code class="language-ini">[targetaccount]
username=shopizeradministrator@timschindler.dev
newpassword=Password2
password=Password2
safename=Safename
foldername=Foldername
objectname=Objectname
PolicyID=PolicyID
; The machine where the Shopizer instance is running.
Address=192.168.178.61
; The port the Shopizer REST API is running on.
Port=8080

[extrapass3]
username=reconcile@timschindler.dev
password=Password1

[extrainfo]
; Path to the chains file.
ChainsFilePath=bin\ShopizerAdministratorChains.xml
Debug=Yes
</code></pre>
<p>This is saved as an <code>.ini</code> file in the root of the CPM folder.</p>
<h1>Crafting <code>ShopizerAdministratorChains.xml</code></h1>
<p>Like a <code>WebFormFieldsFile</code>, the Chains file will hold the details for all three operations of our CPM plugin. For each operation, we will need to define the necessary requests that will be used by the links in the chains that represent each CPM operation.</p>
<p>We start with the bare minimum and some Fails.</p>
<pre><code class="language-xml">&lt;WSChains name="ShopizerAdministrator"&gt;
	&lt;GlobalSettings&gt;
	&lt;/GlobalSettings&gt;
	&lt;Requests&gt;
	&lt;/Requests&gt;
	&lt;Fails&gt;
		&lt;Fail name="FAILBadRequest" rc="8101" message="Status Code = 400. Bad Request" /&gt;
		&lt;Fail name="FAILUnauthorized" rc="8102" message="Status Code = 401. The request sent by the CPM could not be authenticated" /&gt;
		&lt;Fail name="FAILForbidden" rc="8103" message="Status Code = 403. Access to the requested (valid) URL by the client is forbidden" /&gt;
		&lt;Fail name="FAILNotFound" rc="8104" message="Status Code = 404. Resource not found." /&gt;
		&lt;Fail name="FAILMethodNotFound" rc="8105" message="Status Code = 405. Method not allowed." /&gt;
		&lt;Fail name="FAILRequestConflict" rc="8106" message="Status Code = 409. Request conflict with current state of the server" /&gt;
		&lt;Fail name="FAILInternalServerError" rc="8107" message="Status Code = 500. The server cannot process the request for an unknown reason" /&gt;
		&lt;Fail name="FAILWebServer" rc="8108" message="Status Code = 501. The server cannot process the request for an unknown reason" /&gt;
		&lt;Fail name="FAILGeneral" rc="8109" message="General Error. Check the logs for more information." /&gt;
		&lt;Fail name="FAILUnauthorizedCurrPass" rc="8110" message= "Failed to authenticate to the API using the target account username and current password. Status code 401." /&gt;
		&lt;Fail name="FAILUnauthorizedNewPass" rc="8111" message= "Failed to authenticate to the API using the target account username and new password. Status code 401." /&gt;
		&lt;Fail name="FAILToParseRecUsername" rc="8112" message= "Failed to parse the target account ID to perform a reconciliation" /&gt;
	&lt;/Fails&gt;

	&lt;Chains&gt;
	&lt;/Chains&gt;
&lt;/WSChains&gt;
</code></pre>
<p>Save the file as <code>ShopizerAdministratorChains.xml</code> in the <code>bin</code> folder of the CPM.</p>
<h2>Verify</h2>
<p>Like the web application framework plugin, we will simply authenticate to the Shopizer REST API. Authenticating with a correct username and password will result in an authentication token.</p>
<p>In the <code>Requests</code> element, the following <code>RequestWrapper</code> needs to be added based on the <a href="https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20authentication%20Api/authenticateUsingPOST_1">Shopizer REST API documentation</a>:</p>
<pre><code class="language-xml">&lt;Requests&gt;
	&lt;RequestWrapper&gt;
		&lt;request name="GetToken"&gt;
			&lt;version&gt;2&lt;/version&gt;
			&lt;general&gt;
				&lt;endpoint&gt;/api/v1/private/login&lt;/endpoint&gt;
				&lt;method&gt;POST&lt;/method&gt;
			&lt;/general&gt;
			&lt;header&gt;
				&lt;Content-Type&gt;application/json&lt;/Content-Type&gt;
				&lt;Accept&gt;application/json&lt;/Accept&gt;
				&lt;User-Agent&gt;Integration/1.0 (CyberArk, CPM, 1)&lt;/User-Agent&gt;
			&lt;/header&gt;
			&lt;body&gt;
				&lt;json&gt;
					{
						"username":"{{username}}",
						"password":"{{pmpass}}"
					}
				&lt;/json&gt;
			&lt;/body&gt;
		&lt;/request&gt;
	&lt;/RequestWrapper&gt;
&lt;/Requests&gt;
</code></pre>
<p>Inside the <code>request</code> element, the <code>name</code> attribute is used to refer to the Request in our Chain's links. <code>{{username}}</code> and <code>{{pmpass}}</code> will be replaced with the username and password of the account when the request is executed.</p>
<p>Under the <code>Chains</code> element, we create a <code>Chain</code> with the name of <code>verifypass</code>. It contains only a single <code>Link</code>:</p>
<pre><code class="language-xml">&lt;Chain name="verifypass"&gt;
	&lt;Link name="VerifyPasswordLink" request="GetToken"&gt;
		&lt;StatusCode value="200" next="END"&gt;
			&lt;Parse name="parsed/token" type="json" value="token" secure="true"/&gt;
		&lt;/StatusCode&gt;
		&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
		&lt;StatusCode value="401" next="FAILUnauthorized"/&gt;
		&lt;StatusCode value="403" next="FAILForbidden"/&gt;
		&lt;StatusCode value="404" next="FAILNotFound"/&gt;
		&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
		&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
		&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
		&lt;StatusCode value="501" next="FAILWebServer"/&gt;
		&lt;StatusCode value="*" next="FAILGeneral"/&gt;
	&lt;/Link&gt;
&lt;/Chain&gt;
</code></pre>
<p>The <code>Link</code> object is mostly self-explanatory: the <code>request</code> attribute defines what <code>Request</code> is invoked and the different <code>StatusCode</code> elements determine how the response to the request is handled.</p>
<div>
<div>❗</div>
<div>There is not a hard requirement to having a child <code>Parse</code> element under the <code>StatusCode</code> that represents the end of the CPM operation however not parsing the returned authentication token will result in it being shown in plaintext in the debug logs!</div>
</div>

<p>When testing, we quickly get positive feedback:</p>
<pre><code class="language-powershell">PS C:\Program Files (x86)\CyberArk\Password Manager&gt; .\bin\CANetPluginInvoker.exe shopizerwschains.ini verifypass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
</code></pre>
<p>Looking in the debug logs, we get a nice picture of what the plugin is doing.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722607018599/5414fb2c-406a-4725-b372-48d2531c51a9.png" alt="" style="display:block;margin:0 auto" />

<h2>Change</h2>
<p>Change is made simple for us as in the same response that provides the authentication token, it provides the <code>id</code> of the user the token is for. This is important <a href="https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20management%20resource%20(User%20Management%20Api)/passwordUsingPATCH">as we need to specify the <code>id</code> of the user whose password we are changing as part of the request URL</a>.</p>
<p>As part of the change operation, at the end, we are going to authenticate with the new password to be sure of the change so we need three <code>requests</code> in total. We can re-use the <code>GetToken</code> request but we need one represent the change operation itself as well as logging in with the new password:</p>
<pre><code class="language-xml">&lt;Requests&gt;
	&lt;RequestWrapper&gt;
		&lt;request name="ChangePassword"&gt;
			&lt;version&gt;2&lt;/version&gt;
			&lt;general&gt;
				&lt;endpoint&gt;/api/v1/private/user/{{parsed/id}}/password&lt;/endpoint&gt;
				&lt;method&gt;PATCH&lt;/method&gt;
			&lt;/general&gt;
			&lt;header&gt;
				&lt;Content-Type&gt;application/json&lt;/Content-Type&gt;
				&lt;Accept&gt;application/json&lt;/Accept&gt;
				&lt;User-Agent&gt;Integration/1.0 (CyberArk, CPM, 1)&lt;/User-Agent&gt;
				&lt;Authorization&gt;Bearer {{parsed/token}}&lt;/Authorization&gt;
			&lt;/header&gt;
			&lt;body&gt;
				&lt;json&gt;
				  {
				  "password": "{{pmpass}}",
				  "changePassword": "{{pmnewpass}}"
				  }
				&lt;/json&gt;
			&lt;/body&gt;
		&lt;/request&gt;
	&lt;/RequestWrapper&gt;
	&lt;RequestWrapper&gt;
		&lt;request name="GetTokenWithNewPass"&gt;
			&lt;version&gt;2&lt;/version&gt;
			&lt;general&gt;
				&lt;endpoint&gt;/api/v1/private/login&lt;/endpoint&gt;
				&lt;method&gt;POST&lt;/method&gt;
			&lt;/general&gt;
			&lt;header&gt;
				&lt;Content-Type&gt;application/json&lt;/Content-Type&gt;
				&lt;Accept&gt;application/json&lt;/Accept&gt;
				&lt;User-Agent&gt;Integration/1.0 (CyberArk, CPM, 1)&lt;/User-Agent&gt;
			&lt;/header&gt;
			&lt;body&gt;
				&lt;json&gt;
					{
						"username":"{{username}}",
						"password":"{{pmnewpass}}"
					}
				&lt;/json&gt;
			&lt;/body&gt;
		&lt;/request&gt;
	&lt;/RequestWrapper&gt;
&lt;/Requests&gt;
</code></pre>
<p>We add the <code>Authorization</code> header with the Bearer token we received from the <code>GetToken</code> request to the <code>ChangePassword</code> request. <code>GetTokenWithNewPass</code> looks exactly like <code>GetToken</code> but we use <code>{{pmnewpass}}</code> as the password.</p>
<p>Under the <code>Chains</code> element, we create a <code>Chain</code> with the name of <code>changepass</code> with three <code>Links</code>:</p>
<pre><code class="language-xml">&lt;Chains&gt;
	&lt;Chain name="changepass"&gt;
		&lt;Link name="ChangePasswordGetTokenLink" request="GetToken"&gt;
			&lt;StatusCode value="200" next="ChangePasswordLink"&gt;
				&lt;Parse name="parsed/token" type="json" value="token" secure="true"/&gt;
				&lt;Parse name="parsed/id" type="json" value="id" secure="false"/&gt;
			&lt;/StatusCode&gt;
			&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
			&lt;StatusCode value="401" next="FAILUnauthorizedCurrPass"/&gt;
			&lt;StatusCode value="403" next="FAILForbidden"/&gt;
			&lt;StatusCode value="404" next="FAILNotFound"/&gt;
			&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
			&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
			&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
			&lt;StatusCode value="501" next="FAILWebServer"/&gt;
			&lt;StatusCode value="*" next="FAILGeneral"/&gt;
		&lt;/Link&gt;
		&lt;Link name="ChangePasswordLink" request="ChangePassword"&gt;
			&lt;StatusCode value="200" next="VerifyNewPassLink"/&gt;
			&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
			&lt;StatusCode value="401" next="FAILUnauthorized"/&gt;
			&lt;StatusCode value="403" next="FAILForbidden"/&gt;
			&lt;StatusCode value="404" next="FAILNotFound"/&gt;
			&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
			&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
			&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
			&lt;StatusCode value="501" next="FAILWebServer"/&gt;
			&lt;StatusCode value="*" next="FAILGeneral"/&gt;
		&lt;/Link&gt;
		&lt;Link name="VerifyNewPassLink" request="GetTokenWithNewPass"&gt;
			&lt;StatusCode value="200" next="END"&gt;
				&lt;Parse name="parsed/tokennew" type="json" value="token" secure="true" /&gt;
			&lt;/StatusCode&gt;
			&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
			&lt;StatusCode value="401" next="FAILUnauthorizedNewPass"/&gt;
			&lt;StatusCode value="403" next="FAILForbidden"/&gt;
			&lt;StatusCode value="404" next="FAILNotFound"/&gt;
			&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
			&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
			&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
			&lt;StatusCode value="501" next="FAILWebServer"/&gt;
			&lt;StatusCode value="*" next="FAILGeneral"/&gt;
		&lt;/Link&gt;
	&lt;/Chain&gt;
&lt;/Chains&gt;
</code></pre>
<p>The first <code>Link</code> is copied from our verify operation's <code>Chain</code>. The only difference is the addition of the <code>Parse</code> element that captures the <code>id</code> of the account so we can use it in the next Link. The <code>next</code> attribute of the Link tells the plugin the name of the Link in the Chain to execute.</p>
<p><code>ChangePasswordLink</code> invokes the <code>ChangePassword</code> request and when executed successfully (the response's status code is <code>200</code>), <code>VerifyNewPassLink</code> is called to login with the password just set. Again we parse the result so that we can mask the authentication token from the response in the logs.</p>
<p>Testing shows success:</p>
<pre><code class="language-powershell">PS C:\Program Files (x86)\CyberArk\Password Manager&gt; .\bin\CANetPluginInvoker.exe shopizerwschains.ini changepass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
</code></pre>
<h2>Reconcile</h2>
<p>Reconcile is complex. We need to use <a href="https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20management%20resource%20(User%20Management%20Api)/updateUsingPUT_16">a different endpoint</a> when changing the password of another user. The request URL includes the <code>id</code> of the user to be changed -- requiring us to either store it as an account property or look this up as part of the CPM operation -- and because it is a PUT operation, we need to provide all the existing details of the user along with the new password, otherwise they will be deleted from the user's profile.</p>
<p>There is <a href="https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20management%20resource%20(User%20Management%20Api)/listUsingGET_18">an endpoint that will list all users and their <code>id</code></a> so we will need to call this as a link in our reconcile operation's chain and parse the response. We will then <a href="https://app.swaggerhub.com/apis/shopizer/shopizer-rest-api/3.0.1#/User%20management%20resource%20(User%20Management%20Api)/getUsingGET_14">retrieve the user's profile</a> in order to get the information we need to pass along as part of the change password request.</p>
<p>We need the following, additional requests:</p>
<pre><code class="language-xml">&lt;Requests&gt;
	&lt;RequestWrapper&gt;
		&lt;request name="RecGetToken"&gt;
			&lt;version&gt;2&lt;/version&gt;
			&lt;general&gt;
				&lt;endpoint&gt;/api/v1/private/login&lt;/endpoint&gt;
				&lt;method&gt;POST&lt;/method&gt;
			&lt;/general&gt;
			&lt;header&gt;
				&lt;Content-Type&gt;application/json&lt;/Content-Type&gt;
				&lt;Accept&gt;application/json&lt;/Accept&gt;
				&lt;User-Agent&gt;Integration/1.0 (CyberArk, CPM, 1)&lt;/User-Agent&gt;
			&lt;/header&gt;
			&lt;body&gt;
				&lt;json&gt;
					{
						"username":"{{extrapass3\username}}",
						"password":"{{pmextrapass3}}"
					}
				&lt;/json&gt;
			&lt;/body&gt;
		&lt;/request&gt;
	&lt;/RequestWrapper&gt;
	&lt;RequestWrapper&gt;
		&lt;request name="GetUserByID"&gt;
			&lt;version&gt;2&lt;/version&gt;
			&lt;general&gt;
				&lt;endpoint&gt;/api/v1/private/users&lt;/endpoint&gt;
				&lt;method&gt;GET&lt;/method&gt;
			&lt;/general&gt;
			&lt;header&gt;
				&lt;Content-Type&gt;application/json&lt;/Content-Type&gt;
				&lt;Accept&gt;application/json&lt;/Accept&gt;
				&lt;User-Agent&gt;Integration/1.0 (CyberArk, CPM, 1)&lt;/User-Agent&gt;
				&lt;Authorization&gt;Bearer {{parsed/token}}&lt;/Authorization&gt;
			&lt;/header&gt;
			&lt;body&gt;
				&lt;json&gt;&lt;/json&gt;
			&lt;/body&gt;
		&lt;/request&gt;
	&lt;/RequestWrapper&gt;
	&lt;RequestWrapper&gt;
		&lt;request name="GetUserProfileByID"&gt;
			&lt;version&gt;2&lt;/version&gt;
			&lt;general&gt;
				&lt;endpoint&gt;/api/v1/private/users/{{parsed/id}}&lt;/endpoint&gt;
				&lt;method&gt;GET&lt;/method&gt;
			&lt;/general&gt;
			&lt;header&gt;
				&lt;Content-Type&gt;application/json&lt;/Content-Type&gt;
				&lt;Accept&gt;application/json&lt;/Accept&gt;
				&lt;User-Agent&gt;Integration/1.0 (CyberArk, CPM, 1)&lt;/User-Agent&gt;
				&lt;Authorization&gt;Bearer {{parsed/token}}&lt;/Authorization&gt;
			&lt;/header&gt;
			&lt;body&gt;
				&lt;json&gt;&lt;/json&gt;
			&lt;/body&gt;
		&lt;/request&gt;
	&lt;/RequestWrapper&gt;
	&lt;RequestWrapper&gt;
		&lt;request name="RecChangePassword"&gt;
			&lt;version&gt;2&lt;/version&gt;
			&lt;general&gt;
				&lt;endpoint&gt;/api/v1/private/user/{{parsed/id}}&lt;/endpoint&gt;
				&lt;method&gt;PUT&lt;/method&gt;
			&lt;/general&gt;
			&lt;header&gt;
				&lt;Content-Type&gt;application/json&lt;/Content-Type&gt;
				&lt;Accept&gt;application/json&lt;/Accept&gt;
				&lt;User-Agent&gt;Integration/1.0 (CyberArk, CPM, 1)&lt;/User-Agent&gt;
				&lt;Authorization&gt;Bearer {{parsed/token}}&lt;/Authorization&gt;
			&lt;/header&gt;
			&lt;body&gt;
				&lt;json&gt;
				{	
					&lt;!-- No quotes because it needs to be treated as a bool --&gt;
					"active": {{parsed/active}},
					"defaultLanguage": "{{parsed/defaultLanguage}}",
					"firstName": "{{parsed/firstName}}",			
					&lt;!-- The value for the groups key will be a string representation of the groups array. --&gt;
					"groups:": {{parsed/groups}},
					"id": "{{parsed/id}}",
					"lastName": "{{parsed/lastName}}",
					"password": "{{pmnewpass}}",
					"repeatPassword": "{{pmnewpass}}",
					"store": "{{parsed/merchant}}",
					"emailAddress": "{{parsed/emailAddress}}",
					"userName": "{{parsed/userName}}"
				}			
				&lt;/json&gt;
			&lt;/body&gt;
		&lt;/request&gt;
	&lt;/RequestWrapper&gt;
&lt;/Requests&gt;
</code></pre>
<p>The <code>RecGetToken</code> request is like <code>GetToken</code> but uses the linked reconcile account's username and password. The <code>GetUserByID</code> and <code>GetUserProfileByID</code> requests facilitate retrieving the needed details from the user's profile while <code>RecChangePassword</code> is used to change the password, including the parsed details.</p>
<p>The <code>reconcilepass</code> Chain will have five links:</p>
<pre><code class="language-xml">&lt;Chains&gt;
	&lt;Chain name="reconcilepass"&gt;
		&lt;Link name="ReconcilePasswordGetTokenLink" request="RecGetToken"&gt;
			&lt;StatusCode value="200" next="GetUserByIDLink"&gt;
				&lt;Parse name="parsed/token" type="json" value="token" secure="true" /&gt;
			&lt;/StatusCode&gt;
			&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
			&lt;StatusCode value="401" next="FAILUnauthorizedCurrPass"/&gt;
			&lt;StatusCode value="403" next="FAILForbidden"/&gt;
			&lt;StatusCode value="404" next="FAILNotFound"/&gt;
			&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
			&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
			&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
			&lt;StatusCode value="501" next="FAILWebServer"/&gt;
			&lt;StatusCode value="*" next="FAILGeneral"/&gt;
		&lt;/Link&gt;
		&lt;Link name="GetUserByIDLink" request="GetUserByID"&gt;
			&lt;StatusCode value="200" next="ByParse" condition="OR" nomatch="FAILToParseRecUsername"&gt;
				&lt;Parse name="parsed/id" type="text" value="&amp;quot;id&amp;quot;:(\d+)(?=[^}]*&amp;quot;userName&amp;quot;:&amp;quot;{{username}}&amp;quot;)"&gt;
					&lt;Equals value="Success" next="END"/&gt;
					&lt;Equals value="*" next="GetUserProfileByIDLink"/&gt;
				&lt;/Parse&gt;
			&lt;/StatusCode&gt;
			&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
			&lt;StatusCode value="401" next="FAILUnauthorized"/&gt;
			&lt;StatusCode value="403" next="FAILForbidden"/&gt;
			&lt;StatusCode value="404" next="FAILNotFound"/&gt;
			&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
			&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
			&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
			&lt;StatusCode value="501" next="FAILWebServer"/&gt;
			&lt;StatusCode value="*" next="FAILGeneral"/&gt;
		&lt;/Link&gt;
		&lt;Link name="GetUserProfileByIDLink" request="GetUserProfileByID"&gt;
			&lt;StatusCode value="200" next="ReconcilePasswordLink"&gt;
				&lt;!-- Uses method=lowercase as we later treat it as a bool. Works with at least v12.5.7.3 of WSChains.dll--&gt;
				&lt;Parse name="parsed/active" type="json" value="active" method="lowercase"/&gt;
				&lt;Parse name="parsed/defaultLanguage" type="json" value="defaultLanguage" /&gt;
				&lt;Parse name="parsed/emailAddress" type="json" value="emailAddress" /&gt;
				&lt;Parse name="parsed/firstName" type="json" value="firstName" /&gt;
				&lt;!-- Use regex to get the value of the groups key as when we parse it as type json, it cannot convert an Array to string. --&gt;
				&lt;Parse name="parsed/groups" type="text" value="&amp;quot;groups&amp;quot;:(\[[^\]]+\])" /&gt;
				&lt;Parse name="parsed/lastName" type="json" value="lastName" /&gt;
				&lt;Parse name="parsed/merchant" type="json" value="merchant" /&gt;
				&lt;Parse name="parsed/username" type="json" value="userName" /&gt;
			&lt;/StatusCode&gt;
			&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
			&lt;StatusCode value="401" next="FAILUnauthorized"/&gt;
			&lt;StatusCode value="403" next="FAILForbidden"/&gt;
			&lt;StatusCode value="404" next="FAILNotFound"/&gt;
			&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
			&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
			&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
			&lt;StatusCode value="501" next="FAILWebServer"/&gt;
			&lt;StatusCode value="*" next="FAILGeneral"/&gt;
		&lt;/Link&gt;
		&lt;Link name="ReconcilePasswordLink" request="RecChangePassword"&gt;
			&lt;StatusCode value="200" next="RecVerifyNewPassLink"/&gt;
			&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
			&lt;StatusCode value="401" next="FAILUnauthorized"/&gt;
			&lt;StatusCode value="403" next="FAILForbidden"/&gt;
			&lt;StatusCode value="404" next="FAILNotFound"/&gt;
			&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
			&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
			&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
			&lt;StatusCode value="501" next="FAILWebServer"/&gt;
			&lt;StatusCode value="*" next="FAILGeneral"/&gt;
		&lt;/Link&gt;
		&lt;Link name="RecVerifyNewPassLink" request="GetTokenWithNewPass"&gt;
			&lt;StatusCode value="200" next="END"&gt;
				&lt;Parse name="parsed/tokennew" type="json" value="token" secure="true"/&gt;
			&lt;/StatusCode&gt;
			&lt;StatusCode value="400" next="FAILBadRequest"/&gt;
			&lt;StatusCode value="401" next="FAILUnauthorizedNewPass"/&gt;
			&lt;StatusCode value="403" next="FAILForbidden"/&gt;
			&lt;StatusCode value="404" next="FAILNotFound"/&gt;
			&lt;StatusCode value="405" next="FAILMethodNotFound"/&gt;
			&lt;StatusCode value="409" next="FAILRequestConflict"/&gt;
			&lt;StatusCode value="500" next="FAILInternalServerError"/&gt;
			&lt;StatusCode value="501" next="FAILWebServer"/&gt;
			&lt;StatusCode value="*" next="FAILGeneral"/&gt;
		&lt;/Link&gt;
	&lt;/Chain&gt;
&lt;/Chains&gt;
</code></pre>
<p>The heavy lifting is done in <code>GetUserByIDLink</code> and <code>GetUserProfileByIDLink</code>.</p>
<p>Regular expression is used to get the <code>id</code> of the user we are trying to reconcile in <code>GetUserByIDLink</code>. In <code>GetUserProfileByIDLink</code>, we can easily parse all the details except for the groups as this is a JSON array and again we use regular expression to grab the array as a string.</p>
<p>Again, our testing validates the chain.</p>
<pre><code class="language-powershell">PS C:\Program Files (x86)\CyberArk\Password Manager&gt; .\bin\CANetPluginInvoker.exe shopizerwschains.ini reconcilepass Cyberark.Extensions.Plugin.WSChains.dll True
The plugin ended successfully (RC = 0)
</code></pre>
<h1>Conclusion</h1>
<p>WSChains is a great framework and our CPM plugin uses only a small amount of its features. I previously <a href="https://timschindler.blog/exploring-cyberark-central-policy-managers-wschains#heading-wrap-up">said it being unsupported by CyberArk makes it unsuitable for production use</a> but I no longer agree with that statement. The maturity of WSChains and the functionality of it makes it a no-brainer for developing CPM plugins that interact with REST APIs.</p>
<p>The platform files, Chain file, and parameter file can be found in my <a href="https://github.com/aaearon/cyberark-shopizer-admin-extensions">GitHub repository</a>. If you are interested in developing your own CPM plugin for Shopizer administrator users, you can use <a href="https://github.com/aaearon/cyberark-shopizer-admin-extensions/blob/main/shopizer/docker-compose.yml">this docker-compose.ymlto quickly stand up a c</a>ontainerized Shopizer environment. Check out the <a href="https://github.com/aaearon/cyberark-shopizer-admin-extensions/tree/main#standing-up-your-own-shopizer-environment-with-docker-compose">README</a> for more information. The same GitHub repository contains the platform files, Chain file, and a WSChains DLL.</p>
]]></content:encoded></item><item><title><![CDATA[Integrating CyberArk (now Idira) self-hosted PAM as a Secret Store in StrongDM]]></title><description><![CDATA[StrongDM bills itself as a Zero Trust PAM solution. It offers a pretty slick way to access resources through a Desktop client as well as a CLI. With either installed, you can easily connect to resourc]]></description><link>https://timschindler.blog/integrating-cyberark-self-hosted-pam-as-a-secret-store-in-strongdm</link><guid isPermaLink="true">https://timschindler.blog/integrating-cyberark-self-hosted-pam-as-a-secret-store-in-strongdm</guid><category><![CDATA[strongdm]]></category><category><![CDATA[cyberark]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[secrets]]></category><category><![CDATA[privileged-session-manager]]></category><category><![CDATA[Privileged access management]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Sun, 26 May 2024 16:44:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716741811680/1d40237d-704e-4977-a78f-574f3a7bf9e6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://www.strongdm.com/">StrongDM</a> bills itself as a Zero Trust PAM solution. It offers a pretty slick way to access resources through a <a href="https://www.strongdm.com/docs/desktop/">Desktop client</a> as well as a <a href="https://www.strongdm.com/docs/cli/">CLI</a>. With either installed, you can easily connect to resources using native tooling. It is a nice alternative to the Privileged Session Manager (PSM).</p>
<p>The credentials for accessing resources can be stored in StrongDM's Strong Vault (a "simple" <a href="https://www.strongdm.com/docs/admin/secret-stores/">secret store</a>) or StrongDM can integrate with various third party stores such as CyberArk — now Idira — (self-hosted PAM and Conjur), AWS Secrets Manager, Azure Key Vault, and a few more.</p>
<p>This is a quick article on integrating CyberArk self-hosted PAM as a secret store in StrongDM.</p>
<h1>Reading through the documentation</h1>
<p>StrongDM provides <a href="https://www.strongdm.com/docs/admin/secret-stores/cyberark-pam/">documentation for integrating CyberArk self-hosted PAM as a secret store</a> but it is largely incomplete, and in one spot even incorrect, but depending on your familiarity with CyberArk, you can read between the lines as to what you need to do.</p>
<p>The first is <a href="https://www.strongdm.com/docs/admin/secret-stores/cyberark-pam/#configure-cyberark-pam">creating the necessary users in CyberArk and configuring the gateways and relays to use them</a>.</p>
<blockquote>
<p>First, any gateway(s) and relay(s) that you intend to use to access resources with via CyberArk PAM must be configured to authenticate with CyberArk. Due to the manner in which CyberArk identifies users and manages seats, each of those gateways and relays must be set up in CyberArk as a user with its own credentials. Once this is done, those gateways and relays are capable of authenticating to CyberArk in order to fetch the required credentials to connect a user to a protected resource.</p>
<p>On the gateway or relay, set the environment variables <code>PAM_USERNAME</code> and <code>PAM_PASSWORD</code> with the user’s corresponding credentials as the value. You can also set <code>PAM_TLS_SKIP_VERIFY=true</code> to skip certificate verification if the CyberArk instance doesn’t have a valid certificate.</p>
</blockquote>
<p>Without explicitly saying it, we can safely assume the integration uses the Password Vault Web Access (PVWA) REST API.</p>
<p>The justification for needing a CyberArk user per gateway or relay isn't clear to me but this requirement could also be in part due to not setting the <a href="https://docs.cyberark.com/pam-self-hosted/latest/en/Content/SDK/CyberArk%20Authentication%20-%20Logon_v10.htm?tocpath=Developer%7CREST%20APIs%7CAuthentication%7CLogon%7C_____1#Bodyparameters"><code>concurrentSession</code> parameter to <code>True</code> when calling the Logon endpoint.</a></p>
<p>The documentation doesn't mention how the CyberArk user authenticates. What is also missing are the needed safe memberships the users must have but they can already be assumed based on the nature of the integration.</p>
<p>Afterwards we <a href="https://www.strongdm.com/docs/admin/secret-stores/cyberark-pam/#set-up-the-secret-store-in-strongdm">set up the Secret Store in StrongDM</a>.</p>
<blockquote>
<ol>
<li><p>In the Admin UI, go to <strong>Network</strong> &gt; <strong>Secret Stores</strong>.</p>
</li>
<li><p>Click <strong>Add secret store</strong>.</p>
</li>
<li><p>Give the secret store a name that is recognizable within your organization, such as “CyberArk PAM.”</p>
</li>
<li><p>Choose <strong>CyberArk PAM</strong> as the secret store type.</p>
</li>
<li><p>For the application URL, enter the IP address of the Windows server that hosts your Central Credential Provider</p>
</li>
</ol>
</blockquote>
<p>Straight forward however step 5 looks to be incorrect. As we need to create CyberArk users for each gateway and relay and set the <code>PAM_USERNAME</code> and <code>PAM_PASSWORD</code> environmental variables, it must mean the PVWA and not the Central Credential Provider. <em>Note: I submitted a case to StrongDM support about CCP vs PVWA and they've already replied they are passing it on to the Docs team.</em></p>
<p>There is no option to choose how the gateways and relays should authenticate with their CyberArk user so it likely only works with the users doing Vault authentication.</p>
<p>The last part is <a href="https://www.strongdm.com/docs/admin/secret-stores/cyberark-pam/#connect-to-a-strongdm-resource">configuring a StrongDM resource to use an account in CyberArk</a>. The example below uses a database.</p>
<blockquote>
<ol>
<li><p>From the Admin UI, go to <strong>Infrastructure &gt; Datasources</strong>.</p>
</li>
<li><p>Click <strong>Add datasource</strong>.</p>
</li>
<li><p>Enter the properties for your database resource.</p>
</li>
<li><p>From the <strong>Secret Store</strong> dropdown menu, select the <strong>CyberArk PAM</strong> option.</p>
</li>
<li><p>In the <strong>Username (path)</strong> field, add the path to retrieve the username, in the format <code>&lt;ACCOUNT_ID&gt;?key=username</code>. Use the CyberArk account ID of your resource, followed by <code>?key=username</code>.</p>
</li>
<li><p>In the <strong>Private Key (path)</strong> field, add the path to retrieve the password, in the format <code>&lt;ACCOUNT_ID&gt;?key=password</code>. Use the CyberArk account ID of your resource, followed by <code>?key=password</code>.</p>
</li>
<li><p>When all required fields are complete, click <strong>Create</strong>.</p>
</li>
</ol>
</blockquote>
<p>The need to use an account ID cements the fact that the integration is using the PVWA REST API. An account ID is a construct only used in the PVWA.</p>
<h1>Integrating CyberArk self-hosted PAM</h1>
<p>The documentation is enough to get us started and we can fill in the gaps along the way.</p>
<p>As my lab environment only has a single gateway, I create a single CyberArk Vault user named <code>strongdm</code>. Despite the documentation saying nothing about safe memberships, we know the user needs <em>something</em> and <code>List accounts</code> together with <code>Retrieve accounts</code> on the safes containing accounts we want to use with StrongDM resources makes the most sense to start with.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716726034459/02d60a6f-c66b-4f25-bab9-6429a251b29e.png" alt="Screenshot of the strongdm user's permissions on a safe" style="display:block;margin:0 auto" />

<p>I <a href="https://www.strongdm.com/docs/admin/nodes/linux/">created a gateway on an existing Linux server</a> in my lab and need to set the <code>PAM_USERNAME</code> and <code>PAM_PASSWORD</code> <a href="https://www.strongdm.com/docs/admin/deployment/environment-variables/">environmental variables</a>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716726362482/8de2d3f2-085e-4ce7-8932-c320508d8d96.png" alt="Screenshot of the sdm-proxy file where you set environmental variables" style="display:block;margin:0 auto" />

<p><code>PAM_TLS_SKIP_VERIFY</code> is set to true as my PVWA's certificate's issuer is not trusted by my Linux server.</p>
<p>Adding CyberArk as a Secret Store is effortless. We just need to keep in mind that, despite the instructions, we provide the URL of the PVWA and not a Central Credential Provider instance. We also do not need to add the <code>/PasswordVault/</code> path.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716726497971/298826f1-2fb9-49d6-be02-a8eb8cd27edd.png" alt="Picture of adding CyberArk PAM as a secret store" style="display:block;margin:0 auto" />

<p>Once the secret store is created, it is marked as healthy as it is reachable by at least one gateway.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716726559199/edd619ad-9ea0-4dd6-8ae1-ec0183fb34ff.png" alt="The secret store showing as healthy after adding it" style="display:block;margin:0 auto" />

<p>If we needed any more proof, we can check the IIS logs. <code>192.168.0.4</code> is the Linux server with the StrongDM gateway installed.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716726706259/e39ba08c-4919-4bf4-912f-6a4f4e4bc7e0.png" alt="" style="display:block;margin:0 auto" />

<p>We can follow the steps for configuring a StrongDM resource to use an account stored in CyberArk self-hosted PAM quite literally. We just need the ID of the account we want to use.</p>
<p>A browser's developer tools is the quickest way to find an account's ID but if we needed to automate the adding of StrongDM resources using CyberArk accounts, the <a href="https://docs.cyberark.com/pam-self-hosted/Latest/en/Content/SDK/GetAccounts.htm?tocpath=Developer%7CREST%20APIs%7CAccounts%7C_____1">Get Accounts endpoint</a> would be more appropriate.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716727077534/413b13b7-4ea7-4263-8f7c-463a08dbfc77.png" alt="Developer tools in a browser with an account's ID shown as part of a network request" style="display:block;margin:0 auto" />

<p>With the account ID in hand, adding the resource looks like the following:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716727456225/0183754b-5f3c-47e8-bbc7-6d7ae332322c.png" alt="Adding a server in StrongDM. The secret store, username, and path are circled." style="display:block;margin:0 auto" />

<p>After resource creation, it is marked as healthy for our Linux gateway.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716728272719/9123272d-d34f-4e94-840a-430973e0c394.png" alt="The server/resource showing as healthy after adding it" style="display:block;margin:0 auto" />

<p>In activities for the account, we see the retrieval by the <code>strongdm</code> user with the reason "StrongDM Connection".</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716728374634/1263f975-e2f0-47f2-9428-fe448c593617.png" alt="A screenshot of the PVWA showing that the strongdm user retrieved the password for the account whose ID was specified when the server was added." style="display:block;margin:0 auto" />

<p>And for the ultimate test: a screenshot of the recording connecting through StrongDM.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716729501912/cb472518-0908-48b0-8a97-dea50dd887ba.png" alt="A screenshot of a StrongDM recording showing that strongdmadmin01 was used to authenticate to server01.timschindler.dev" style="display:block;margin:0 auto" />

<p>Even with the documentation for the integration not being complete, it was still pretty quick and easy.</p>
<h1>Closing thoughts</h1>
<p>For what it is, StrongDM's CyberArk self-hosted PAM integration works nicely. A few thoughts:</p>
<ul>
<li><p>Being that it works with the PVWA REST API, it doesn't require any Central Credential Provider or Credential Provider licenses, which -- together with removing the need for any PSMs and their associated RDS CAL costs -- can be a nice cost savings.</p>
</li>
<li><p>Having to use the ID of an account can make the initial creation of a resource a bit of a pain but with how fast connecting to a resource with StrongDM is, even when StrongDM needs to retrieve the password from the PVWA, it makes up for it.</p>
</li>
<li><p>If the accounts you intend to use with StrongDM resources will also be used by users for password retrieval or PSM sessions in CyberArk, you need to be mindful of the platforms having exclusive usage active as StrongDM will not check-in an account after a StrongDM connection closes.</p>
</li>
<li><p>Don't forget to provide <code>Access Safe without confirmation</code> in addition to <code>List accounts</code> and <code>Retrieve accounts</code> to StrongDM's CyberArk users when accounts have dual control enforced.</p>
</li>
<li><p>Central Credential Provider integration would be a nice option. StrongDM does not seem to retrieve the password for an account for each connection so you could run the CP without <a href="https://docs.cyberark.com/credential-providers/latest/en/Content/CP%20and%20ASCP/cps_capacity-best-practices.htm#CredentialProvidercapacity">cache in order to not hit the 10,000 mark</a>. To avoid the CP having access to more than 50,000 accounts, a gateway or relay could use a CyberArk Application that has access to a certain scope of accounts.</p>
</li>
<li><p>Expanding support to CyberArk's Privilege Cloud would be a big plus as together with StrongDM it could greatly reduce the amount of infrastructure you need to manage for a PAM solution.</p>
</li>
</ul>
<p>Let me know your experience with integrating CyberArk self-hosted PAM and StrongDM.</p>
]]></content:encoded></item><item><title><![CDATA[Creating a CyberArk (now Idira) Central Policy Manager plugin for a web application]]></title><description><![CDATA[In a previous article, we developed a Microsoft Edge-based CyberArk (now Idira) Privileged Session Manager connection component for the web application Shopizer and with the knowledge we gained, we wi]]></description><link>https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-a-web-application</link><guid isPermaLink="true">https://timschindler.blog/creating-a-cyberark-central-policy-manager-plugin-for-a-web-application</guid><category><![CDATA[central-policy-manager]]></category><category><![CDATA[cyberark]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Sat, 30 Dec 2023 16:46:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/FnA5pAzqhMM/upload/08e002a937737ad34fb2bdd0b4a96614.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In <a href="https://timschindler.blog/creating-a-cyberark-privileged-session-manager-connection-component-for-a-web-application">a previous article, we developed a Microsoft Edge-based CyberArk (now Idira) Privileged Session Manager connection component for the web application Shopizer</a> and with the knowledge we gained, we will create <a href="https://docs.cyberark.com/PAS/Latest/en/Content/Plugins/CPM_WebApplication.htm">a Central Policy Manager plugin for a web application</a> to manage the password of Shopizer administrator users.</p>
<p>Note: You can find the platform files for the Shopizer administrator user CPM plugin at <a href="https://github.com/aaearon/cyberark-shopizer-admin-extensions">https://github.com/aaearon/cyberark-shopizer-admin-extensions</a>, including a <a href="https://github.com/aaearon/cyberark-shopizer-admin-extensions/blob/main/shopizer/docker-compose.yml"><code>docker-compose.yml</code></a> file that can be used to quickly stand up your own Shopizer environment.</p>
<div>
<div>💡</div>
<div>This is article is one in a <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://timschindler.blog/series/shopizer-admin-ext" style="pointer-events:none">series of articles around creating PSM connectors and CPM plug-ins</a>.</div>
</div>

<h1>Understanding what a CPM plugin for a web app is doing</h1>
<p>A CPM plugin for a web application works fundamentally the same as a PSM connection component for a web application.</p>
<p>The <a href="https://cyberark.my.site.com/mplace/s/#a352J000000WU3aQAG-a392J0000013WwCQAU">Web Application CPM Plugin Framework</a> uses a WebDriver to identify and interact with elements in a web page's DOM based on a set of commands that represent the verify, change, and reconcile operations. We <a href="https://timschindler.blog/creating-a-cyberark-privileged-session-manager-connection-component-for-a-web-application#heading-understanding-what-a-psm-connection-component-for-a-web-app-is-doing">already covered WebDrivers and the DOM in more depth</a> when we created a PSM connection component.</p>
<p>But instead of using the <code>WebFormFields</code> property, we use <code>WebFormFieldsFile</code> -- an <code>.ini</code> file -- that will hold the commands for each CPM operation. This file will live in the <code>bin</code> folder of the Central Policy Manager and the CPM plugin framework will parse and perform the commands contained.</p>
<h2>A closer look at a <code>WebFormFieldsFile</code></h2>
<p>With an out of the box installation of the CPM there is no <code>WebFormFieldsFile</code> in the <code>bin</code> folder but <a href="https://docs.cyberark.com/PAS/Latest/en/Content/Plugins/CPM_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7C_____4#Configureplatformsforwebapplications">the documentation</a> tells us what it looks like (step 6): an <code>.ini</code> file with three sections -- verify, change, and reconcile.</p>
<pre><code class="language-ini">[Verify]
# Verify commands go here, i.e. username &gt; {username} (searchby=name)
[Change]
# Change commands go here
[Reconcile]
# Reconcile commands go here
</code></pre>
<p>The .<code>ini</code> file can be named anything so we want to make sure it is something unique and meaningful.</p>
<h1>Developing our Shopizer CPM plugin</h1>
<p>We <a href="https://timschindler.blog/creating-a-cyberark-privileged-session-manager-connection-component-for-a-web-application#heading-identifying-elements-in-the-dom">already understand how to identify and select the best DOM elements</a> so we can jump right in with creating our CPM plugin. The <a href="https://docs.cyberark.com/PAS/Latest/en/Content/Plugins/CPM_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7C_____4#Configureplatformsforwebapplications">Create CPM plugins for Web applications documentation</a> has pretty explicit instructions for creating the platform.</p>
<h2>Setting the stage</h2>
<p>Before creating the platform, we will create two administrator users in Shopizer for the purpose of development: <code>shopizeradministrator@timschindler.dev</code> and <code>reconcile@timschindler.dev</code> -- both with full administrator rights. The <code>shopizeradministrator</code> user will be the shared user and <code>reconcile</code> will serve as the reconciliation account for it.</p>
<h2>Creating a new web application platform</h2>
<p>Picking up at step 5 of <a href="https://docs.cyberark.com/PAS/Latest/en/Content/Plugins/CPM_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7C_____4#Configureplatformsforwebapplications">the documentation for configuring platforms for web applications</a>, we need to define <code>VerifyURL</code>, <code>ChangeURL</code>, <code>ReconcileURL</code>, and <code>WebFormFieldsFile</code> for our newly created platform.</p>
<p>The <code>VerifyURL</code>, <code>ChangeURL</code>, and <code>ReconcileURL</code> parameters function the same as to that of <code>LogonURL</code> for PSM connection components for web apps: before executing the commands in the <code>WebFormFieldsFile</code>, the framework navigates to the URL for the relevant section. We can use dynamic parameters corresponding to account properties as part of the parameters, too (<code>{Address}</code>, <code>{Port}</code>, etc.)</p>
<p>The <code>WebFormFieldsFile</code> property will hold the name of the file with all the operations' commands. For our CPM plugin used to manage Shopizer administrators, we will name the file <code>ShopizerAdministrator.ini</code>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703686127363/3128e9be-b07a-411f-8945-4aceef83678e.png" alt="A screenshot of the platform settings in the PVWA showing the WebFormFieldsFile and it's value of ShopizerAdministrator.ini" style="display:block;margin:0 auto" />

<p>We will keep the default values for the parameters, changing only <code>BrowserPath</code> and adding the parameter <code>Browser</code> with a value of <code>Edge</code> to reflect the fact we will use Microsoft Edge. We set <code>EnforceCertificate</code> to <code>No</code> as our Shopizer development environment does not have a valid TLS certificate (but we would want to change this to <code>Yes</code> for production.) We will leave the <code>VerifyURL</code>, <code>ChangeURL</code>, and <code>ReconcileURL</code> parameters for the time being as we will set them at same time we define the commands for the CPM operations in <code>ShopizerAdministrator.ini</code>. Our last change will be defining the value for the <code>Port</code> parameter as <code>4200</code> -- the port our Shopizer administrator portal is running as -- under <code>Additional Policy Settings</code>.</p>
<p>With our (incomplete) platform created, we can onboard our two Shopizer accounts into CyberArk.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703754826283/a65d4fa2-c15e-4007-b58f-b6350dca50ab.png" alt="A screenshot of the PVWA with the shopizeradministrator@timschindler.dev and reconcile@timschindler.dev accounts onboarded" style="display:block;margin:0 auto" />

<h1>Crafting <code>ShopizerAdministrator.ini</code></h1>
<p>For each operation, we need to determine:</p>
<ol>
<li><p>All the DOM elements that need to be interacted with to perform the operation. We need these for the commands.</p>
</li>
<li><p>The appropriate URL to start the commands at for the particular operation (<code>VerifyURL</code>, <code>ChangeURL</code>, and <code>ReconcileURL</code>.)</p>
</li>
</ol>
<p>Important to note: Unlike a PSM connection component where we can disable validations, CPM plugins for web applications require at least one validation per operation so that the CPM knows if it was successful or not.</p>
<h2>Verify section</h2>
<p>As a verify operation is simply logging into the website with the vaulted credentials, we can simply <a href="https://timschindler.blog/creating-a-cyberark-privileged-session-manager-connection-component-for-a-web-application#heading-defining-our-webformfields">re-use what we already have from our Shopizer PSM connection component</a> plus add the required validation.</p>
<p>This results in our <code>ShopizerAdministrator.ini</code> file looking like:</p>
<pre><code class="language-ini">[Verify]
username&gt;{Username} (SearchBy=name)
password&gt;{Password} (SearchBy=name)
//button[@type="submit"]&gt;(Button) (SearchBy=XPath)
Informations sur le magasin&gt;(Validation) (SearchBy=text)
Store information&gt;(Validation) (SearchBy=text)
</code></pre>
<p>Note that, unlike the <code>WebFormFields</code> for a PSM connection component, we must not escape any brackets. And because the default Shopizer language is French, we search for the text 'Store information' or 'Informations sur le magasin' to validate we have logged in successfully.</p>
<p>We can also use the same value for <code>LogonURL</code> that we used in our PSM connection component for <code>VerifyURL</code> in our platform however we will not hard code the port and instead use a dynamic parameter, resulting in the final value being <code>http://{Address}:{Port}/#/auth</code> .</p>
<p>Clicking Verify in the PVWA, we see a successful verify operation.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703758494289/aa5f62ed-f5b1-47fb-81a8-943a1765ec8b.png" alt="A screenshot of the shopizeradministrator@timschindler.dev account details in the PVWA showing Verify was successful" style="display:block;margin:0 auto" />

<h2>Change section</h2>
<p>For a Shopizer administrator user to change their own password, they need to:</p>
<ol>
<li><p>Login to the Shopizer administrator portal.</p>
</li>
<li><p>Navigate to their profile.</p>
</li>
<li><p>Click Change Password from the pulldown.</p>
</li>
<li><p>Enter the current password, the new password, the new password again, and hit save.</p>
</li>
</ol>
<p>To simplify the commands in our Change section, we will use <code>http://{Address}:{Port}/#/auth</code> as <code>ChangeURL</code> and then use the <code>Navigate</code> command to have the browser go directly to where we can change the password, <code>http://{Address}:{Port}/#/pages/user-management/change-password</code>.</p>
<p>This results in our Change section looking like:</p>
<pre><code class="language-ini">[Change]
username&gt;{Username} (SearchBy=name)
password&gt;{Password} (SearchBy=name)
//button[@type="submit"]&gt;(Button) (SearchBy=XPath)
Informations sur le magasin&gt;(Validation) (SearchBy=text)
Store information&gt;(Validation) (SearchBy=text)
(Wait=1)
(Navigate=http://{Address}:{Port}/#/pages/user-management/change-password)
password&gt;{password} (SearchBy=id)
newPassword&gt;{newpassword} (SearchBy=id)
confirmNewPassword&gt;{newpassword} (SearchBy=id)
//button[text()=' Save ' or text()=' Sauvegarder ']&gt;(Click) (SearchBy=xpath)
correctement&gt;(Validation) (SearchBy=text)
Password successfully changed&gt;(Validation) (SearchBy=text)
</code></pre>
<p>There is a lot going on so lets break it apart.</p>
<pre><code class="language-ini">username&gt;{Username} (SearchBy=name)
password&gt;{Password} (SearchBy=name)
//button[@type="submit"]&gt;(Button) (SearchBy=XPath)
Informations sur le magasin&gt;(Validation) (SearchBy=text)
Store information&gt;(Validation) (SearchBy=text)
</code></pre>
<p>The first lines are a copy and paste of our Verify section as we need to login first.</p>
<pre><code class="language-ini">(Wait=1)
(Navigate=http://{Address}:{Port}/#/pages/user-management/change-password)
</code></pre>
<p>We use a <code>Wait</code> to break the validation context and then use <code>Navigate</code> to go directly to the profile page where we can change the password.</p>
<pre><code class="language-ini">password&gt;{password} (SearchBy=id)
newPassword&gt;{newpassword} (SearchBy=id)
confirmNewPassword&gt;{newpassword} (SearchBy=id)
//button[text()=' Save ' or text()=' Sauvegarder ']&gt;(Click) (SearchBy=xpath)
</code></pre>
<p>After entering the current password and the new password twice we search for a button that has either the text <code>Save</code> or <code>Sauvegarder</code> , which is the Save button for changing the password, because the site may load in either English or French.</p>
<pre><code class="language-ini">correctement&gt;(Validation) (SearchBy=text)
Password successfully changed&gt;(Validation) (SearchBy=text)
</code></pre>
<p>Our validations cover the <a href="https://en.wikipedia.org/wiki/Pop-up_notification">Toast notification</a> that briefly pops up when the password is changed successfully. What isn't visible in our <code>WebFormFieldsFile</code> is a necessary tweak to the <code>ActionTimeout</code> value.</p>
<p><code>ActionTimeout</code> determines how long to wait for a command or an action to complete. When the command is clicking on a button, the framework expects the button to disappear from the page as part of the action completing. The framework will wait up to the value of <code>ActionTimeout</code> before proceeding with the next command.</p>
<p>Because a successful change does not result in the Save button disappearing and the Toast disappears after 3 or 4 seconds, we have to adjust <code>ActionTimeout</code> in our platform to <code>2</code> so that the validations can catch the Toasts before they are gone.</p>
<p>The account's password is rotated after clicking Change in the PVWA.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703843020210/a4354486-ab8a-4e09-84cc-7392f70c7e63.png" alt="A screenshot of the shopizeradministrator@timschindler.dev account details in the PVWA showing Change was successful" style="display:block;margin:0 auto" />

<h2>Reconcile section</h2>
<p>Reconcile will be similar to change in that for Shopizer administrators to change the password for another user, they need to:</p>
<ol>
<li><p>Login to the Shopizer administrator portal.</p>
</li>
<li><p>Navigate to the Users list.</p>
</li>
<li><p>Find the user in the list and edit their details.</p>
</li>
<li><p>Enter the new password, repeat the new password again, and hit Save.</p>
</li>
</ol>
<p>Like the Change section, our Reconcile section will use <code>http://{Address}:{Port}/#/auth</code> as <code>ReconcileURL</code> and then use <code>Navigate</code> to navigate directly to the Users list, <code>http://{Address}:{Port}/#/pages/user-management/users</code>.</p>
<p>Our Reconcile section results in:</p>
<pre><code class="language-ini">[Reconcile]
username&gt;{reconcileaccount\username} (SearchBy=name)
password&gt;{reconcileaccount\password} (SearchBy=name)
//button[@type="submit"]&gt;(Button) (SearchBy=XPath)
Informations sur le magasin&gt;(Validation) (SearchBy=text)
Store information&gt;(Validation) (SearchBy=text)
(Wait=1)
(Navigate=http://{Address}:{Port}/#/pages/user-management/users)
//tbody//tr//td[3]//div[text()="{username}"]/ancestor::tr/td[5]//i&gt;(Click) (Searchby=XPath)
password&gt;{newpassword} (SearchBy=id)
repeatPassword&gt;{newpassword} (SearchBy=id)
//button[text()=' Save ' or text()=' Sauvegarder ']&gt;(Click) (SearchBy=xpath)
utilisateur&gt;(Validation) (SearchBy=text)
User updated&gt;(Validation) (SearchBy=text)
</code></pre>
<p>Like for Change, lets break it down into manageable pieces:</p>
<pre><code class="language-ini">username&gt;{reconcileaccount\username} (SearchBy=name)
password&gt;{reconcileaccount\password} (SearchBy=name)
//button[@type="submit"]&gt;(Button) (SearchBy=XPath)
Informations sur le magasin&gt;(Validation) (SearchBy=text)
Store information&gt;(Validation) (SearchBy=text)
</code></pre>
<p>The first lines are a copy and paste of our Verify and Change sections as however we need to login with the reconcile account so we use <code>{reconcileaccount\username}</code> and <code>{reconcileaccount\password}</code> instead of <code>{username}</code> and <code>{password}</code>.</p>
<pre><code class="language-ini">(Wait=1)
(Navigate=http://{Address}:{Port}/#/pages/user-management/users)
</code></pre>
<p>Again, we use a <code>Wait</code> to break the validation context and then use <code>Navigate</code> to go directly to the Users list.</p>
<pre><code class="language-ini">//tbody//tr//td[3]//div[text()="{username}"]/ancestor::tr/td[5]//i&gt;(Click) (Searchby=XPath)
</code></pre>
<p>This is the most complex part of our CPM plugin.</p>
<p>The XPath expression identifies the Edit user "button" (in actuality it is an image) used to navigate to a user's details by looking for a <code>&lt;div&gt;</code> element with text content that matches the account's username within the third column (<code>&lt;td&gt;)</code> of a table row in the User's list table. Then, it goes up to the parent row (<code>&lt;tr&gt;</code>) and selects the fifth column (<code>&lt;td&gt;</code>), and finally, it searches for an <code>&lt;i&gt;</code> element anywhere within that fifth column (<code>&lt;td&gt;</code>).</p>
<p>Once found, it clicks on the image to navigate to that user's details page where the password can be updated.</p>
<pre><code class="language-ini">password&gt;{newpassword} (SearchBy=id)
repeatPassword&gt;{newpassword} (SearchBy=id)
//button[text()=' Save ' or text()=' Sauvegarder ']&gt;(Click) (SearchBy=xpath)
</code></pre>
<p>The new password is entered twice and the Save button is clicked.</p>
<pre><code class="language-ini">utilisateur&gt;(Validation) (SearchBy=text)
User updated&gt;(Validation) (SearchBy=text)
</code></pre>
<p>Similar to changing a password, once we click Save, a toast pops up informing us that the user is updated. As <code>ActionTimeout</code> is already set to <code>2</code>, the framework spots the notification and the reconciliation should be considered successful.</p>
<p>Let's give it a shot.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703930882385/ccfdab27-7274-4f68-9d49-0ec128787c3d.png" alt="A screenshot of the error message in the PVWA received when trying to Reconcile shopizeradministrator@timschindler.dev" style="display:block;margin:0 auto" />

<p>Unlike our Verify and Change operations, Reconcile isn't working on the first attempt.</p>
<p>The error message says it cannot press on the Save button, which is different from being unable to find the button. Digging in the Debug logs, we see the following error:</p>
<p><code>30/12/2023 11:06:47.438 | ERROR -&gt; WebElementWrapper :: Click -&gt; Failed to press on button. Exception Details: OpenQA.Selenium.ElementClickInterceptedException: element click intercepted: Element &lt;button _ngcontent-dex-c30="" class="success_button appearance-filled size-medium status-basic shape-rectangle nb-transition" nbbutton="" nbspinnersize="large" nbspinnerstatus="control" type="button" _nghost-dex-c4="" aria-disabled="false" tabindex="0"&gt;...&lt;/button&gt; is not clickable at point (1036, 20). Other element would receive the click: &lt;div _ngcontent-dex-c16="" class="user-container"&gt;...&lt;/div&gt; (Session info: MicrosoftEdge=120.0.2210.91)</code></p>
<p>The significant part is at the end: <code>Other element would receive the click: &lt;div _ngcontent-dex-c16="" class="user-container"&gt;...&lt;/div&gt;</code> .</p>
<p>The framework is telling us that it could not click the Save button that it found at an earlier position because now another element is on top of it.</p>
<p>At this point, we need to see how the framework is interacting with the page and unlike with PSM connection components, we cannot easily flip on switch to see the operations (<code>EnableTrace=yes</code>) but we can test the plugin manually without having to invoke an operation in CyberArk.</p>
<h1>Troubleshooting Reconcile by testing the CPM plugin</h1>
<p>Testing a CPM plugin is already <a href="https://docs.cyberark.com/PAS/Latest/en/Content/Plugins/CPM_WebApplication.htm#Step3Testtheplugin">well documented by CyberArk</a>. As we already have our <code>WebFormFieldsFile</code>, we just need to create a <code>user.ini</code> file that will hold the parameters sent to the CPM plugin as part of our testing.</p>
<p>Our <code>user.ini</code> file -- which I've named <code>shopizeruser.ini</code> as there is no requirement around the filename -- looks like:</p>
<pre><code class="language-ini">[targetaccount]
username=shopizeradministrator@timschindler.dev
newpassword=Password2
password=Password1
safename=Safename
foldername=Foldername
objectname=Objectname
PolicyID=Policyid
Address=192.168.178.61
Port=4200

[extrapass3]
username=reconcile@timschindler.dev
password=Password1

[extrainfo]
VerifyURL=http://{Address}:{Port}/#/auth
ChangeURL=http://{Address}:{Port}/#/auth
ReconcileURL=http://{Address}:{Port}/#/auth
WebFormFieldsFile=ShopizerAdministrator.ini
RunVerifyAfterChange=No
RunVerifyAfterReconcile=No
ActionTimout=2
PageLoadTimeout=30
EnforceCertificate=No
Debug=Yes
BrowserPath=C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe
Browser=Edge
</code></pre>
<p>The <code>targetaccount</code> section holds parameters related to the account we are testing with, <code>extrapass3</code> has parameters relating to a reconcile account, and <code>extrainfo</code> has the same parameters and values as defined in our platform.</p>
<p>Note: The values for the <code>safename</code>, <code>foldername</code>, <code>objectname</code>, and <code>policyid</code> parameters are irrelevant for our testing but are required as they are used to form the name of the log file found in the <code>Logs\ThirdParty</code> folder.</p>
<p>With <code>shopizeruser.ini</code> saved to the root of the <code>Password Manager</code> folder, we can test the CPM plugin. Let's start with an operation we know works -- verify:</p>
<p><code>.\bin\CANetPluginInvoker.exe shopizeruser.ini verifypass CyberArk.Extensions.Plugin.WebApp.dll True</code></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703934800377/e56df251-0f97-40c5-9146-6f04762d1cb9.gif" alt="A video showing that the Verify operation was successful when manually tested" style="display:block;margin:0 auto" />

<p>The plugin ends with error code 0, which is the error code returned when the operation is successful.</p>
<p>We know we have a working test setup so let's proceed with reconcile, with the expectation that it will fail:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703935299131/40dd08e7-e8f9-4983-a4ce-04e392a9825b.gif" alt="A video showing that the Reconcile operation was successful when manually tested" style="display:block;margin:0 auto" />

<p>Reconcile works when we test but not when the CPM invokes the plugin.</p>
<p>It's a nice confirmation that our commands for the CPM plugin are correct but we need to understand why we get different results.</p>
<p>Because the error message we received mentions that another element would receive the click, it's worth opening the User details page in a normal Edge browser and experiencing the page when navigating around it. It is also important to change the browser window size as we are not sure what the window size is when the CPM invokes the plugin.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703936447052/08b40760-493e-4dc0-8468-4d3ce045030d.gif" alt="A video of interacting with the Shopizer administrator portal" style="display:block;margin:0 auto" />

<p>At the resolution we are connected to the CPM with and the browser maximized, at no point is the Save button not visible. It's only when we put the browser in windowed mode is the Save button not visible after we focus on the Password and Repeat Password fields.</p>
<p>Resizing our RDP window to a smaller resolution and testing the plugin once more, we can reproduce the error that we originally received:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703936731734/42fd05c5-8f79-4d77-8e11-92770087f1df.gif" alt="A video showing that the Reconcile operation failed when manually tested" style="display:block;margin:0 auto" />

<p>Checking the Debug log, we see:</p>
<p><code>30/12/2023 12:44:47.066 | ERROR -&gt; WebElementWrapper :: Click -&gt; Failed to press on button. Exception Details: OpenQA.Selenium.ElementClickInterceptedException: element click intercepted: Element &lt;button _ngcontent-bkl-c30="" class="success_button appearance-filled size-medium status-basic shape-rectangle nb-transition" nbbutton="" nbspinnersize="large" nbspinnerstatus="control" type="button" _nghost-bkl-c4="" aria-disabled="false" tabindex="0"&gt;...&lt;/button&gt; is not clickable at point (792, 20). Other element would receive the click: &lt;div _ngcontent-bkl-c16="" class="user-container"&gt;...&lt;/div&gt;</code></p>
<p>The message details differ slightly but it still complains about another element receiving the click. We can reasonably assume that when the framework focuses on the Password and New Password fields to enter text, the browser window no longer shows the Save button because the floating header is covering it.</p>
<p>How can we move the browser window to the top of the page?</p>
<p>With the framework, there is no command we can define in our <code>WebFormFieldsFile</code> to have the browser scroll to the top of the page. Furthermore, we cannot send keys outside of an input field so we could not simply instruct the framework to hit the Home key.</p>
<h1>Fixing the Reconcile operation</h1>
<p>The solution to fixing the Reconcile operation isn't elegant.</p>
<p>We know the browser scrolls to an input field when text is being entered so we need to enter text in the <code>First name</code> field. This will result in the browser 'scrolling up' and the header not covering the Save button.</p>
<p>Because we use <code>Shopizer</code> as the <code>First name</code> for our Shopizer users and the value of this user property is irrelevant, we can have commands to send <code>Shopizer</code> to the <code>First name</code> field before trying to click the Save button. The relevant part of our Reconcile section transforms to:</p>
<pre><code class="language-ini">password&gt;{newpassword} (SearchBy=id)
repeatPassword&gt;{newpassword} (SearchBy=id)
firstname&gt;Shopizer (SearchBy=id)
//button[text()=' Save ' or text()=' Sauvegarder ']&gt;(Click) (SearchBy=xpath)
</code></pre>
<p>Testing our reconcile operation, we see a success:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703937722677/ce41d640-4aee-4059-a1e6-4bcccbe4505f.gif" alt="A video showing that the Reconcile operation was successful when manually tested" style="display:block;margin:0 auto" />

<p>But what matters is if it is successful when the CPM invokes the plugin.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703937906294/7a174f2e-e31d-4e8a-ab70-97af1a35db94.png" alt="A screenshot of the shopizeradministrator@timschindler.dev account details in the PVWA showing Reconcile was successful" style="display:block;margin:0 auto" />

<p>Adding the command to populate the <code>First name</code> field so that the browser would have the Save button in view worked!</p>
<p>Our final Reconcile section is:</p>
<pre><code class="language-ini">[Reconcile]
username&gt;{reconcileaccount\username} (SearchBy=name)
password&gt;{reconcileaccount\password} (SearchBy=name)
//button[@type="submit"]&gt;(Button) (SearchBy=XPath)
Informations sur le magasin&gt;(Validation) (SearchBy=text)
Store information&gt;(Validation) (SearchBy=text)
(Wait=1)
(Navigate=http://{Address}:{Port}/#/pages/user-management/users)
//tbody//tr//td[3]//div[text()="{username}"]/ancestor::tr/td[5]//i&gt;(Click) (Searchby=XPath)
password&gt;{newpassword} (SearchBy=id)
repeatPassword&gt;{newpassword} (SearchBy=id)
firstname&gt;Shopizer (SearchBy=id)
//button[text()=' Save ' or text()=' Sauvegarder ']&gt;(Click) (SearchBy=xpath)
utilisateur&gt;(Validation) (SearchBy=text)
User updated&gt;(Validation) (SearchBy=text)
</code></pre>
<h1>Next steps</h1>
<p>Our CPM plugin for Shopizer administrator users works but there are some improvements that can be made before rolling out to production:</p>
<ol>
<li><p>Each operation should include Failure commands so that meaningful error messages can be shown in the PVWA.</p>
</li>
<li><p>We should change <code>EnforceCertificate</code> to <code>Yes</code> after securing all our Shopizer administrator portals with trusted TLS certificates.</p>
</li>
<li><p>Evaluate if using the Web App framework is the best solution. The Shopizer administrator portal leverages REST APIs and our CPM plugin would be less prone to breaking if we call the endpoints directly.</p>
</li>
</ol>
<p>If you are interested in developing your own CPM plugin for Shopizer administrator users, you can use <a href="https://github.com/aaearon/cyberark-shopizer-admin-extensions/blob/main/shopizer/docker-compose.yml">this docker-compose.yml</a> to quickly stand up a containerized Shopizer environment. Check out the <a href="https://github.com/aaearon/cyberark-shopizer-admin-extensions/tree/main#standing-up-your-own-shopizer-environment-with-docker-compose">README</a> for more information. The same GitHub repository contains the platform files and <code>WebFormFieldsFile</code>.</p>
]]></content:encoded></item><item><title><![CDATA[Generating TOTPs for CyberArk (now Idira) Privileged Session Manager Webapp Connectors using a PreConnect DLL]]></title><description><![CDATA[With version 13.0 of CyberArk (now Idira)'s Privileged Session Manager, as part of a Webapp PSM connector, you can inject a time-based one-time password. You can even store the secret for the TOTP in ]]></description><link>https://timschindler.blog/generating-totps-for-cyberark-privileged-session-manager-webapp-connectors-using-a-preconnect-dll</link><guid isPermaLink="true">https://timschindler.blog/generating-totps-for-cyberark-privileged-session-manager-webapp-connectors-using-a-preconnect-dll</guid><category><![CDATA[cyberark]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[MFA]]></category><category><![CDATA[privileged-session-manager]]></category><category><![CDATA[Privileged access management]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Thu, 12 Oct 2023 15:59:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/BXOXnQ26B7o/upload/06bd070120b8677ef6395e0235ea2fff.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With version 13.0 of CyberArk (now Idira)'s Privileged Session Manager, as part of a Webapp PSM connector, you can inject a time-based one-time password. You can even <a href="https://timschindler.blog/generating-time-based-one-time-passwords-in-privileged-session-manager-webapp-connectors">store the secret for the TOTP in a separate account and link it as a logon account to secure it</a>.</p>
<p>As the latest <a href="https://docs.cyberark.com/EOL/en/Content/EOL-Self-hosted-products.htm#CyberArkPAMSelfHostedVaultPVWACPMPSMPSMforSSHPTA">LTS version is 12.6</a>, many customers are 'stuck' without TOTP injection support but by using a custom <a href="https://docs.cyberark.com/PAS/12.2/en/Content/PASIMP/psm_WebApplication_Preconnect.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors%7CWeb%20applications%20for%20PSM%7C_____1">PreConnect DLL</a> for Webapp PSM connectors -- a feature introduced in PSM version 12.2-- we can achieve the same functionality and have TOTP injection with PSMs as early as version 12.2</p>
<h1>What is a PreConnect DLL?</h1>
<p>A PreConnect DLL is a piece of code that is run before a Webapp PSM connector's <code>WebFormFields</code> is executed. <a href="https://docs.cyberark.com/PAS/12.2/en/Content/PASIMP/psm_WebApplication_Preconnect.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors%7CWeb%20applications%20for%20PSM%7C_____1#Configurethewebapplication">Account properties can be passed to the DLL as parameters and return values can be used in the connector's <code>WebFormFields</code>' input fields.</a> CyberArk provides <a href="https://docs.cyberark.com/PAS/12.2/en/Content/PASIMP/psm_WebApplication_Preconnect.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors%7CWeb%20applications%20for%20PSM%7C_____1#ImplementthePreConnectDLLfile">straightforward documentation</a> on how to develop and configure a PreConnect DLL.</p>
<h1>Creating the PreConnect DLL</h1>
<p>We will keep the PreConnect DLL simple.</p>
<p>It will take a single parameter -- <code>logonaccount_password</code> -- being the TOTP secret, generate a six-digit TOTP using it, and return the TOTP. We will use <code>&amp;MfaCode&amp;</code> to represent the TOTP for use in the <code>WebFormFields</code>.</p>
<p>As version 12.2 of the PSM comes with <a href="https://github.com/kspearrin/Otp.NET">Otp.NET</a> out of the box in the <code>Components</code> folder, we will re-use the library to do the OTP generating and leave our PreConnect DLL to handle just the receiving of the parameter and the returning of the TOTP. With this, we have only a single file we need to distribute to each PSM.</p>
<pre><code class="language-csharp">using CyberArk.PSM.WebAppDispatcher.PreconnectUtils;
using OtpNet;
using System;
using System.Collections.Generic;
using System.Net;
using System.Security;
using System.Text;

namespace CyberArk.PSM.GenerateTOTPPreconnect
{   
    
    public class GenerateTOTP : IPreconnectContract
    {
        private readonly string TOTP_OUTPUT_PARAMETER = "MFACode";

        public Dictionary&lt;string, SecureString&gt; GetParameters(Dictionary&lt;string, SecureString&gt; parameters, LogUtils.WriteToLogHandler writeToLogMethod)
        {
            string totpCode;

            try
            {
                byte[] totpSecretBytes = Encoding.UTF8.GetBytes(new NetworkCredential(string.Empty, parameters["logonaccount_password"]).Password);

                writeToLogMethod("Generating TOTP.", Consts.LOG_LEVEL_INFO);
                
                var totp = new Totp(totpSecretBytes);
                totpCode = totp.ComputeTotp();
            }
            catch (KeyNotFoundException ex)
            {
                writeToLogMethod(string.Format("Key not found in parameters dictionary: {0}", ex.ToString()), Consts.LOG_LEVEL_ERROR);
                throw new PreconnectException("Key not found in parameters dictionary");
            }
            catch (Exception ex)
            {
                writeToLogMethod(string.Format("Error while generating TOTP: {0}", ex.ToString()), Consts.LOG_LEVEL_ERROR);
                throw new PreconnectException("Error while generating TOTP");
            }

            writeToLogMethod("Successfully generated TOTP.", Consts.LOG_LEVEL_INFO);

            return new Dictionary&lt;string, SecureString&gt; {
                {
                    TOTP_OUTPUT_PARAMETER,
                    new NetworkCredential(string.Empty, totpCode).SecurePassword
                }
            };
        }
    }
}
</code></pre>
<i>Note: I am not a C# developer so there is room for improvement!</i>

<p>After compiling the DLL and moving it to the <code>Components</code> folder of our PSM, we are ready to leverage it with the Webapp PSM connector.</p>
<p>You can find the code and Visual Studio project, including <a href="https://github.com/aaearon/CyberArk.PSM.GenerateTOTPPreconnect/releases/">a compiled DLL</a>, in my GitHub repository below:</p>
<p><a href="https://github.com/aaearon/CyberArk.PSM.GenerateTOTPPreconnect">https://github.com/aaearon/CyberArk.PSM.GenerateTOTPPreconnect</a></p>
<h1>Configuring the Webapp PSM Connector</h1>
<div>
<div>💡</div>
<div>This was tested in a PAM self-hosted environment with Vault version 13.0 and PSM 12.2. The 12.2 PSM is the <strong>second PSM</strong> installed, after one running version 13.0.</div>
</div>

<p>With the compiled DLL copied to all PSMs,</p>
<ol>
<li><p>Ensure that the Webapp PSM connector has <code>LogonAccount</code> added under Component Parameters &gt; Target Settings &gt; Supported Capabilities.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697112511393/ecdea847-3a62-459d-8758-27d026757569.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>Add the required parameters <code>PreConnectDllName</code> and <code>PreConnectParameters</code> as described in <a href="https://docs.cyberark.com/PAS/12.2/en/Content/PASIMP/psm_WebApplication_Preconnect.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors%7CWeb%20applications%20for%20PSM%7C_____1#Configurethewebapplication">step three</a>. For <code>PreConnectDllName</code>, specify the name of the DLL (<code>CyberArk.PSM.GenerateTOTPPreconnect.dll</code> if downloaded from my GitHub repository) and for <code>PreConnectParameters</code> provide just <code>logonaccount_password</code>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697112557290/cf71ddcf-f383-4f28-b323-e748abe5614e.png" alt="a screenshot of the PreConnectDllName parameter" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697112580674/f51f78b5-26d2-4841-9dc7-47473d84a600.png" alt="a screenshot of the PreConnectParameters parameter" style="display:block;margin:0 auto" />
</li>
<li><p>In <code>WebFormFields</code>, use <code>&amp;MfaCode&amp;</code> to inject the TOTP in the desired input field.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697112762884/229028a2-6838-4883-88f6-5c77be306695.png" alt="" style="display:block;margin:0 auto" /></li>
</ol>
<p>Connecting with our Webapp PSM connector, the TOTP is generated based on a secret that is stored as the password for the account's linked logon account.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697112416490/ea46cec0-f74c-4159-abd7-deb0f3d124d9.png" alt="a screenshot showing the generated TOTP being injected" style="display:block;margin:0 auto" />

<p>The code and a compiled DLL can be found at <a href="https://github.com/aaearon/CyberArk.PSM.GenerateTOTPPreconnect">https://github.com/aaearon/CyberArk.PSM.GenerateTOTPPreconnect</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Exploring CyberArk (now Idira) Central Policy Manager's WSChains]]></title><description><![CDATA[If you have downloaded newer CPM plug-ins from the CyberArk (now Idira) Marketplace, you may have noticed that some deviate from the typical TPC or C#-based implementations and instead leverage a new ]]></description><link>https://timschindler.blog/exploring-cyberark-central-policy-managers-wschains</link><guid isPermaLink="true">https://timschindler.blog/exploring-cyberark-central-policy-managers-wschains</guid><category><![CDATA[cyberark]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[wschains]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Tue, 29 Aug 2023 19:36:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/K8iHtzoIKQ4/upload/f692feb7eba8c32e71c3ddf7a30a8d07.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you have downloaded newer CPM plug-ins from the CyberArk (now Idira) Marketplace, you may have noticed that some deviate from the typical TPC or C#-based implementations and instead leverage a new WSChains DLL with an accompanying XML file. From the plug-in descriptions and the XML file content, WSChains is used when communicating with a REST API.</p>
<p>Scouring the CyberArk technical community and official documentation results in practically zero information about WSChains but studying the XML files of the Marketplace plug-ins utilizing it provides a fair amount of information about this extremely powerful and flexible plug-in type.</p>
<h1>First look at WSChains</h1>
<p>Like the Secure Web App Framework, WSChains is declarative in nature. It consists of two files: an XML file and the WSChains DLL that acts on it and the contents within.</p>
<p>In the XML, there are four top elements:</p>
<ul>
<li><p>GlobalSettings</p>
</li>
<li><p>Requests</p>
</li>
<li><p>Fails</p>
</li>
<li><p>Chains</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693337186551/40b587a9-9d66-44f4-99ed-7f268e1703c8.png" alt="" style="display:block;margin:0 auto" />

<h2>GlobalSettings</h2>
<p>Of the three plug-ins I looked at, only one had a child <code>GlobalSetting</code> elements in <code>GlobalSettings</code>.</p>
<p>In the <a href="https://cyberark-customers.force.com/mplace/s/#a352J0000003MQpQAM-a392J000002KqxrQAC">Cisco ISE Internal Users via API</a> plug-in, the <code>GlobalSetting</code> element and the comment imply that the <code>Address</code> property of the account is used as the base URL for all the subsequently defined <code>Request</code> elements.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693331980239/9f5fbbef-6f7e-48f9-aeb6-064505587e71.png" alt="" style="display:block;margin:0 auto" />

<h2>Requests</h2>
<p>The <code>Requests</code> section is made up of <code>RequestWrapper</code> child elements with each <code>RequestWrapper</code> element representing an API call that can be made as part of a later-defined chain.</p>
<p>Within each <code>RequestWrapper</code> there is a <code>Request</code> element where the relative URL, HTTP method, headers, and request body can be defined as child elements. Seemingly, the base URL and the relative URL are automatically concatenated to create the full URL.</p>
<p>Account properties -- including those of the linked logon and reconcile accounts -- are available to use inside the elements of the <code>Request</code> element.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693332383760/aa201e1f-6211-4632-9372-066ce035fe22.png" alt="" style="display:block;margin:0 auto" />

<p>The <a href="https://cyberark-customers.force.com/mplace/s/#a352J000000WUKqQAO-a392J0000013eWLQAY">Tenable.io - CPM</a> plug-in shows that the body can contain JSON when inside a <code>json</code> element.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693334512457/c22b6a37-dc74-47f1-91b0-d52241461b43.png" alt="" style="display:block;margin:0 auto" />

<p>When the <code>HTTP POST</code> request sends form data and not <code>json</code>, the <code>Request</code> element can look like:</p>
<pre><code class="language-xml">&lt;request name="PasswordChange"&gt;
	&lt;version&gt;2&lt;/version&gt;
	&lt;general&gt;
		&lt;endpoint&gt;/authentication/users/{{username}}&lt;/endpoint&gt;
		&lt;method&gt;POST&lt;/method&gt;
	&lt;/general&gt;
	&lt;header&gt;
		&lt;Content-Type&gt;application/x-www-form-urlencoded&lt;/Content-Type&gt;
		&lt;Authorization&gt;Basic {{username}}:{{pmpass}}&lt;/Authorization&gt;
	&lt;/header&gt;
	&lt;body&gt;
		&lt;text&gt;
          oldpassword={{pmpass}}&amp;amp;password={{pmnewpass}}
        &lt;/text&gt;
	&lt;/body&gt;
&lt;/request&gt;
</code></pre>
<p>It is important to remember when sending multiple key-value paris in the <code>body</code> element, that the ampersand must be escaped as <code>&amp;amp;</code>!</p>
<h2>Fails</h2>
<p>The <code>Fails</code> section is made up of one or more child <code>Fail</code> elements, which are similar to what you would find in a TPC-based plug-in's Process file.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693332738718/1c8d2820-d78c-4c07-831b-d2772bed635e.png" alt="" style="display:block;margin:0 auto" />

<h2>Chains</h2>
<p>The <code>Chains</code> section is where it all comes together.</p>
<p>Each CPM operation has its own <code>Chain</code> element consisting of one or more child <code>Link</code> elements. Each <code>Link</code> element has <code>name</code> and <code>request</code> attributes, where the <code>request</code> attribute's value is the name of a <code>Request</code> element to be executed.</p>
<p><code>Link</code> elements contain child <code>StatusCode</code> elements, each with a <code>value</code> attribute whose value corresponds to a HTTP status code and a <code>next</code> attribute's value that corresponds to the name of a <code>Fail</code> element or a sibling <code>Link</code> element in the same <code>Chain</code> to be executed when the HTTP status code is matched.</p>
<p>In the case that the value for <code>next</code> is <code>END</code>, similar to a Process file, the operation is considered to have been completed successfully.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693335436466/e991657a-fdf0-4936-858c-037e640b8c2e.png" alt="" style="display:block;margin:0 auto" />

<h3>StatusCode</h3>
<p>The <code>StatusCode</code> elements are particularly interesting.</p>
<p>The content returned in a response's body can be parsed and stored as a variable to be used in <code>Request</code> elements, such as in the case where a session token must first be generated and used as part of subsequent requests.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693335606201/d5d70579-8f0a-4546-9873-e32d73bd5763.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693335693321/60315a9a-df24-489f-a581-828f9c26b96c.png" alt="" />

<p>In the case that the next <code>Link</code> depends not just on the HTTP status code returned but also on the content of a response's body, <code>StatusCode</code> elements can have child <code>Parse</code> elements that in turn have child <code>Equals</code> elements. The <code>Equals</code> elements have <code>next</code> attributes that operate the same as they do with <code>StatusCode</code> elements.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693335976356/9491decc-c7d1-4591-a5df-2773eed07977.png" alt="" style="display:block;margin:0 auto" />

<p><code>Equals</code> elements are also powerful as the <a href="https://cyberark-customers.force.com/mplace/s/#a35Ht000001VhyWIAS-a39Ht0000032Gw5IAE">Hitachi Administrator Link CS</a> plug-in shows that they can be used to query an asynchronous job's status.</p>
<pre><code class="language-xml">&lt;Equals value="*" incrementcounter="PollJobCounter" maxcounter="{{maxretries}}"  maxcounterreached="FAILStorageOnboardingFailedChange" sleep="{{sleepseconds}}" next="CheckJobIdStatusLink" /&gt;
</code></pre>
<h1>Wrap-up</h1>
<p>With an understanding of CPM plug-ins and REST APIs, it is relatively easy to create your own WSChains-based plug-ins using existing plug-ins as references, even without official documentation. However, since it appears to be an internal-use-only framework for CyberArk, it is not suitable for use in a production setting.</p>
<p>Hopefully, CyberArk will choose to support WSChains in the same way they do TPC and the Web App Frameworks, and provide official documentation. WSChains is powerful in its current state and could potentially replace the need for C# or TPC-based plug-ins developed to interact with REST APIs.</p>
]]></content:encoded></item><item><title><![CDATA[Creating a Universal iDRAC CyberArk (now Idira) PSM Connector with if-else Conditional Statements]]></title><description><![CDATA[With the version 13.2 release of the Secure Web Application Connectors Framework for the CyberArk (now Idira) Privileged Session Manager, if-else conditional statements can be used in WebForm fields w]]></description><link>https://timschindler.blog/creating-a-universal-idrac-cyberark-psm-connector-with-if-else-conditional-statements</link><guid isPermaLink="true">https://timschindler.blog/creating-a-universal-idrac-cyberark-psm-connector-with-if-else-conditional-statements</guid><category><![CDATA[cyberark]]></category><category><![CDATA[privileged-session-manager]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[dell]]></category><category><![CDATA[idrac]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Sun, 09 Jul 2023 20:38:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/1kvdplxe_pA/upload/f467456a5fb7a32a253dc22524ac6719.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With the version 13.2 release of the <a href="https://cyberark-customers.force.com/mplace/s/#a3550000000EiCMAA0-a3950000000jjUwAAI">Secure Web Application Connectors Framework</a> for the CyberArk (now Idira) Privileged Session Manager, <code>if-else</code> conditional statements can be used in WebForm fields which can be used to build more resilient connectors and allow a single PSM connector to be used for different versions of an application that have different web elements.</p>
<p>Using <code>if-else</code> conditional statements, we will build a PSM connector that will cover both iDRAC versions 8 and 9.</p>
<div>
<div>💡</div>
<div>Version 13.2 of the <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://cyberark-customers.force.com/mplace/s/#a352J000000WU3aQAG-a392J0000013WwCQAU" style="pointer-events:none">Web Application CPM Plugin Framework</a> offers the same functionality.</div>
</div>

<h1>The Framework's Conditional Statements</h1>
<p>The <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PASIMP/psm_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors%7C_____2#Webformfields">documentation for conditional statements in the Secure Web Application Connectors Framework</a> is pretty comprehensive, if not a bit overwhelming in parts.</p>
<p>It offers the standard conditional statements found in other programming languages (<code>if</code>, <code>else-if</code>, <code>else</code>) as well as conditional operators (<code>or</code>, <code>and</code>) that can be used between conditions.</p>
<p>It allows the following actions:</p>
<ul>
<li><p>Exists - verifies if an element exists on the page with operators <code>true</code> and <code>false</code>.</p>
</li>
<li><p>Count - counts how many elements exist on the page with operators <code>eq</code>, <code>ne</code>, <code>gt</code>,<code>ge</code>, <code>lt</code>, and <code>le</code>.</p>
</li>
<li><p>Placeholder - allows for usage of account properties or <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PASIMP/psm_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors%7C_____2#Preconnectcustomcode">Preconnect</a> return values with the single operator <code>eq</code>.</p>
</li>
</ul>
<p>Using a conditional statement in <code>WebFormFields</code> would look something like:</p>
<pre><code class="language-ini">if ( (Login &gt; (Condition) (searchby=text) (exists eq true)) )
    username &gt; {Username}
    password &gt; {password}
    submit &gt; (Click)
if-end
</code></pre>
<p><code>(Condition)</code> is similar to <code>(Click)</code>, <code>(Validation)</code>, and <code>{accountProperty}</code> in that it informs the Framework on how to interpret the statement.</p>
<h1>Creating a Universal iDRAC PSM Connector</h1>
<p>A Universal iDRAC PSM connector makes for a simple example of using conditional statements but is also practical as iDRACs can be upgraded without CyberArk administrators being informed and while an account can be assigned a platform with PSM connectors for all possible iDRAC versions, it may not be known to the end user which one to use.</p>
<p>Our Universal iDRAC PSM Connector will work for versions 8 and 9 and due to the nature of iDRACs (being a part of enterprise-grade, physical hardware), you may not have all or any of the versions available so we will use publically exposed (!) iDRAC interfaces:</p>
<ul>
<li><p>iDRAC8 -&gt; <a href="https://103.215.176.114/">https://103.215.176.114/</a></p>
</li>
<li><p>iDRAC9 -&gt; <a href="https://www.deos.lr.tudelft.nl/restgui/start.html">https://</a><a href="http://www.deos.lr.tudelft.nl">www.deos.lr.tudelft.nl</a></p>
</li>
</ul>
<p>These are not iDRACs we have access to and therefore we do not have valid credentials for but it is enough for us to build our PSM connector. We will know if our Universal iDRAC connector works or not because we will not receive any messages about elements not being found but rather error messages that the login or password is invalid.</p>
<h2>Connectors for each version iDRAC version</h2>
<p>As the point is to use conditional statements, we will assume we already have a functioning PSM connector for both iDRAC versions and that all we need to do is to combine them into a single one.</p>
<p>If you don't have working PSM connectors for each version, you can <a href="https://cyberark-customers.force.com/mplace/s/#--Dell-CyberArk_Solution__c-PrivilegedSessionManagement">import from the Marketplace</a> or use the following WebFormFields (taken from the Marketplace connectors minus validations) when creating your own:</p>
<h3>iDRAC8</h3>
<pre><code class="language-ini">user &gt; {username}(searchby=id)
password &gt; {password}(searchby=id)
submit_lbl &gt; (Button)(searchby=id)
</code></pre>
<h3>iDRAC9</h3>
<pre><code class="language-ini">username &gt; {username}(searchby=name)
(wait=2)
password &gt; (Button)(searchby=name)
password &gt; {password}(searchby=name)
cux-button &gt; (Button)(searchby=class)
</code></pre>
<h1>Creating the Universal iDRAC Connector</h1>
<p>Before defining our WebFormFields using the <code>if-else</code> statements, we need to determine the logic we will use.</p>
<p>As we are only considering two versions, we need only a single <code>if</code> statement to identify a specific iDRAC version and an <code>else</code> statement to handle the other. We only need to develop a condition for the <code>if</code> statement as <code>else</code> does not take one.</p>
<p>Version 8 of iDRAC uses a lot of JavaScript that updates the DOM and elements may not be found by the Framework so we will build a condition for our <code>if</code> statement that identifies version 9 of iDRAC and <code>if</code> the condition is not met, assume version 8 in our <code>else</code> section.</p>
<p>In addition, versions 8 and 9 of iDRAC have different paths for their login forms (8: <code>/login.html</code>, 9: <code>/restgui/start.html</code> ) so when the <code>if</code> condition is not met and the version is 8, we need to first navigate to the correct path in our <code>else</code> section.</p>
<p>With all the above considered, the WebFormFields for our Universal iDRAC connector looks like:</p>
<pre><code class="language-ini">if ( (Integrated Dell Remote Access Controller 9 &gt; (Condition) (searchby=text) (exists eq true)) )
    username &gt; {username}(searchby=name)
    (wait=2)
    password &gt; (Button)(searchby=name)
    password &gt; {password}(searchby=name)
    cux-button &gt; (Button)(searchby=class)
end-if
else
    (Navigate=https://{address}/login.html)
    user &gt; {username}(searchby=id)
    password &gt; {password}(searchby=id)
    submit_lbl &gt; (Button)(searchby=id)
end-else
</code></pre>
<p>The condition for our <code>if</code> statement is simple: we search for the text <code>Integrated Dell Remote Access Controller 9</code> and if found, treat it as iDRAC version 9. If not, treat it as version 8 but navigate to the version 8-specific login form first.</p>
<p>After cloning the <code>PSM-DellDRAC9</code> PSM connector, setting a unique PSM connector ID and appropriate connector display name, <code>RunValidations</code> to <code>No</code> under Client Specific Target Settings and <code>EnforceCertificateValidation</code> to <code>No</code> in Web Form Settings, we can set the above as the <code>WebFormFields</code> value.</p>
<h1>Testing the Universal iDRAC Connector</h1>
<p>I've onboarded two accounts both with the username and password <code>test</code> but with different addresses: <code>103.215.176.114</code> (iDRAC8) and <code>www.deos.lr.tudelft.nl</code> (iDRAC9). Both have the same platform with only our Universal iDRAC Connector.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688933716096/03be101e-274d-4aff-ab59-8031cf059e7a.png" alt="" style="display:block;margin:0 auto" />

<p>Connecting to <code>103.215.176.114</code>, the result is an iDRAC error message indicating the username and password are invalid -- a success in our case!</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688933971153/c9cfd83c-7580-443b-97ef-240c486c5192.png" alt="" style="display:block;margin:0 auto" />

<p>And with <code>www.deos.lr.tudelft.nl</code> is the same situation.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688933985669/78f15b61-ee26-44de-9d76-cc08e0e1cc3d.png" alt="" style="display:block;margin:0 auto" />

<p>The conditional statements introduced as part of version 13.2 PSM and CPM web application Frameworks are a nice addition. They provide flexibility that is both beneficial to CyberArk administrators and end users.</p>
<div>
<div>💡</div>
<div>Looking for more content around CyberArk PSM Web application connectors? Check out my other post <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://timschindler.blog/creating-a-cyberark-privileged-session-manager-connection-component-for-a-web-application" style="pointer-events:none">Creating a CyberArk Privileged Session Manager connection component for a web application</a>.</div>
</div>]]></content:encoded></item><item><title><![CDATA[Generating time-based one-time passwords in Privileged Session Manager WebApp Connectors]]></title><description><![CDATA[Edit: After conversing with a contact, this requires version 13.0 of the PSM no matter the version of the Secure Web Application Connectors Framework!
Edit 2: This is not officially supported but ther]]></description><link>https://timschindler.blog/generating-time-based-one-time-passwords-in-privileged-session-manager-webapp-connectors</link><guid isPermaLink="true">https://timschindler.blog/generating-time-based-one-time-passwords-in-privileged-session-manager-webapp-connectors</guid><category><![CDATA[cyberark]]></category><category><![CDATA[MFA]]></category><category><![CDATA[privileged-session-manager]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Sat, 10 Jun 2023 20:40:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/UAvYasdkzq8/upload/09d5cd67c210cf9d7a3a6d4998de787b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Edit</strong>: After conversing with a contact, this requires version 13.0 of the PSM no matter the version of the Secure Web Application Connectors Framework!</p>
<p><strong>Edit 2</strong>: This is not officially supported but there exists <a href="https://cyberark-customers.force.com/s/article/Support-automatic-TOTP-injection-through-PSM-3ee5-a70">an enhancement request you can vote on</a>.</p>
<p>Like possible with CyberArk (now Idira) Central Policy Manager plugins for Web applications, you can generate <a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">time-based one-time passwords</a> (TOTP) as part of a Privileged Session Manager (PSM) connector.</p>
<p>The <a href="https://cyberark-customers.force.com/mplace/s/#a352J000000WU3aQAG-a392J0000013WwCQAU">Web Application CPM Plugin Framework</a> enables you to <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/13.0/en/Content/Plugins/CPM_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7C_____4&amp;highlight=MFA%20code#WebFormFields">generate a TOTP as part of the WebFormFields</a> but this functionality is not advertised for PSM connectors as part of its <a href="https://cyberark.my.site.com/mplace/s/#a3550000000EiCMAA0-a3950000000jjUwAAI">Secure Web Application Connectors Framework</a>. Despite this, it is still possible.</p>
<p>Furthermore, we can generate a TOTP with a secret stored as a Logon Account instead of storing the secret as part of the PSM connector's WebFormFields.</p>
<h1>Generating a TOTP in WebFormFields</h1>
<p>Generating a TOTP for a Webapp PSM connector is done the same way as with a CPM plugin for a Webapp. You can copy and paste right out of Example 3 in the Web Form Fields section of the <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/13.0/en/Content/Plugins/CPM_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7C_____4#WebFormFields">CPM plugin for Web applications documentation</a>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686426520121/c459b6c4-2aaa-408a-8b08-380d6f925045.png" alt="" style="display:block;margin:0 auto" />

<p>Launching the PSM connector, the OTP is generated and injected successfully:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686426633892/b7bef5e4-90cd-4f3f-8599-57b20e53fa1a.png" alt="" style="display:block;margin:0 auto" />

<p>But hardcoding the TOTP secret as shown in the example doesn't make sense as the secret will be different for each account and the secret itself should be kept as secure as the password.</p>
<h1>The TOTP secret as a Logon Account</h1>
<p>The best way to securely store the TOTP secret is to onboard it as an account. Afterward, we will use that account as the Logon Account from which we will retrieve the TOTP secret.</p>
<p>The account the TOTP secret is onboarded with can have any platform, address, username, etc. CPM management can be disabled unless you have a CPM plugin that can rotate the TOTP secret itself.</p>
<p>After linking the TOTP secret as a Logon Account, we can tweak our <code>WebFormFields</code> to generate the TOTP using the Logon Account's password using the <code>{logonaccount\password}</code> property instead of a hardcoded value:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686427678662/33cd5519-95b4-4578-b173-056c4acb8961.png" alt="" style="display:block;margin:0 auto" />

<p>Before we test our new PSM connector, we need to add <code>LogonAccount</code> to the Supported Capabilities under the Target Settings of the PSM connector:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686427978870/689fc6a0-192a-4fb1-abdc-b18ec397f9a3.png" alt="" style="display:block;margin:0 auto" />

<p>If we neglect to add <code>LogonAccount</code>, then the Logon Account's properties and password will not be available to the PSM connector and we receive an error:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686428135483/5d18727a-5a97-4651-b67e-1f67139bc22f.png" alt="" style="display:block;margin:0 auto" />

<p>With the <code>WebFormFields</code> updated and the capability added, the PSM connector launches, generates a TOTP based on the secret stored in the password of the Logon Account, and injects it successfully:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686428358229/387c19a0-bc09-421e-b919-cb937139f5ef.png" alt="" style="display:block;margin:0 auto" />

<p>We see the username of the Logon Account in the details of the PSM Connect activity:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686428656605/522b3393-7632-46b7-bf9d-0384653d915c.png" alt="" style="display:block;margin:0 auto" />

<p>And we see a Connect in the activities of the Logon Account:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1686428692063/d19f1549-8ad4-492e-9d6a-aa3cc09fb810.png" alt="" style="display:block;margin:0 auto" />

<p>It's not clear why this PSM connector functionality is undocumented by CyberArk. Maybe it is not supported or the documentation was simply overlooked?</p>
<p>Either way, just like generating a TOTP as part of a CPM plugin may be needed in some scenarios, the same goes for PSM connectors.</p>
]]></content:encoded></item><item><title><![CDATA[Solution to Network Connectivity Issues after the Hardening of the CyberArk (now Idira) PAM self-hosted's Vault on Virtualized Hardware]]></title><description><![CDATA[The CyberArk (now Idira) PAM self-hosted Vault is not typically installed on virtualized hardware but some cases make the scenario justified -- one of them being when standing up a lab environment.
Wh]]></description><link>https://timschindler.blog/solution-to-network-connectivity-issues-after-the-hardening-of-the-cyberark-pam-self-hosteds-vault-on-virtualized-hardware</link><guid isPermaLink="true">https://timschindler.blog/solution-to-network-connectivity-issues-after-the-hardening-of-the-cyberark-pam-self-hosteds-vault-on-virtualized-hardware</guid><category><![CDATA[cyberark]]></category><category><![CDATA[Vault]]></category><category><![CDATA[firewall]]></category><category><![CDATA[windows defender firewall]]></category><category><![CDATA[Security]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Fri, 05 May 2023 17:12:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/40XgDxBfYXM/upload/91b006ea54c39b1597d0426e5555995c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The CyberArk (now Idira) PAM self-hosted Vault is not typically installed on virtualized hardware but some cases make the scenario justified -- <a href="https://timschindler.blog/effortlessly-setting-up-a-cyberark-pam-self-hosted-lab-with-automatedlab">one of them being when standing up a lab environment</a>.</p>
<p>When using Hyper-V (and potentially other hypervisors), after hardening the Vault during the installation process, the Vault may lose network connectivity upon subsequent reboot. This results in the inability to establish inbound or outbound connections for all services.</p>
<p>This issue arises because the Windows Firewall rules created by the CyberArk Vault servers do not take effect, necessitating minor adjustments to two registry keys to resolve the problem.</p>
<h1>Determining if your Vault is impacted</h1>
<p>The easiest way to tell if you are being impacted by this is by checking the Vault machine's network connectivity. This can be done using <a href="https://learn.microsoft.com/en-us/powershell/module/nettcpip/test-netconnection?view=windowsserver2019-ps"><code>Test-NetConnection</code></a> and seeing that outbound connections to all services such as Active Directory and the NTP server fail.</p>
<p>Another way is checking the Firewall pane under the Monitoring section in the Windows Defender Firewall snap-in.</p>
<p>This pane shows the rules in effect and when the Vault services are running, four rules representing the allowing of outbound and inbound Vault-related traffic should be showing. If it is empty, then there are no effective rules and no inbound or outbound traffic is allowed.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683305255552/e3eee1d8-bc48-4aae-8227-acba449ee291.png" alt="A screenshot of an empty Windows Firewall Defender Firewall pane" style="display:block;margin:0 auto" />

A screenshot of the Firewall pane when there is no network connectivity.

<h1>The Fix</h1>
<p>The fix is straightforward. You simply need to change the two following Windows Defender Firewall-related registry keys from <code>0</code> to <code>1</code>:</p>
<ul>
<li><p><code>HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\WindowsFirewall\PublicProfile\AllowLocalIPsecPolicyMerge</code></p>
</li>
<li><p><code>HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\WindowsFirewall\PublicProfile\AllowLocalPolicyMerge</code></p>
</li>
</ul>
<p>Afterward, reboot the Vault machine. Rules will show up in the Firewall pane and network connectivity will be restored.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683304713006/9e90581f-75b4-4f57-a0ca-42ea146fec7b.png" alt="A screenshot of a populated Windows Firewall Defender Firewall pane" style="display:block;margin:0 auto" />

A screenshot of the Firewall pane when network connectivity is working.

<h1>What is happening as part of the hardening?</h1>
<p>These keys control if locally defined Windows Defender Firewall rules -- such as the ones created by the Vault when the service starts -- should take effect or not and as part of the Vault hardening, these registry keys are explicitly set to <code>0</code> for the Public profile and <code>1</code> for the Private profile. It is likely that the intention is for the Vault to take on the Private profile but is for an unknown reason assuming the Public profile.</p>
<p>Even CyberArk is uncertain about why this occurs, as when the Vault hardening process identifies that the Vaults are in AWS, CyberArk explicitly sets both keys to <code>1</code>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1683305824007/2d8e1ebb-855d-41b5-8086-916ec15c27b2.png" alt="An exerpt from the HardenWindowsFireWall.ps1 script found in Server-Rls-x.x/Hardening/Scripts" style="display:block;margin:0 auto" />

An exerpt from the HardenWindowsFireWall.ps1 script found in Server-Rls-x.x/Hardening/Scripts]]></content:encoded></item><item><title><![CDATA[Effortlessly Setting Up A CyberArk (now Idira) PAM Self-Hosted Lab with AutomatedLab]]></title><description><![CDATA[If you want to learn CyberArk (now Idira) PAM self-hosted without risking your production environment or experiment with different configurations and test scenarios, setting up a personal lab environm]]></description><link>https://timschindler.blog/effortlessly-setting-up-a-cyberark-pam-self-hosted-lab-with-automatedlab</link><guid isPermaLink="true">https://timschindler.blog/effortlessly-setting-up-a-cyberark-pam-self-hosted-lab-with-automatedlab</guid><category><![CDATA[cyberark]]></category><category><![CDATA[Security]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Powershell]]></category><category><![CDATA[automatedlab]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Mon, 24 Apr 2023 06:09:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/qwtCeJ5cLYs/upload/7d3b14233a83ad753ec4dc80e4a4d451.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you want to learn CyberArk (now Idira) PAM self-hosted without risking your production environment or experiment with different configurations and test scenarios, setting up a personal lab environment is a great option. However, installing the Vault and its components can take up valuable time. Fortunately, with <a href="https://automatedlab.org/">AutomatedLab</a> and <a href="https://github.com/aaearon/AutomatedLab.CyberArk">custom CyberArk roles</a>, you can automate the entire process and focus on more important tasks. In this article, you will see how to deploy a CyberArk PAM self-hosted lab environment using AutomatedLab with Active Directory Domain Services.</p>
<h1>What is AutomatedLab?</h1>
<p>"AutomatedLab is a provisioning solution and framework that lets you deploy complex labs on HyperV and Azure with simple PowerShell scripts. It supports all Windows operating systems from 2008 R2 to 2022, some Linux distributions and various products like AD, Exchange, PKI, IIS, etc." [0]</p>
<p>AutomatedLab will provision virtual machines as well as install and configure the operating system. It provides PowerShell cmdlets by which you can easily install your software.</p>
<p>You can assign roles to machines which will result in services like Active Directory Domain Services, Exchange, or IIS being installed. <a href="https://automatedlab.org/en/latest/Wiki/Roles/roles/">AutomatedLab also provides methods for creating your own custom roles</a>.</p>
<p>[0] <a href="https://github.com/AutomatedLab/AutomatedLab">https://github.com/AutomatedLab/AutomatedLab</a></p>
<h1>Deploying CyberArk PAM self-hosted with AutomatedLab</h1>
<p>AutomatedLab has a bit of a learning curve but provides <a href="https://automatedlab.org/en/latest/Wiki/Basic/gettingstarted/">documentation to help you get started</a>. The remainder of this article assumes you have successfully installed your first lab as described in the documentation.</p>
<p>In addition, you need the installation archives from the <a href="https://cyberark-customers.force.com/mplace/s/#software">CyberArk Marketplace</a> as well as Operator/Master keys and a license file. These can be placed anywhere on your local machine but I have stored them under <code>$LabSources\CyberArkInstallFiles</code>.</p>
<h2>Installing the custom CyberArk roles</h2>
<p>Installation is nothing more than copying the folders representing the roles to <code>$LabSources\CustomRoles</code> .</p>
<ol>
<li>Check out the repository at <a href="https://github.com/aaearon/AutomatedLab.CyberArk">https://github.com/aaearon/AutomatedLab.CyberArk</a></li>
</ol>
<p><a class="embed-card" href="https://github.com/aaearon/AutomatedLab.CyberArk">https://github.com/aaearon/AutomatedLab.CyberArk</a></p>

<ol>
<li>Copy all the folders starting with <code>PAM</code> to <code>\(LabSources\CustomRoles</code> . On Windows, unless you changed it during the installation of AutomatedLab, <code>C:\LabSources</code> is used for <code>\)LabSources</code>.</li>
</ol>
<p>The Vault and each component have their folder. <code>PAMCommon</code> contains common functions used among all the roles.</p>
<h2>Creating the lab definition</h2>
<p>We will use Hyper-V running locally and our lab will consist of four machines:</p>
<ul>
<li><p>DC01 - the domain controller with Active Directory Domain Services.</p>
</li>
<li><p>VAULT01 - the CyberArk Vault.</p>
</li>
<li><p>COMP01 - the CyberArk Password Vault Web Access (PVWA) and CyberArk Central Policy Manager (CPM.)</p>
</li>
<li><p>PSM01 - the CyberArk Privileged Session Manager (PSM.)</p>
</li>
</ul>
<p>We will have a switch with a defined address space to segregate our lab from other Hyper-V machines.</p>
<p>First, we create the lab definition, add our switch, and set some defaults so we do not need to repeat ourselves when defining the machines:</p>
<pre><code class="language-powershell">New-LabDefinition -Name PAM -DefaultVirtualizationEngine HyperV -VmPath C:\AutomatedLab-VMs

Add-LabVirtualNetworkDefinition -Name PAM -AddressSpace 192.168.0.0/24

$PSDefaultParameterValues = @{
    'Add-LabMachineDefinition:OperatingSystem'      = 'Windows Server 2019 Datacenter Evaluation (Desktop Experience)'
    'Add-LabMachineDefinition:Memory'               = 4GB
    'Add-LabMachineDefinition:Network'              = 'PAM'
}
</code></pre>
<p>Then define our domain controller with the 'RootDC' role and set the <code>DomainName</code> parameter to a domain name of our choice:</p>
<pre><code class="language-powershell">$DC01MachineProperties = @{
    Name = 'DC01'
    Roles = 'RootDC'
    DomainName = 'acme.corp'
}

Add-LabMachineDefinition @DC01MachineProperties
</code></pre>
<h2>Defining the PAM-self hosted machines</h2>
<p>For each of our CyberArk PAM self-hosted lab machines, we need to define role properties that will be used when installing the CyberArk software and the properties for the machine itself -- where we will specify the custom role as a <code>PostInstallationActivity</code>.</p>
<p>All the roles require the installation archive path to be defined. The Vault necessitates two folders to be specified – one for each of the Master and Operator key sets. The license in XML format is also needed. Furthermore, we can designate the Master and Administrator account passwords, but if we do not, they will both default to <code>Cyberark1</code>.</p>
<p>The other components require at least the Vault's IP address to be defined, and optionally, the username and password of the user for installation. If neither is specified, the default values of <code>Administrator</code> and <code>Cyberark1</code> will be used.</p>
<p>All component servers will have <code>DomainName</code> defined, directing AutomatedLab to join them to our domain.</p>
<p>Our Vault is defined with:</p>
<pre><code class="language-powershell">$PAMVaultRoleProperties = @{
    InstallationArchivePath = 'C:\LabSources\CyberArkInstallFiles\Server-Rls-v13.0.zip'
    OperatorKeysFolder      = 'C:\LabSources\CyberArkInstallFiles\DemoOperatorKeys'
    MasterKeysFolder        = 'C:\LabSources\CyberArkInstallFiles\DemoMasterKeys'
    LicensePath             = 'C:\LabSources\CyberArkInstallFiles\nfr_license.xml'
}
\(PAMVaultRole = Get-LabPostInstallationActivity -CustomRole PAMVault -Properties \)PAMVaultRoleProperties

$PAMVaultMachineProperties = @{
    Name = 'VAULT01'
    IpAddress = '192.168.0.100'
    PostInstallationActivity = $PAMVaultRole
}

Add-LabMachineDefinition @PAMVaultMachineProperties
</code></pre>
<p>COMP01 hosts both the PVWA and CPM and is defined by:</p>
<pre><code class="language-powershell">Add-LabMachineDefinition @PAMVaultMachineProperties

$PAMPvwaRoleProperties = @{
    InstallationArchivePath = 'C:\LabSources\CyberArkInstallFiles\Password Vault Web Access-Rls-v13.0.zip'
    VaultIpAddress          = '192.168.0.100'
}
\(PAMPvwaRole = Get-LabPostInstallationActivity -CustomRole PAMPvwa -Properties \)PAMPvwaRoleProperties

$PAMCpmRoleProperties = @{
    InstallationArchivePath = 'C:\LabSources\CyberArkInstallFiles\Central Policy Manager-Rls-v13.0.zip'
    VaultIpAddress          = '192.168.0.100'
}
\(PAMCpmRole = Get-LabPostInstallationActivity -CustomRole PAMCpm -Properties \)PAMCpmRoleProperties

$COMP01MachineParameters = @{
    Name = 'COMP01'
    DomainName = 'acme.corp'
    PostInstallationActivity = \(PAMPvwaRole,\)PAMCpmRole
}

Add-LabMachineDefinition @COMP01MachineParameters
</code></pre>
<p>Finally, our PSM is:</p>
<pre><code class="language-powershell">$PAMPsmRoleProperties = @{
    InstallationArchivePath = 'C:\LabSources\CyberArkInstallFiles\Privileged Session Manager-Rls-v13.0.1.zip'
    VaultIpAddress          = '192.168.0.100'
}
\(PAMPsmRole = Get-LabPostInstallationActivity -CustomRole PAMPsm -Properties \)PAMPsmRoleProperties

$PSM01MachineParameters = @{
    Name = 'PSM01'
    DomainName = 'acme.corp'
    PostInstallationActivity = $PAMPsmRole
}

Add-LabMachineDefinition @PSM01MachineParameters
</code></pre>
<h2>Installing the lab</h2>
<p>As the Vault hardening removes the ability for AutomatedLab to communicate with the machine over WinRM, we cannot simply invoke <code>Install-Lab</code> rather we need to install in stages.</p>
<p>First, we do everything but the post-installation activities:</p>
<pre><code class="language-powershell">Install-Lab -BaseImages -NetworkSwitches -VMs -Domains -NoValidation
</code></pre>
<p>After, we have use <code>Invoke-LabCommand</code> to install CyberArk PAM-self hosted installed.</p>
<pre><code class="language-powershell"># Perform the Vault installation. The hardening part of the installation will kill the ability to use WinRM so it will timeout and throw an error. We want to ignore that error.
Invoke-LabCommand -ComputerName VAULT01 -PostInstallationActivity -ActivityName 'CyberArk Vault installation' -ErrorAction SilentlyContinue
Invoke-LabCommand -ComputerName COMP01 -PostInstallationActivity -ActivityName 'CyberArk Pvwa and CPM installation'
Invoke-LabCommand -ComputerName PSM01 -PostInstallationActivity -ActivityName 'CyberArk PSM installation'
</code></pre>
<p>The entire lab definition can be found below:</p>
<p><a class="embed-card" href="https://gist.github.com/aaearon/d584a11f09efa21091785b1bf129c6a9">https://gist.github.com/aaearon/d584a11f09efa21091785b1bf129c6a9</a></p>

<p>A little less than two hours later (depending on the resources allocated to each machine), a complete CyberArk PAM self-hosted lab environment is fully operational and ready for use.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682237396652/8598098d-437a-485f-adb8-eccd23576835.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1682237332822/26018cbb-a0ae-4f5d-ad40-c7334713ca1e.png" alt="" style="display:block;margin:0 auto" />

<h1>Elevating our lab</h1>
<p>We did little more than the bare minimum to get our lab environment off the ground however with all the different functionality AutomatedLab provides plus the ability to write custom roles, we can take it much further.</p>
<p>We could have our domain controller serve as <a href="https://github.com/AutomatedLab/AutomatedLab/blob/a30d1de5247b94fb6cda4c56b53e1bb2c8c559c7/Help/Wiki/SampleScripts/Introduction/en-us/09%20Web%20Servers%20with%20SSL%20certs%2C%20Root%20CA%2C%20domain%20joined.md">a certificate authority and then issue a certificate and configure the HTTPS binding in IIS.</a> We could also execute a script that creates safes and onboard accounts after the installation of the components.</p>
]]></content:encoded></item><item><title><![CDATA[Installing and configuring Utimaco's SecurityServer Simulator]]></title><description><![CDATA[In a previous blog where I integrated a HSM with the CyberArk (now Idira) Vault using Utimaco's SecurityServer simulator, I did not touch on at all how to install and configure it but this is arguably]]></description><link>https://timschindler.blog/installing-and-configuring-utimacos-securityserver-simulator</link><guid isPermaLink="true">https://timschindler.blog/installing-and-configuring-utimacos-securityserver-simulator</guid><category><![CDATA[pkcs11]]></category><category><![CDATA[utimaco]]></category><category><![CDATA[cyberark]]></category><category><![CDATA[hsm]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Thu, 03 Nov 2022 20:06:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/y5N2HDwagVw/upload/v1667506052168/N3229-iO9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a <a href="https://timschindler.blog/using-a-hardware-security-module-to-secure-the-cyberark-vaults-server-key">previous blog where I integrated a HSM with the CyberArk (now Idira) Vault using Utimaco's SecurityServer simulator</a>, I did not touch on at all how to install and configure it but this is arguably the toughest part of the integration.</p>
<p>Though the documentation from <a href="https://utimaco.com/">Utimaco</a> is comprehensive, the sheer amount can be overwhelming so in this post I want to show how the Utimaco SecurityServer simulator can installed and configured. We will then configure Utimaco PKCS#11 provider DLL on a client, using a CyberArk Vault as an example.</p>
<h1>Installation and configuration of the SecurityServer simulator</h1>
<h2>Requirements</h2>
<p>You will need:</p>
<ul>
<li><p>The <a href="https://utimaco.com/downloads/free-simulators-and-sdks/securityserver-simulator">Utimaco SecurityServer simulator</a>. You can only download it after you register for an account in the Utimaco portal. As the simulator is meant for commercial evaluation, you should sign up with your business contact details. You may need to wait up to 48 hours until the account is activated.</p>
</li>
<li><p>A Windows server with a Java Development Kit installed with the Zip archive downloaded from the Utimaco portal.</p>
</li>
</ul>
<h2>Installing the simulator</h2>
<p>Installing the simulator is as simple as using the .msi provided in the Zip archive but be sure to choose a <strong>Complete</strong> setup type otherwise the tool needed to initialize the HSM slots will not be installed.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667495470220/yD1m7TVHw.png" alt="image.png" />

<p>After installation, you will have three new icons on your desktop:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667495564508/oxp9M8KIZ.png" alt="image.png" />

<p>We only need the CryptoServer Simulator and the PKCS#11 CryptoServer Administration applications. <strong>If you do not see the PKCS#11 CryptoServer Administration then you did not install the Complete setup type as part of the installation.</strong></p>
<h2>Configuration the provider and initializing the slots</h2>
<p>The simulator is our HSM. Double clicking the CryptoServer Simulator icon starts the simulator and this needs to run continuously. The PKCS#11 CryptoServer Administration tool is only used to initialize the token and slots.</p>
<p>Just like a client uses the PKCS#11 provider DLL to communicate with the HSM, so does the PKCS#11 CryptoServer Administration tool. The PKCS#11 provider DLL uses a configuration file -- <code>cs_pkcs11_R3.cfg</code> -- to know the location of the HSM. We need to change the <code>cs_pkcs11_R3.cfg</code> to point to the simulator running on the same server plus enable some logging for easier troubleshooting.</p>
<p>The provider DLL knows where to find <code>cs_pkcs11_R3.cfg</code> based on the value of the system environmental variable <code>CS_PKCS11_R3_CFG</code>. This is automatically created as part of the installation and the default location is <code>C:\ProgramData\Utimaco\PKCS11_R3\cs_pkcs11_R3.cfg</code>.</p>
<p>Opening <code>C:\ProgramData\Utimaco\PKCS11_R3\cs_pkcs11_R3.cfg</code> we want to change three things: enable the PKCS#11 provider to write logs in the same directory as the configuration file, change the log level to <code>INFO</code>, and define the address of the ‘device’ the provider connects to -- in our case, the locally-running simulator.</p>
<p>Change the following settings to have the following values, uncommenting the settings as necessary:</p>
<ol>
<li><p><code>Logpath = C:/ProgramData/Utimaco/PKCS11_R3</code></p>
</li>
<li><p><code>Logging = 3</code></p>
</li>
<li><p>Under a <code>[CryptoServer]</code> section that is uncommented, set <code>Device = 3001@127.0.0.1</code></p>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667500299430/aRPhhaphm.png" alt="image.png" />

<p>Save the changes.</p>
<p>With the the CryptoServer Simulator running, open the PKCS#11 CryptoServer Administration tool. Ten slots should be shown on the left.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667500462545/K5jHydGjv.png" alt="image.png" />

<p>Click on the first slot, Login/Logout at the top, and click Login Generic. Type in <code>ADMIN</code> as the username, select <code>Keyfile</code> as the authentication token, and define <code>C:\Program Files\Utimaco\SecurityServer\Administration\ADMIN.key</code> as the Keyfile. Do not define a password.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667502244073/GSK_MH5HR.png" alt="image.png" />

<p><strong>NOTE: In the latest versions of the SecurityServer, complexity requirements are being enforced for the SO and User PIN. Use strong passwords as the PINs for both the SO and User.</strong></p>
<p>Once logged in, the token needs to be initialized by setting the SO PIN. Click Slot Management at the top, expand <code>Init Token</code>, and set the SO PIN and Confirm SO PIN and click Init Token. In the Status pane, you will see the message <code>Slot 0000 0000: Token initialized.</code></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667502413917/DtbfmLNU0.png" alt="image.png" />

<p>With the slot's token initialized, we then login with the SO PIN and initialize (another, separate PIN.)</p>
<p>As you can only be logged in with a single user at a time, hit Login/Logout, click the Logout All button, click on Login/Logout once more, and login with the SO PIN. After successful login with the SO PIN, click on Slot Management at the top and expand <code>Init PIN</code>. Define both <code>Normal User PIN</code> and <code>Confirm Normal User PIN</code>. <strong>This is the PIN that will be secured with</strong> <code>CAVaultManager SecureSecretFiles</code> <strong>when defining</strong> <code>HSMPinCode</code> <strong>in the</strong> <code>dbparm.ini</code> <strong>and used clients to access the Server key.</strong> Non-CyberArk Vault clients will also use a PIN when needing to access keys.</p>
<p>After initializing the PIN, log out once more by clicking Login/Logout and then clicking Logout All. Login as <code>User</code> by clicking on the Login/Logout button, expanding Login User, and logging in with the <code>Normal User PIN</code> you defined previously.</p>
<p>After successful authentication with the <code>Normal User PIN</code>, the Object Management button at top is clickable. Any cryptographic information (symmetric/asymmetric keys) will show up as an object in this pane but as we just initialized the PIN, it is empty.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667502914386/T4hUxsm3R.png" alt="image.png" />

<p>We can generate our own keys by clicking Generate. In the case of integration with the CyberArk Vault, we will do it as part of <code>CAVaultManger GenerateKeyOnHSM /ServerKey</code>.</p>
<h1>Setting up the CS PKCS#11 Provider on the CyberArk Vault</h1>
<p>In order to use the HSM from another client, we need the PKCS#11 provider DLL and the configuration file -- just like we have it on the server running the simulator.</p>
<p>With the CyberArk Vault as our client, in order to use the SecurityServer simulator as our HSM, we need to:</p>
<ul>
<li><p>On the server running the SecurityServer simulator, allow inbound traffic on port <code>3001/tcp</code>.</p>
</li>
<li><p>Copy <code>cs_pkcs11_R3.dll</code> to the Vault. This can be found in the Zip archive in the <code>Software\Windows\x86-64\Crypto_APIs\PKCS11_R3\lib\</code> folder.</p>
</li>
<li><p>Copy <code>cs_pkcs11_R3.cfg</code> to the Vault.</p>
</li>
<li><p>Update <code>cs_pkcs11_R3.cfg</code> to reflect the proper paths on the Vault.</p>
</li>
<li><p>Define the system environmental variable <code>CS_PKCS11_R3_CFG</code>.</p>
</li>
</ul>
<h2>Configuring the CyberArk Vault</h2>
<p>On the server running the SecurityServer simulator, add a rule that allows inbound traffic over <code>3001/tcp</code>. In a PowerShell opened as Administrator, run <code>New-NetFirewallRule -DisplayName "SecurityServerSimulator" -Direction Inbound -LocalPort 3001 -Protocol TCP -Action Allow</code>.</p>
<p>On the Vault server, create the <code>HSM</code> folder under <code>C:\Program Files (x86)\PrivateArk\Server</code>. Copy <code>cs_pkcs11_R3.dll</code>, and <code>cs_pkcs11_R3.cfg</code> into <code>C:\Program Files (x86)\PrivateArk\Server\HSM</code>.</p>
<p>Open <code>cs_pkcs11_R3.cfg</code> and set the following:</p>
<ol>
<li><p><code>Logpath = C:\Program Files (x86)\PrivateArk\Server\HSM</code></p>
</li>
<li><p>Under a <code>[CryptoServer]</code> section that is uncommented, set <code>Device = 3001@&lt;ip of the server running the SecurityServer simulator&gt;</code></p>
</li>
<li><p><code>SlotCount = 1</code></p>
</li>
</ol>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667503811990/8Hgdq6_YC.png" alt="image.png" />

<p>Now edit the system environmental variables and add <code>CS_PKCS11_R3_CFG</code> with a value of <code>C:\Program Files (x86)\PrivateArk\Server\HSM\cs_pkcs11_R3.cfg</code></p>
<p>At this point the PKCS#11 provider is configured. The easiest way to test connectivity is <a href="https://timschindler.blog/using-a-hardware-security-module-to-secure-the-cyberark-vaults-server-key#heading-generate-the-new-server-key">trying to generate a Server key with <code>CAVaultManger GenerateKeyOnHSM /ServerKey</code></a> after <a href="https://timschindler.blog/using-a-hardware-security-module-to-secure-the-cyberark-vaults-server-key#heading-initial-vault-configurations">performing the initial Vault configurations for HSM integration</a>.</p>
<h1>Closing notes</h1>
<ul>
<li><p>If you are having trouble, do not forget to look in the <code>cs_pkcs11_R3.log</code> file. Together with error messages returned by the client using the PKCS#11 provider DLL, the log contains useful information.</p>
</li>
<li><p>Utimaco is the only vendor I have seen provide something like the SecurityServer simulator to the public. If you are in search of a HSM solution, consider checking out their offerings as a way to say 'thanks.'</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Creating a CyberArk (now Idira) Privileged Session Manager connection component for a web application]]></title><description><![CDATA[After helping someone in a CyberArk Discord that I frequent with correctly identifying a button as part of a Privileged Session Manager connection component for a web application, I realized that I've]]></description><link>https://timschindler.blog/creating-a-cyberark-privileged-session-manager-connection-component-for-a-web-application</link><guid isPermaLink="true">https://timschindler.blog/creating-a-cyberark-privileged-session-manager-connection-component-for-a-web-application</guid><category><![CDATA[privileged-session-manager]]></category><category><![CDATA[cyberark]]></category><category><![CDATA[Xpath]]></category><category><![CDATA[DOM]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Tue, 01 Nov 2022 18:14:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/OqtafYT5kTw/upload/v1667326410011/eTu8NHNv2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After helping someone in a <a href="https://discord.gg/daScMcyWkZ">CyberArk Discord that I frequent</a> with correctly identifying a button as part of a <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PASIMP/psm_WebApplication.htm">Privileged Session Manager connection component for a web application</a>, I realized that I've never made a connection component or a CPM plugin for a web application myself.</p>
<p>From experience I have from 'previous lives', I have already worked with the Document Object Model (DOM) and Selenium -- both used by the PSM and CPM web application frameworks -- so the fundamentals are already familiar but I've never done it in the context of CyberArk (now Idira).</p>
<p>In this post, we will develop a Microsoft Edge-based PSM connection component for the web application <a href="https://www.shopizer.com/">Shopizer</a> -- an open-source eCommerce platform that is available as a container so we can focus our time on developing the connection component.</p>
<div>
<div>💡</div>
<div>This is article is one in a <a target="_blank" rel="noopener noreferrer nofollow" class="text-primary underline underline-offset-2 hover:text-primary/80 cursor-pointer" href="https://timschindler.blog/series/shopizer-admin-ext" style="pointer-events:none">series of articles around creating PSM connectors and CPM plug-ins</a>.</div>
</div>

<h1>Understanding what a PSM connection component for a web app is doing</h1>
<p>Before we start thinking about creating a PSM connection component for a web app, it makes sense to understand how they work.</p>
<p>The CyberArk <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PASIMP/psm_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors">Web applications for PSM documentation</a> does not explicitly explain how they function but reviewing the page gives us important hints: <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PASIMP/psm_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors#Supportedbrowsers">the need to have the proper (Web)Driver for the browser</a> and the <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PASIMP/psm_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors#Syntax">syntax</a> and <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PASIMP/psm_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors#WebFormFields">examples</a> for defining Web Form Fields.</p>
<p>What is a driver and what does it do? A (Web)Driver <a href="https://www.w3.org/TR/webdriver1/">".. is a remote control interface that enables introspection and control of user agents. It provides a platform- and language-neutral wire protocol as a way for out-of-process programs to remotely instruct the behavior of web browsers. Provided is a set of interfaces to discover and manipulate DOM elements in web documents and to control the behavior of a user agent."</a></p>
<p>We can extract another hint from the description of a WebDriver: the DOM or Document Object Model. The DOM <a href="https://www.w3.org/TR/WD-DOM/introduction.html">"... is a programming API for HTML and XML documents. It defines the logical structure of documents and the way a document is accessed and manipulated. ... With the Document Object Model, programmers can create and build documents, navigate their structure, and add, modify, or delete elements and content."</a></p>
<p>From the information we gleaned, we can assume that the PSM uses a WebDriver to identify and interact with elements in the DOM. It will inject usernames and passwords into input fields, click buttons to submit forms, and read resulting DOMs to perform validations to determine if the login was successful or not. The connection component's WebFormFields property is how we instruct the PSM to identify elements and what to do with them.</p>
<h1>Identifying elements in the DOM</h1>
<p>We can tell CyberArk to identify elements in the DOM several ways: using the name, class, text, or XPath. <a href="https://www.w3schools.com/xml/xpath_intro.asp">XPath can be used to navigate through elements and attributes in an XML document.</a> XPath can also be used with an HTML document.</p>
<p>Explaining HTML elements and XPath is outside the scope of this post however let us quickly see the multiple ways we can <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PASIMP/psm_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors#Syntax">identify elements in a connection component's WebFormsField using its syntax</a> with a simplified DOM:</p>
<pre><code class="language-html">&lt;html&gt;
    &lt;form&gt;
        &lt;input type="text" id="i_username" name="username" class="input_field" placeholder="Enter your username"/&gt; &lt;br&gt;
        &lt;input type="password" id="i_password" name="password" class="input_field" placeholder="Enter your password"/&gt; &lt;br&gt;
        &lt;input type="submit" id="s_login" class="submit_button" value="Login"/&gt;
    &lt;/form&gt;
&lt;/html&gt;
</code></pre>
<h2>Identifying elements with id, name, class, or text in <code>WebFormsField</code></h2>
<p>With the above DOM as an example, we can identify the elements in our <code>WebFormsField</code> effortlessly. Identifying by the elements' <code>id</code> and <code>name</code>, it would look something like:</p>
<pre><code class="language-powershell">i_username &gt; {Username} (SearchBy=id)
password &gt; {Password} (SearchBy=name)
s_login &gt; (Click) (SearchBy=id)
</code></pre>
<p>We can mix and match what we search by. Depending on the elements, it may lend itself to one way or the other.</p>
<h2>Identifying elements with XPath in <code>WebFormsField</code></h2>
<p>We can accomplish the same as we did purely with XPath:</p>
<pre><code class="language-xml">//*^[@id="i_username"^] &gt; {Username} (SearchBy=XPath)
//*/form/input^[2^] &gt; {Password} (SearchBy=XPath)
//*^[@class="submit_button"^] &gt; (Click) (SearchBy=XPath)
</code></pre>
<p>With XPath we can use the same identifiers as we previously had and do not need to specify element attributes at all like in the case of the password field -- which could be useful if an element we are working at has attributes with dynamic values or cannot be defined uniquely by it's attributes.</p>
<p><strong>Note:</strong> The way we defined the XPath for the password field is not recommended. It is simply selecting the second <code>input</code> element on the page. If the page is later updated and an additional <code>input</code> is added before the two existing ones (for example: a search field in a header of the page) then our password field is no longer the second <code>input</code> but rather the third.</p>
<h1>Creating our Shopizer connection component</h1>
<p>With a better understanding of how PSM connection components for web apps work and how we can identify DOM elements in the <code>WebFormsField</code>, the creation of the connection component should be pretty straightforward.</p>
<p>Shopizer offers two portals: a customer-facing one and one meant for administrators. We will create a connection component for the Shopizer administrator portal.</p>
<p>With our local Shopizer containers running, a local test Shopizer administrator user account created, and our PSM equipped with Edge, the Edge Driver and the <a href="https://cyberark-customers.force.com/mplace/s/#a3550000000EiCMAA0-a3950000000jjUwAAI">latest Secure Web Application Connectors Framework</a>, let's create our first connection component for a web application.</p>
<h2>Creating a new web connection component</h2>
<p>As instructed in the <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PASIMP/psm_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CPSM%20Connectors#Configuration">Configuration part of the documentation</a>, we first need to clone the <code>PSM-WebAppSample</code> and change the <code>Id</code> of it. We will set <code>PSM-ShopizerAdministrator</code> as the new <code>Id</code>.</p>
<p>Under <code>Target Settings</code>, we set the <code>ClientApp</code> to <code>Edge</code>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667305935155/B6zPmCemn.png" alt="image.png" />

<p>Under <code>Client Specific</code>, we add new two new parameters: <code>BrowserPath</code> and <code>DriverPath</code>. As we are using Edge, the paths are <code>C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe</code> and <code>C:\Program Files (x86)\CyberArk\PSM\Components\msedgedriver.exe</code> respectively. While in the area, we set the value for <code>RunValidations</code> to <code>No</code> -- this is something we should do later.</p>
<p>Then under <code>Web Form Settings</code>, we adjust the <code>LogonURL</code> to <code>http://{address}:4200/#/auth</code>. The administrator web interface runs on port <code>4200</code> and it would be better to add <code>Port</code> as a connection parameter but as we are not creating this for production use, we hard-code the port.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667306129101/NvFm06Y_m.png" alt="image.png" />

<h2>Defining our <code>WebFormFields</code></h2>
<p>The <code>WebFormFields</code> is the key to this connection component working. As we did in the example above, we need to define the username, password, and submit button elements.</p>
<p>But unlike in our example, we do not have the DOM clearly laid out in front of us so how can we identify those elements? This is when your favorite browser's Developer Tools come into play. I am using Firefox but other browsers have similar functionality.</p>
<p>Assuming the Shopizer containers are running on your local machine, navigate to <code>http://localhost:4200/#/auth</code> and you should be greeted with the login page.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667306384752/NYv0svjKL.png" alt="image.png" />

<p>From there, we can right-click the username field and click <code>Inspect</code> to be brought directly to the element in the DOM. When we mouse over the element in the Inspector tool, the element is highlighted above in the web page.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667306588176/Z5v62ip6R.png" alt="image.png" />

<p>Now it is just about how we want to identify the <code>input</code> element that represents the Username field.</p>
<p>In addition to XPath, as the <code>input</code> element has an <code>id</code>, <code>class</code>, and <code>name</code> attributes defined, we can use whatever we want in our <code>WebFormFields</code> to identify the element. But which way is best?</p>
<p>Ultimately, it depends. As we are typically developing connection components for web applications we are not developing ourselves, we are at the will of the application's developers.</p>
<p>An element that has a <code>class</code> defined one day may not have it defined the next day if the site goes through a re-vamp. Some attributes may have values with random digits at the end giving us the clue that the attribute's values are not explicitly set and may be auto-generated with each deployment. XPath can be pretty resilient if our path expression does not include too many nodes.</p>
<p>For the username field of our Shopizer login page, we have a few options. The <code>id</code> and <code>name</code> values both look explicitly set and feel as if they have a relatively low risk of changing in the future. Let's go with <code>name.</code></p>
<p>Back in our <code>WebFormFields</code> we update it to identify the field to input the account username using the attribute <code>name</code> with the value of <code>username</code>. We use <code>Inspect</code> in Firefox to identify the password and submit buttons and define those too, resulting in:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667307635405/GL7D0Cb9i.png" alt="image.png" />

<p>Now after adding our new connection component to the platform assigned to the onboarded test Shopizer administrator account we created, let's give it a whirl.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667320671257/ivQuKm5lQ.png" alt="image.png" />

<p>It's in French -- this seems to be the default language for Shopizer -- but it worked! We are logged into the Shopizer administrator portal.</p>
<h1>Troubleshooting</h1>
<p>Depending on the complexity of the web app, you may run into problems. With complex DOMs -- especially ones that involve iFrames -- it may not be straightforward when identifying elements.</p>
<p>Thankfully, though not well documented at all, you can enable very verbose trace levels in <code>&lt;session id&gt;.ClientDispatcher.log</code> that are created in the <code>Logs\Components\</code> folder. To do so, set <code>TraceLevels=1,2</code> in the PVWA options under <code>Privileged Session Management &gt; General Settings &gt; Client Connection Settings</code>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667595495627/YyCpPbLgt.png" alt="image.png" />

<p>After a restart of the PSM service or waiting for the PVWA configuration refresh on the PSM, you will see the log file with helpful content such as:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667595659656/UAV_OE6B0.png" alt="image.png" />

<p>Another helpful debugging 'tool' is setting <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/13.0/en/Content/PASIMP/psm_cc_target_settings.htm?highlight=EnableTrace#Clientspecific"><code>EnableTrace</code> to <code>Yes</code></a>. This shows the entire login process that the PSM obscures.</p>
<h1>Improving our connection component</h1>
<p>We developed our connection component 'quick and dirty' which is fine for a lab environment but nothing we would want to introduce into production. We have a lot of areas to improve on:</p>
<ol>
<li><p>We should not hardcode the Port in the <code>LogonURL</code>, rather allow this to be defined at the account level to handle the case where the Shopizer administrator portal is running on a port other than the default.</p>
</li>
<li><p>We have no validations. After we login, we should use an element unique to a successfully-authenticated Shopizer administrator portal to inform the PSM the login was successful.</p>
</li>
<li><p>Similar to a validation of a successful login, we should set a validation for login failures. Like many applications, Shopizer returns an error message when a login was unsuccessful and we can use that element to clue the PSM into the fact that it failed.</p>
</li>
</ol>
<p>With the above points addressed, the connection component moves closer to being production-ready and with our new knowledge of how a connection component for a web app works, we likely have a lot of the information we need to develop <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/Plugins/CPM_WebApplication.htm?tocpath=Developer%7CCreate%20extensions%7CCreate%20CPM%20plugins%7C_____4">a CPM plugin for a web app</a>, too.</p>
]]></content:encoded></item><item><title><![CDATA[Using a hardware security module to secure the CyberArk (now Idira) Vault's Server Key]]></title><description><![CDATA[The CyberArk (now Idira) Vault allows for the Server key to be stored in a hardware security module (HSM). The Server key is used as a key-encryption-key so it is appropriate to use a HSM as they prov]]></description><link>https://timschindler.blog/using-a-hardware-security-module-to-secure-the-cyberark-vaults-server-key</link><guid isPermaLink="true">https://timschindler.blog/using-a-hardware-security-module-to-secure-the-cyberark-vaults-server-key</guid><category><![CDATA[hsm]]></category><category><![CDATA[cyberark]]></category><category><![CDATA[infosec]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Cryptography]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Mon, 31 Oct 2022 10:42:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/lVZjvw-u9V8/upload/v1667212799892/lMqDai-yU.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The CyberArk (now Idira) Vault allows for the Server key to be stored in a <a href="https://en.wikipedia.org/wiki/Hardware_security_module">hardware security module (HSM)</a>. The Server key is used as a <a href="https://csrc.nist.gov/glossary/term/key_encryption_key">key-encryption-key</a> so it is appropriate to use a HSM as they provide the highest level of protection for the Server key.</p>
<p>HSM integration with CyberArk is actually well-documented. CyberArk provides information on <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PAS%20INST/configuring-HSM-Key-Management.htm?TocPath=Administrator%7CComponents%7CDigital%20Vault%7COperate%20the%20CyberArk%20Vault%7CIntegrate%20the%20Digital%20Vault%20with%20Third-Party%20Components%7C_____3">loading an existing Server key into an HSM</a> as well as generating a new <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/PAS%20INST/configuring-HSM-Key-Management.htm?TocPath=Administrator%7CComponents%7CDigital%20Vault%7COperate%20the%20CyberArk%20Vault%7CIntegrate%20the%20Digital%20Vault%20with%20Third-Party%20Components%7C_____3#_Ref242158975">Server key directly in the HSM</a> but as with anything, hands-on experience goes a long way to demystifying a concept.</p>
<p>In a lab environment, we will generate a new Server key directly on the HSM then perform a re-key of the Vault using that key. The <a href="https://utimaco.com/downloads/free-simulators-and-sdks/securityserver-simulator">SecurityServer simulator from Utimaco</a> will be used as our HSM.</p>
<h1>What are hardware security modules and what do they offer?</h1>
<p>Hardware security modules provide a secure location to store cryptographic information (typically keys) as well as provide a secure place to perform cryptographic operations (such as using the keys to encrypt or decrypt objects.) HSMs are commonly physical, hardware devices that have tamper proof protections. They can be in the form of a network appliance or a PCI device.</p>
<p>Applications interact with HSMs through different APIs -- the <a href="https://en.wikipedia.org/wiki/PKCS_11">PKCS#11 standard</a> being one of the most common and the one CyberArk uses to talk to HSMs.</p>
<p>As HSMs store cryptographic information and provide the place to perform operations involving that information, applications need a constant connection to the HSM. When the connection is broken, the ability to use the cryptographic information is lost. The same applies to CyberArk: when it cannot connect to the HSM, critical functionality such as retrieving credentials from the Vault is does not work!</p>
<h1>The Vault and it's encryption keys</h1>
<p>Before doing any sort of HSM integration, it first makes sense to understand the encryption keys used by the Vault to secure Vault objects. The CyberArk Technical Community has <a href="https://cyberark-customers.force.com/s/article/What-are-the-Vault-Keys-for">a nice article on all the Vault keys and their purpose</a>. We will focus on the ones used to encrypt Vault objects.</p>
<p>There are three 'tiers' of keys in the Vault encryption hierarchy: Vault keys, safe keys, and object keys. Each object is encrypted by a unique key specific to the object, object keys are encrypted by a safe key, and Vault keys encrypt the safe keys.</p>
<p>There are two types of Vault keys when referring to the encryption of Vault objects: the Recovery key (really it is a pair as the Recovery 'key' is asymmetric) and the Server key (symmetric.) Safe keys are encrypted and decrypted by both leading to two copies of the encrypted safe key. The Server key must be accessible to the Vault in order for it to start.</p>
<p>Only the Server key can be stored on a HSM and the private key for the Recovery key pair should be kept securely offline.</p>
<p><em>The CyberArk Technical Community has an excellent knowledge article regarding</em> <a href="https://cyberark-customers.force.com/s/article/Hierarchical-Key-Management"><em>hierarchical key management</em></a><em>.</em></p>
<h1>HSM and CyberArk integration</h1>
<p>To better understand HSM integration with CyberArk, in our CyberArk 12.6 Primary-DR lab environment running on Windows Server 2019, we will:</p>
<ol>
<li><p>Perform initial Vault configurations</p>
</li>
<li><p>Generate a new Server key directly on the HSM</p>
</li>
<li><p>Re-encrypt all the Vault data and metadata with the new Server key on the HSM</p>
</li>
</ol>
<p>The Server key will be generated on the HSM from the DR Vault. We will then re-encrypt the DR Vault before re-encrypting the Primary vault.</p>
<p><em>Note: In the lab environment, the HSM (Utimaco's SecurityServer simulator) is already configured with a slot initialized to store our new Server key. On both the Primary and DR Vaults, the PKCS#11 provider used to communicate with the HSM is configured and functional. See</em> <a href="https://timschindler.blog/installing-and-configuring-utimacos-securityserver-simulator"><em>this blog post dedicated to configuring Ultimaco's SecurityServer simulator</em></a><em>.</em></p>
<h2>Initial Vault configurations</h2>
<p>On both Vaults, we need to directly edit the <code>dbparm.ini</code> to define the path to our HSM's PKCS11 provider DLL via the <code>PKCS11ProviderPath</code> as well as use <code>AllowNonStandardFWAddresses</code> to allow outbound communication to the HSM -- the last part being optional if your Vaults are <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/Release%20Notes/RN-WhatsNew12-6.htm?tocpath=Get%20Started%7CWhat%E2%80%99s%20New%7CRelease%20Notes%7C_____2#WindowsServer2019HardeningrevisedtofollowCISstandards">version 12.6 and running Windows Server 2019 due to hardening changes</a>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1666975248812/eD7XDcUbw.png" alt="image.png" />

<p>Afterwards we need to define the PIN that the Vault will use to access the slot the Server key is located using <code>CAVaultManager.exe SecureSecretFiles /SecretType HSM /Secret 1111</code></p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1666975373287/1lqJhySzO.png" alt="image.png" />

<p>Opening <code>dbparm.ini</code> we see <code>HSMPinCode</code> defined with the value of our encrypted PIN.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1666975498748/73g5_MrYC.png" alt="image.png" />

<p>Now we are in a position to generate the Server key.</p>
<h2>Generate the new Server key</h2>
<p>We will generate the Server key on the DR Vault. This only needs to be done once as the same Server key will be used for both the Primary and DR Vault. There is no requirement to generate it on one Vault versus the other -- we just do it on the DR Vault as we will re-encrypt the DR Vault with it first.</p>
<p>After stopping the CyberArk Vault Disaster Recovery service, we use <code>CAVaultManger GenerateKeyOnHSM /ServerKey</code> to generate the Server key. <code>CAVaultManager</code> uses the parameters defined in <code>dbparm.ini</code> to communicate with the HSM. Successful Server key generation lets you know the Vault will be able to as well.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1666977073748/QkQrpgT94.png" alt="image.png" />

<p>We need to note down the <code>KeyID</code> as we will use it later.</p>
<h2>Re-encrypt the DR Vault with our new Server key</h2>
<p>At this point it makes sense to ensure the Primary Vault's Vault services have been stopped as after the re-encryption on the DR Vault the two will be using different Server keys and replication between them will not be possible until both are using the same, new Server key.</p>
<p>With the Master private key at the location defined at <code>RecoveryPrvKey</code> in the <code>dbparm.ini</code>, <code>ChangeServerKeys</code> is used to re-encrypt the Vault data.</p>
<p>We run <code>ChangeServerKeys.exe C:\Keys\DemoOperatorKeys\ C:\Keys\DemoOperatorKeys\VaultEmergency.pass HSM#1</code>. <code>C:\Keys\DemoOperatorKeys\</code> refers to the location of the keys that will be used to re-encrypt the Vault -- including our existing Master private key. The Server key used will be the one on the HSM, however.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667161189577/MvhKDrvDa.png" alt="image.png" />

<p>The changing of keys may take awhile depending on the specifications of your Vault and the HSM as well as the amount of data in the Vault. As this process creates new safe and object keys based on the new Server key, this could take <strong>days</strong>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667161140278/_P1nugqUr.png" alt="image.png" />

<p>Like the output says, now we need to update the <code>ServerKey</code> parameter in the <code>dbparm.ini</code> to refer to the HSM.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667161408747/BnzkQGmNR.png" alt="image.png" />

<p>And quickly start the Vault services.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667161507786/I67SS8Xvi.png" alt="image.png" />

<p>The final verification is logging into PrivateArk.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667202466856/Z9vlemOBA.png" alt="image.png" />

<p>After logging out of PrivateArk and stopping the Vault services on the DR Vault, we are ready to move on to the Primary.</p>
<h2>Re-encrypt the Primary Vault with our new Server key</h2>
<p>We run the exact same <code>ChangeServerKeys</code> command on the Primary Vault.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667202746063/mNiPmVH83.png" alt="image.png" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667202777029/uOfzu2scr.png" alt="image.png" />

<p>Like the DR Vault, we change the <code>dbparm.ini</code> on the Primary Vault to have the <code>ServerKey</code> parameter point to <code>HSM#1</code>, start the Vault services, and login.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667203040697/UqycOzCWW.png" alt="image.png" />

<p>At this point, we can start the CyberArk Vault Disaster Recovery service on the DR Vault and observe successful replication.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667203450300/De5cw8QfZ.png" alt="image.png" />

<h1>Vault behavior when the HSM is unavailable</h1>
<p>Our Server key being stored on a HSM increases the security posture of our environment but also introduces complexity and another point of failure. We should ensure we have a good understanding on what the behavior of the Vault is when the HSM is unavailable and what we need to do.</p>
<p>We should consider the following:</p>
<ul>
<li><p>Vault behavior when the Vault tries to start and the HSM is unavailable.</p>
</li>
<li><p>Vault behavior when the Vault is already running and the HSM is unavailable.</p>
</li>
<li><p>What needs to be done on the Vault side when the HSM becomes available.</p>
</li>
</ul>
<h2>Starting the Vault with the HSM unavailable</h2>
<p>This is a pretty straightforward case. As the Server key is needed to start the Vault services and the Server key lives on the HSM, the HSM being unavailable prevents the Vault services from starting.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667205302261/K3-fvxwTj.png" alt="image.png" />

<p>Looking in the log file for the PKCS#11 provider we are using, we see the device cannot be found which could give us a hint where to start our troubleshooting.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667205473151/tBHpooo38.png" alt="image.png" />

<h2>An already running Vault with the HSM unavailable</h2>
<p>If the Vault is already running and the HSM becomes unavailable, the Vault does not become completely unavailable. Some functionality -- user authentication, report generation, user management, changing of safe memberships, etc. -- may still work and give a false sense of security.</p>
<p>What is guaranteed not to work is the retrieval of Vault objects (credentials.) When attempting to retrieve a credential while the HSM is unavailable, there will be an error about the Safe key being incorrect.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667206182346/IHGlFSkNH.png" alt="image.png" />

<p>Peeking in the trace logs, we can even see the decryption failing:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667207053584/Avo26CpFX.png" alt="image.png" />

<p>Based on our understanding of the Vault's encryption hierarchy, this makes sense. We know each Vault object is encrypted by an individual key, which is then encrypted by a Safe key where the Safe key is then encrypted by the Server key. With the Server key being unavailable, the Safe key cannot be decrypted in order to be used to decrypt the object's key.</p>
<p>In our current setup, the HSM becoming available again is enough for the Vault to assume normal operations. The first retrieval of a credential takes awhile -- the Vault is probably doing whatever it needs to do to recover from an HSM outage -- while the subsequent ones go much quicker:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667208137185/RnJTtTgNl.png" alt="image.png" />

<h2>Recovery of the Vault with <code>ReconnectHSMOnErrorCodes</code></h2>
<p>In the case the Vault does not assume normal operations automatically after the HSM becoming unavailable again then we can use <a href="https://cyberark-customers.force.com/s/article/How-to-configure-the-Vault-to-reconnect-to-a-HSM"><code>ReconnectHSMOnErrorCodes</code> to have the Vault reconnect to the HSM</a>.</p>
<p><code>DBPARM.sample.ini</code> has an example of the values for <code>ReconnectHSMOnErrorCodes</code> (<code>ReconnectHSMOnErrorCodes=48,50,179</code>) but there does not seem to be a complete list of possible HSM error codes however we can see them in the trace logs with debug levels <code>CRYPT(1,2)</code>.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667209974103/FNPIOdVhU.png" alt="image.png" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1667211013982/UZ9_HswtV.png" alt="image.png" />

<h1>Wrapping it up</h1>
<p>HSMs provide the the best security for our Vaults' Server key by providing a secure location for them to reside and to perform cryptographic operations with. Generating a Server key and re-encrypting our Vaults' data is easy with the available documentation from CyberArk.</p>
<p>Once the integration with an HSM is complete, together with an understanding of the Vault's encryption hierarchy, Vault debug levels and trace logs give us important insight into when and how the Vault interacts with it, demystifying both HSMs and HSM integration with CyberArk's Vault.</p>
<p>Are you planning to or have you already integrated your CyberArk environment with a HSM? What HSM vendor are you using? Provide some feedback in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[Managing objects in CyberArk (now Idira) with PowerShell Desired State Configuration]]></title><description><![CDATA[PowerShell Desired State Configuration (DSC) is a tool similar to Ansible, Puppet, and Chef that enables declaratively setting how an environment is configured. PowerShell DSC can be used to ensure re]]></description><link>https://timschindler.blog/managing-objects-in-cyberark-with-powershell-desired-state-configuration</link><guid isPermaLink="true">https://timschindler.blog/managing-objects-in-cyberark-with-powershell-desired-state-configuration</guid><category><![CDATA[cyberark]]></category><category><![CDATA[Powershell]]></category><category><![CDATA[configuration]]></category><category><![CDATA[Security]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Mon, 19 Sep 2022 10:07:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/HfFoo4d061A/upload/v1663357698259/eSKclOiVL.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://docs.microsoft.com/de-de/powershell/scripting/dsc/overview?view=powershell-7.2">PowerShell Desired State Configuration (DSC)</a> is a tool similar to Ansible, Puppet, and Chef that enables declaratively setting how an environment is configured. PowerShell DSC can be used to ensure <a href="https://docs.microsoft.com/en-us/powershell/dsc/concepts/resources?view=dsc-2.0"><em>resources</em></a> such as environmental variables, software, files, Active Directory users, and application-specific settings are either present or absent through the use of <a href="https://docs.microsoft.com/en-us/powershell/dsc/concepts/configurations?view=dsc-2.0"><em>configurations</em></a>.</p>
<p>A few of the most obvious, CyberArk (now Idira)-related use cases for PowerShell DSC include making sure that the required software is installed after importing a <a href="https://timschindler.blog/creating-cyberark-connection-component-connector-packages">Universal Connector package</a> or after <a href="https://timschindler.blog/continuous-deployment-of-cyberark-platforms-using-github-actions">deploying a CPM plugin as part of a continuous deployment pipeline</a>. It can be used to manage the rules within an <code>PSMConfigureAppLocker.xml</code> file or invoke the automated installation or upgrade processes for components.</p>
<p>Though with the <a href="https://github.com/aaearon/CyberArkDsc"><code>CyberArkDsc</code> module</a>, PowerShell DSC can also be used to manage CyberArk Vault objects such as safes, safe memberships, and accounts.</p>
<p><a class="embed-card" href="https://github.com/aaearon/CyberArkDsc">https://github.com/aaearon/CyberArkDsc</a></p>

<h1>Why use PowerShell DSC to manage Vault objects?</h1>
<p>Using PowerShell DSC to manage Vault objects makes sense for the same reasons configuration management makes sense for infrastructure.</p>
<ul>
<li><p><strong>Combat configuration drift</strong> - Ensure accounts have the intended configurations long after they are created -- particularly useful in a test or lab environment where experimentation may lead to false settings and thus unintended consequences.</p>
</li>
<li><p><strong>Idempotency</strong> - Safes, safe memberships, and accounts can be created and configured using scripts, the Password Upload Utility, or PACLI but running executing these when the objects already exist may produce different results or fail entirely.</p>
</li>
<li><p><strong>Treat Vault objects as code</strong> - Like with infrastructure as code, Vault objects can be defined in configurations and these configurations can be stored in version control, be included in continuous deployment pipelines, or reviewed by other team members.</p>
</li>
<li><p><strong>Included in PowerShell 5.1 and above</strong> - PowerShell DSC is included in PowerShell 5.1 and above which absolves the need to install to install more software, such as Python when dealing with Ansible.</p>
</li>
</ul>
<h1>Using PowerShell DSC and <code>CyberArkDsc</code> to populate an empty Vault</h1>
<p>One of the leading use cases for using PowerShell DSC together with CyberArk is that it can be used to quickly create accounts on target systems and onboard them into the Vault.</p>
<p>In the following section, a DSC configuration will be used to create Active Directory users, safes to store them in the Vault, assign the needed safe memberships to make the accounts accessible, and finally onboard the users into the safes with the needed reconcile accounts.</p>
<p><em>Note: For the sake of brevity, knowledge of PowerShell DSC concepts and</em> <a href="https://docs.microsoft.com/en-us/powershell/dsc/concepts/terminology?view=dsc-2.0"><em>terminology</em></a> <em>such as resources and configurations is assumed.</em></p>
<h2>Installing the <code>CyberArkDsc</code> and <code>ActiveDirectoryDsc</code> modules</h2>
<p>Installing the <code>ActiveDirectoryDsc</code> module is straightforward as it can be installed from the PowerShell Gallery like any other PowerShell module. The <a href="https://github.com/dsccommunity/ActiveDirectoryDsc/wiki/#getting-started"><code>ActiveDirectoryDsc</code> wiki</a> has information on getting started.</p>
<p><code>CyberArkDsc</code> is not available in the PowerShell Gallery and needs to be manually installed. As by default configurations are executed under the <code>SYSTEM</code> account, <code>CyberArkDsc</code> needs to, along with it's dependency <a href="https://github.com/pspete/psPAS"><code>psPAS</code></a>, exist in a non-user-specific folder location defined in <code>\(env:PSModulePath</code> -- for example: <code>\)env:ProgramFiles\WindowsPowerShell\Modules</code>.</p>
<p>We can use <code>Get-DscResource</code> to ensure that both DSC modules are installed correctly.</p>
<pre><code class="language-powershell">PS &gt; Get-DscResource | Where-Object {\(_.Name -eq 'CYA_Account' -or \)_.Name -eq 'ADUser'}

ImplementedAs   Name                      ModuleName                     Version    Properties
-------------   ----                      ----------                     -------    ----------
PowerShell      ADUser                    ActiveDirectoryDsc             6.2.0      {DomainName, UserName, AccountNotDelegated, All...
PowerShell      CYA_Account               CyberArkDsc                    0.0.2      {Address, AuthenticationType, Credential, Ensur...

PS &gt;
</code></pre>
<h2>Writing our configuration</h2>
<p>We will keep our DSC configuration relatively short and sweet. We already have LDAP integration configured for the Vault, a user that is in both Vault Admins and Domain Admins groups, and platforms created in the Vault that the accounts will be onboarded with so this just leaves us with:</p>
<ol>
<li><p>Creating two users in Active Directory, one to be used as a reconcile account.</p>
</li>
<li><p>Adding the reconcile account as a member to the Domain Admins group in Active Directory so it can reconcile the other.</p>
</li>
<li><p>Creating two safes to store the accounts in: one for the reconcile account, the other.</p>
</li>
<li><p>Assigning a membership to each safe enabling an existing Active Directory group named <code>CyberArk Administrators</code> full permissions to the reconcile account safe and another, existing Active Directory group named <code>Windows Administrators</code> with list, retrieve, and use accounts to the other.</p>
</li>
<li><p>Onboarding both accounts: The reconcile account will be onboarded with the password defined when creating it in Active Directory and the other account will be onboarded with the reconcile account defined as a linked account.</p>
</li>
</ol>
<h2>Defining the base of our configuration</h2>
<p>Each DSC resource in <code>CyberArkDsc</code> takes at least four parameters: <code>PvwaUrl</code>, <code>AuthenticationType</code>, <code>Credential</code>, and <code>SkipCertificateCheck.</code> We define these as parameters our configuration accepts. In addition, we define an <code>InitialPassword</code> parameter that will be used when creating the reconcile account's Active Directory user and when onboarding into CyberArk.</p>
<p>We will have a single node -- <code>localhost</code> -- as we will run the DSC configuration on the same machine we compile it on. Our resources will go inside it.</p>
<p>Both <code>Credential</code> and <code>InitialPassword</code> will be passed <code>PSCredential</code> objects. As this is a lab environment I will not go through the <a href="https://docs.microsoft.com/en-us/powershell/dsc/configurations/configdatacredentials?view=dsc-1.1">effort to have the credentials be properly secured in the resulting, compiled configuration</a> so I set a switch to allow the passwords to be stored plaintext.</p>
<pre><code class="language-powershell">Configuration CreateLab_Configuration {
    param(
        $PvwaUrl,
        $AuthenticationType,
        $Credential,
        $SkipCertificateCheck,

        $InitialPassword
    )

    Node localhost {

    }

}

$ConfigData = @{
    AllNodes = @(
        @{
            NodeName                    = 'localhost'
            PSDscAllowDomainUser        = $true
            PSDscAllowPlainTextPassword = $true
        }
    )
}

$VaultCredential = New-Object System.Management.Automation.PSCredential('allison', (ConvertTo-SecureString 'Password!' -AsPlainText -Force))
$Password = New-Object System.Management.Automation.PSCredential('banana', (ConvertTo-SecureString 'P@$$W0RD12345' -AsPlainText -Force))

$ConfigurationParameters = @{
    ConfigurationData    = $ConfigData

    PvwaUrl              = 'https://192.168.137.101'
    AuthenticationType   = 'LDAP'
    SkipCertificateCheck = $true
    Credential           = $VaultCredential
    InitialPassword      = $Password
}

CreateLab_Configuration @ConfigurationParameters
</code></pre>
<h2>Defining resources</h2>
<p>Our configuration will have nine resources. Using documentation, our familiarity with managing Vault objects via <code>psPAS</code> and <code>Get-DscResource -Name ADUser,ADGroup,CYA_Account,CYA_Safe,CYA_SafeMember -Syntax</code> to know how to define each resource, our complete configuration looks like:</p>
<pre><code class="language-powershell">Configuration CreateLab_Configuration {
    param(
        $PvwaUrl,
        $AuthenticationType,
        $Credential,
        $SkipCertificateCheck,

        $InitialPassword
    )

    Import-DscResource -ModuleName 'ActiveDirectoryDsc' -Name 'ADUser', 'ADGroup'
    Import-DscResource -ModuleName 'CyberArkDsc'

    Node 'localhost' {

        ADUser 'iosharp\windowsReconcile50' {
            Ensure     = 'Present'
            UserName   = 'windowsReconcile50'
            DomainName = 'iosharp.dev'
            Password   = $InitialPassword
            PasswordNeverResets =  $true

            Credential = $Credential
        }

        ADUser 'iosharp\windowsAdmin50' {
            Ensure     = 'Present'
            UserName   = 'windowsAdmin50'
            DomainName = 'iosharp.dev'
            Password   = $InitialPassword
            PasswordNeverResets =  $true

            Credential = $Credential
        }

        ADGroup 'Domain Admins' {
            Ensure           = 'Present'
            GroupName        = 'Domain Admins'
            MembersToInclude = 'windowsReconcile50'
            Credential       = $Credential

            DependsOn        = '[ADUser]iosharp\windowsReconcile50'
        }

        CYA_Safe 'WinRec50' {
            Ensure                = 'Present'

            SafeName              = 'WinRec50'
            Description           = 'Windows Reconcile Safe'
            ManagingCPM           = 'PasswordManager'
            NumberOfDaysRetention = '1'

            PvwaUrl               = $PvwaUrl
            AuthenticationType    = $AuthenticationType
            SkipCertificateCheck  = $SkipCertificateCheck
            Credential            = $Credential
        }

        CYA_Safe 'WinAdmins50' {
            Ensure                = 'Present'

            SafeName              = 'WinAdmins50'
            Description           = 'Windows Admins Safe'
            ManagingCPM           = 'PasswordManager'
            NumberOfDaysRetention = '1'

            PvwaUrl               = $PvwaUrl
            AuthenticationType    = $AuthenticationType
            SkipCertificateCheck  = $SkipCertificateCheck
            Credential            = $Credential
        }

        CYA_SafeMember 'WinRec\CyberArk Administrators' {
            Ensure                                 = 'Present'

            SafeName                               = 'WinRec50'
            MemberName                             = 'CyberArk Administrators'
            SearchIn                               = 'iosharp.dev'
            UseAccounts                            = $true
            RetrieveAccounts                       = $true
            ListAccounts                           = $true
            AddAccounts                            = $true
            UpdateAccountContent                   = $true
            UpdateAccountProperties                = $true
            InitiateCPMAccountManagementOperations = $true
            SpecifyNextAccountContent              = $true
            RenameAccounts                         = $true
            DeleteAccounts                         = $true
            UnlockAccounts                         = $true
            ManageSafe                             = $true
            ManageSafeMembers                      = $true
            BackupSafe                             = $true
            ViewAuditLog                           = $true
            ViewSafeMembers                        = $true
            AccessWithoutConfirmation              = $true
            CreateFolders                          = $true
            DeleteFolders                          = $true
            MoveAccountsAndFolders                 = $true
            RequestsAuthorizationLevel1            = $true
            RequestsAuthorizationLevel2            = $false

            PvwaUrl                                = $PvwaUrl
            AuthenticationType                     = $AuthenticationType
            SkipCertificateCheck                   = $SkipCertificateCheck
            Credential                             = $Credential

            DependsOn                              = '[CYA_Safe]WinRec50'
        }

        CYA_SafeMember 'WinAdmins\Windows Administrators' {
            Ensure               = 'Present'

            SafeName             = 'WinAdmins50'
            MemberName           = 'Windows Administrators'
            SearchIn             = 'iosharp.dev'
            UseAccounts          = $true
            RetrieveAccounts     = $true
            ListAccounts         = $true
            ViewSafeMembers      = $true

            PvwaUrl              = $PvwaUrl
            AuthenticationType   = $AuthenticationType
            SkipCertificateCheck = $SkipCertificateCheck
            Credential           = $Credential

            DependsOn            = '[CYA_Safe]WinAdmins50'
        }

        CYA_Account 'windowsReconcile50' {
            Ensure               = 'Present'
            UserName             = 'windowsReconcile50'
            Address              = 'iosharp.dev'
            PlatformId           = 'ioSHARPWindowsDomainReconcileAccount'
            SafeName             = 'WinRec50'
            Name                 = 'windowsReconcile50@iosharp.dev'
            Password             = $InitialPassword

            PvwaUrl              = 'https://192.168.137.101'
            AuthenticationType   = 'LDAP'
            SkipCertificateCheck = $true
            Credential           = $VaultCredential

            DependsOn            = '[CYA_Safe]WinRec50'
        }

        CYA_Account 'windowsAdmin50' {
            Ensure               = 'Present'
            UserName             = 'windowsAdmin50'
            Address              = 'iosharp.dev'
            PlatformId           = 'ioSHARPWindowsDomainAccount'
            SafeName             = 'WinAdmins50'
            Name                 = 'windowsAdmin50@iosharp.dev'
            ReconcileAccount     = @{
                Name   = 'windowsReconcile50@iosharp.dev'
                Safe   = 'WinRec50'
                Folder = 'root'
            }

            PvwaUrl              = 'https://192.168.137.101'
            AuthenticationType   = 'LDAP'
            SkipCertificateCheck = $true
            Credential           = $VaultCredential

            DependsOn            = '[CYA_Account]windowsReconcile50', '[CYA_Safe]WinAdmins50'

        }

    }
}

$ConfigData = @{
    AllNodes = @(
        @{
            NodeName                    = 'localhost'
            PSDscAllowDomainUser        = $true
            PSDscAllowPlainTextPassword = $true
        }
    )
}

$VaultCredential = New-Object System.Management.Automation.PSCredential('allison', (ConvertTo-SecureString 'Password!' -AsPlainText -Force))
$Password = New-Object System.Management.Automation.PSCredential('banana', (ConvertTo-SecureString 'P@$$W0RD12345' -AsPlainText -Force))

$ConfigurationParameters = @{
    ConfigurationData    = $ConfigData

    PvwaUrl              = 'https://192.168.137.101'
    AuthenticationType   = 'LDAP'
    SkipCertificateCheck = $true
    Credential           = $VaultCredential
    InitialPassword      = $Password
}

CreateLab_Configuration @ConfigurationParameters
</code></pre>
<h2>Compiling and running our configuration</h2>
<p>We dot source our saved configuration file to compile it:</p>
<pre><code class="language-powershell">PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt; . .\CreateLab.ps1


    Directory: C:\Users\Tim.IOSHARP\Desktop\DSC\CreateLab_Configuration


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----        9/16/2022   7:43 PM          15494 localhost.mof


PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt;
</code></pre>
<p>We can use <code>Test-DscConfiguration</code> to see if our resources in their desired state.</p>
<pre><code class="language-powershell">PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt; Test-DscConfiguration .\CreateLab_Configuration\ | Select-Object -ExpandProperty ResourcesNotInDesiredState | Select-Object ResourceId

ResourceId
----------
[ADUser]iosharp\windowsReconcile50
[ADUser]iosharp\windowsAdmin50
[ADGroup]Domain Admins
[CYA_Safe]WinRec50
[CYA_Safe]WinAdmins50
[CYA_SafeMember]WinRec\CyberArk Administrators
[CYA_SafeMember]WinAdmins\Windows Administrators
[CYA_Account]windowsReconcile50
[CYA_Account]windowsAdmin50


PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt;
</code></pre>
<p>No surprise, none of our resources are as they do not even exist.</p>
<p>From there, we can start the configuration using <code>Start-DscConfiguration</code> and passing the <code>Wait</code> parameter.</p>
<pre><code class="language-powershell">PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt; Start-DscConfiguration .\CreateLab_Configuration\ -Wait
PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt;
</code></pre>
<p>We do not get any output from <code>Start-DscConfiguration</code> which is exactly what we want. Running <code>Test-DscConfiguration</code> but this time looking at resources in the desired state shows us all nine of them.</p>
<pre><code class="language-powershell">PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt; Test-DscConfiguration .\CreateLab_Configuration\ | Select-Object -ExpandProperty ResourcesInDesiredState | Select-Object ResourceId

ResourceId
----------
[ADUser]iosharp\windowsReconcile50
[ADUser]iosharp\windowsAdmin50
[ADGroup]Domain Admins
[CYA_Safe]WinRec50
[CYA_Safe]WinAdmins50
[CYA_SafeMember]WinRec\CyberArk Administrators
[CYA_SafeMember]WinAdmins\Windows Administrators
[CYA_Account]windowsReconcile50
[CYA_Account]windowsAdmin50


PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt;
</code></pre>
<p>But what matters is what we see in CyberArk. Browsing to the PVWA, we see our two newly onboarded accounts in our newly created safes. <code>windowsAdmin50</code> has <code>windowsReconcile50</code> defined as it's reconcile just as we defined in our DSC configuration.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663355550955/wF-pLMLnp.png" alt="image.png" />

<h1>Changing existing Vault objects with PowerShell DSC</h1>
<p>Our configuration creates the objects when they are missing but what happens if they already exist in some form?</p>
<p>We make small changes to our accounts -- change their names, unlink the reconcile account, different platform -- with the goal that running <code>Start-DscConfiguration</code> will adjust them back to what is defined in our configuration.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663496265835/-w2pRHmqu.png" alt="image.png" />

<p>A quick <code>Test-DscConfiguration</code> shows that DSC knows the accounts in the Vault are incorrect based on the configuration:</p>
<pre><code class="language-powershell">PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt; Test-DscConfiguration .\CreateLab_Configuration\ | Select-Object -ExpandProperty ResourcesNotInDesiredState | Select-Object ResourceId

ResourceId
----------
[CYA_Account]windowsReconcile50
[CYA_Account]windowsAdmin50

PS C:\Users\Tim.IOSHARP\Desktop\DSC&gt;
</code></pre>
<p>Simply running <code>Start-DscConfiguration</code> 'silently' adjusts the accounts in CyberArk to their desired configuration.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663581070966/J56czAoxV.png" alt="image.png" />

<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1663581091383/wApMU8z6j.png" alt="image.png" />

<p>There we have it: the same configuration we used to create the Vault objects was used to correct them without having to do any changes.</p>
<p>The <code>CyberArkDsc</code> module can be found on GitHub. Currently it only has resources for safes, safe memberships, and accounts. Pull requests are welcome!</p>
<p><a class="embed-card" href="https://github.com/aaearon/CyberArkDsc">https://github.com/aaearon/CyberArkDsc</a></p>
]]></content:encoded></item><item><title><![CDATA[SecretManagement.CyberArk: An extension for the  SecretManagement PowerShell module]]></title><description><![CDATA[The SecretManagement PowerShell module provides a common interface to interact with a wide array of secret vaults enabled through SecretManagement extensions. There are a handful of SecretManagement e]]></description><link>https://timschindler.blog/secretmanagementcyberark-an-extension-for-the-secretmanagement-powershell-module</link><guid isPermaLink="true">https://timschindler.blog/secretmanagementcyberark-an-extension-for-the-secretmanagement-powershell-module</guid><category><![CDATA[cyberark]]></category><category><![CDATA[Powershell]]></category><category><![CDATA[Security]]></category><category><![CDATA[secrets]]></category><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Mon, 22 Aug 2022 13:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/-MMeeP7pjbE/upload/v1660995508913/0KTPc-jsN.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The <a href="https://docs.microsoft.com/en-us/powershell/utility-modules/secretmanagement/overview?view=ps-modules">SecretManagement</a> PowerShell module provides a common interface to interact with a wide array of secret vaults enabled through SecretManagement extensions. There are a handful of SecretManagement extensions, including for <a href="https://www.powershellgallery.com/packages/Az.KeyVault">Azure KeyVault</a>, <a href="https://www.powershellgallery.com/packages/SecretManagement.KeePass">KeePass</a>, and <a href="https://www.powershellgallery.com/packages/SecretManagement.LastPass">LastPass</a>.</p>
<p>The <code>SecretManagement</code> module supports having multiple secret vault types registered at the same time -- allowing you to access secrets in Azure KeyVault, KeePass, LastPass, etc. through the same cmdlets. The same secrets vault type can also be registered multiple times, allowing you to easily retrieve secrets from multiple KeePass databases.</p>
<p><a href="https://www.powershellgallery.com/packages/SecretManagement.CyberArk/">SecretManagement.CyberArk</a> is a SecretManagement extension that brings support to connecting to CyberArk (now Idira) using the Central Provider/Central Credential Provider (using <a href="https://github.com/pspete/CredentialRetriever/">CredentialRetriever</a>) or REST API (using <a href="https://github.com/pspete/psPAS">psPAS</a>).</p>
<h1>Using SecretManagement.CyberArk with the Central Credential Provider</h1>
<p>The <a href="https://github.com/aaearon/SecretManagement.CyberArk">SecretManagement.CyberArk README</a> provides the information we need to start working with the module.</p>
<p>The first step is to install and import both the <code>Microsoft.PowerShell.SecretManagement</code> and <code>SecretManagement.CyberArk</code> modules.</p>
<pre><code class="language-powershell">PS &gt; Install-Module Microsoft.PowerShell.SecretManagement
PS &gt; Install-Module SecretManagement.CyberArk
PS &gt; Import-Module Microsoft.PowerShell.SecretManagement
PS &gt; Import-Module SecretManagement.CyberArk
</code></pre>
<p>Next is to register a secret vault with the module <code>SecretManagement.CyberArk</code>. We give it a name to refer to when working with a secret and specify that we want to use the Central Credential Provider and pass the relevant details in <code>VaultParameters</code>.</p>
<pre><code class="language-powershell">PS &gt; $VaultParameters = @{
           ConnectionType       = 'CentralCredentialProvider'
           AppID                = 'windowsScript'
           URL                  = 'https://comp01'
           SkipCertificateCheck = $true
    }

PS &gt; Register-SecretVault -Name CyberArk -ModuleName SecretManagement.CyberArk -VaultParameters $VaultParameters
</code></pre>
<p><code>Get-SecretInfo</code> returns information about a secret: the name of the account, the data type of the secret when using <code>Get-Secret</code> and the <code>SecretManagement</code> Vault the secret is found in.</p>
<p>It also returns a <code>Metadata</code> property that shows information in CyberArk about the account.</p>
<pre><code class="language-powershell">PS &gt; Get-SecretInfo -VaultName CyberArk -Filter windowsAdmin01
Name                                                                    Type         VaultName
----                                                                    ----         ---------
Operating System-ioSHARPWindowsDomainAccount-iosharp.lab-windowsAdmin01 SecureString CyberArk

PS &gt; Get-SecretInfo -VaultName CyberArk -Filter windowsAdmin01 | Select-Object -ExpandProperty Metadata

Key                       Value
---                       -----
PolicyID                  ioSHARPWindowsDomainAccount
Folder                    Root
LastTask                  ChangeTask
CreationMethod            PVWA
Safe                      Windows
CPMStatus                 success
UserName                  windowsAdmin01
PasswordChangeInProcess   False
Address                   192.168.0.10
Name                      Operating System-ioSHARPWindowsDomainAccount-iosharp.lab-windowsAdmin01
LastSuccessVerification   1660836856
SequenceID                116
LastSuccessReconciliation 1660834970
DeviceType                Operating System
LastSuccessChange         1660923260
RetriesCount              -1

PS &gt;
</code></pre>
<p>We retrieve the secret in a similar way through <code>Get-Secret</code>, returned as a <code>SecureString</code>.</p>
<pre><code class="language-powershell">PS &gt; $Secret = Get-Secret -VaultName CyberArk -Name 'Operating System-ioSHARPWindowsDomainAccount-iosharp.lab-windowsAdmin01'
PS &gt; $Secret.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    SecureString                             System.Object

PS &gt; $Secret | ConvertFrom-SecureString -AsPlainText
UTyga8!ZOEDvPhQsrwlI(xjb2K$c
PS &gt;
</code></pre>
<p>The SecretManagement module supports adding and removing secrets through <code>Set-Secret</code> and <code>Remove-Secret</code> however as the Central Provider and Central Credential Provider do not support this functionality, these two cmdlets are only implemented for the <code>REST</code> ConnectionType.</p>
<h1>Working with multiple <code>SecretManagement.CyberArk</code> secret vaults</h1>
<p>We can register multiple <code>SecretManagement.CyberArk</code> secret vaults. These secret vaults can use different connection types.</p>
<pre><code class="language-powershell">PS &gt; $TestEnvironmentVaultParameters = @{
           ConnectionType = 'CredentialProvider'
           AppID          = 'linuxApp'
           ClientPath     = 'C:\Path\To\CLIPasswordSDK.exe'
      }

PS &gt; Register-SecretVault -Name CyberArkTest -ModuleName SecretManagement.CyberArk -VaultParameters $TestEnvironmentVaultParameters
PS &gt; $ProductionEnvironmentVaultParameters = @{
           ConnectionType       = 'CentralCredentialProvider'
           AppID                = 'windowsScript'
           URL                  = 'https://comp01'
     }

PS &gt; Register-SecretVault -Name CyberArkProduction -ModuleName SecretManagement.CyberArk -VaultParameters $ProductionEnvironmentVaultParameters
</code></pre>
<p>Executing <code>Get-SecretVault</code> shows us all our register secret vaults and their names so we can specify which Vault to get the secret from.</p>
<pre><code class="language-powershell">PS &gt; Get-SecretVault

Name               ModuleName                IsDefaultVault
----               ----------                --------------
CyberArkProduction SecretManagement.CyberArk False
CyberArkTest       SecretManagement.CyberArk False

PS &gt;
</code></pre>
<h1>Conclusion</h1>
<p>The <code>SecretManagement.CyberArk</code> extension allows you to retrieve secrets from multiple CyberArk environments using different connection methods through the same cmdlets provided by the <code>SecretManagement</code> module.</p>
<p><code>SecretManagement.CyberArk</code> can be found in the <a href="https://www.powershellgallery.com/packages/SecretManagement.CyberArk/">PowerShell Gallery</a>. The code is available on <a href="https://github.com/aaearon/SecretManagement.CyberArk">GitHub</a>.</p>
<p><a class="embed-card" href="https://github.com/aaearon/SecretManagement.CyberArk">https://github.com/aaearon/SecretManagement.CyberArk</a></p>
]]></content:encoded></item><item><title><![CDATA[Continuous Deployment of CyberArk (now Idira) Platforms using GitHub Actions]]></title><description><![CDATA[Management of CyberArk (now Idira) platforms can be difficult. They have numerous settings that are integral to how privileged accounts are used and their credentials managed. An incorrect configurati]]></description><link>https://timschindler.blog/continuous-deployment-of-cyberark-platforms-using-github-actions</link><guid isPermaLink="true">https://timschindler.blog/continuous-deployment-of-cyberark-platforms-using-github-actions</guid><category><![CDATA[cyberark]]></category><category><![CDATA[Powershell]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[continuous deployment]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Wed, 13 Jul 2022 16:43:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/842ofHC6MaI/upload/v1657726852834/IH9ec4Kdw.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Management of CyberArk (now Idira) platforms can be difficult. They have numerous settings that are integral to how privileged accounts are used and their credentials managed. An incorrect configuration could leave an account in an unusable state.</p>
<p>Platforms are typically staged across multiple environments. Whereas the REST API allows for easy export and import of platforms from environments, it lacks endpoints to comprehensively manage their settings once they are created.</p>
<p>Furthermore, it is common to have platforms recorded outside of CyberArk -- the XML-based visual representation of them in the PVWA often shoehorned inside a table in Confluence or Word -- with the necessity to keep the documentation accurate, which can be challenging in an environment with a large number of platforms and people who can make edits.</p>
<p>Being that platforms are <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.6/en/Content/Platforms/Platform-Packages-Import-Introduction.htm">represented by both an XML and INI file</a> we can ditch the awkward tables and stash them in a Git repository while reaping the rewards version control offers. From there, when it is a new platform, we can create a platform package with the help of the <a href="https://github.com/aaearon/New-PASExtensions">New-PASExtensions</a> module and import it into a CyberArk environment but how can we streamline this in the case where the platform already exists in the Vault and thus importing is not an option?</p>
<p>We could still keep manually updating platforms in CyberArk based on what is documented in our Git repository but this is still time-intensive and error prone. After understanding how the importing of platform packages works, we may be able to use part of or all of the functionality in order to develop a <a href="https://en.wikipedia.org/wiki/Continuous_deployment">continuous deployment</a> workflow.</p>
<h1>Platforms and platform packages</h1>
<p>The first step to seeing how our continuous deployment could leverage CyberArk's platform deployment mechanism is understanding how the entire import platform package process works. And that starts with understanding a platform package.</p>
<p>According to the <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.2/en/Content/Platforms/Platform-Packages-Import-Introduction.htm?tocpath=End%20user%7CPrivileged%20Accounts%7CClassic%20Interface%7CManage%20platforms%20classic%20interface%7C_____5">documentation</a> a platform package is straightforward: it is just a Zip file that contains a CPM policy file (an INI file with the name <code>Policy-\(PolicyId.ini</code>), a PVWA settings file (a XML file with the name <code>Policy-\)PolicyId.xml</code>), and (optionally) the CPM plugin files (executables, PowerShell scripts, TPC prompts and process files, or a combination.)</p>
<p>Once created, the Zip file is <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.2/en/Content/Platforms/Platform-Packages-Import-Introduction.htm?tocpath=End%20user%7CPrivileged%20Accounts%7CClassic%20Interface%7CManage%20platforms%20classic%20interface%7C_____5#ImportaPlatformPackage">imported through the PVWA</a> and the CPM plugin files are copied automatically to the CPM.</p>
<p>Whereas the documentation does a good job telling us what to do, it does not provide enough details on how it is exactly done and this "how" is what we need to really understand if we want or can use this process in our continuous deployment.</p>
<h2>Understanding the import process of a platform's package</h2>
<p>The best place to start is downloading a plugin from the <a href="https://cyberark-customers.force.com/mplace/s/">CyberArk Marketplace</a>, importing it, and observing the behavior.</p>
<p>I chose the <a href="https://cyberark-customers.force.com/mplace/s/#a352J000000pthhQAA-a392J000001h52sQAA">RealVNC Service Mode</a> as it is a TPC plugin using a PowerShell script with the classic prompts and process files, in addition to the required CPM policy and PVWA setting files, so we will see how those files are distributed.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655645497292/XmHYgYGkW.png" alt="image.png" />

<p>Upon importing the platform package for the first time we see that it is done successfully and is added under Imported Platforms:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655644697282/XQOL5xIyv.png" alt="image.png" />

<p>With the knowledge we already have from <a href="https://timschindler.blog/changing-cyberark-platform-properties-with-powershell">programmatically changing CyberArk platform properties using PowerShell</a> we know that the PVWA settings file <code>Policy-RealVNC.xml</code> was likely merged into <code>Policies.xml</code> in the <code>PVWAConfig</code> safe and the CPM policy file <code>Policy-RealVNC.ini</code> was dropped into the <code>Policies</code> folder of the <code>PasswordManagerShared</code> safe but what happened to the CPM plugin files made up of the PowerShell script and the prompts and process files?</p>
<p>As with anything to do with platforms and their files, the first place to look should be the <code>PasswordManagerShared</code> safe.</p>
<p>First lets take a quick look to make sure the CPM policy file <code>Policy-RealVNC.ini</code> landed in the <code>Policies</code> folder like we expected:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655645726671/r42Rz4ZN-.png" alt="image.png" />

<p>What is interesting to note is that as part of the import process the CPM policy file was renamed from <code>Policy-RealVNC.ini</code> to <code>Policy-RealVNCServiceMode.ini</code>, which is the <code>PolicyID</code> of the plugin.</p>
<p>Moving on to finding the CPM plugins files, inside the same safe we see the <code>ImportedPlatforms</code> folder and a sub-folder named <code>Policy-RealVNCServiceMode</code> that contains our CPM plugin files. The sub-folder seems to be named based off the convention <code>Policy-$PolicyId</code>, which is something we should keep in mind for our CD workflow:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655645427276/abfKj5y5M.png" alt="image.png" />

<p>Step 3 of <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.2/en/Content/Platforms/Platform-Packages-Import-Introduction.htm?tocpath=End%20user%7CPrivileged%20Accounts%7CClassic%20Interface%7CManage%20platforms%20classic%20interface%7C_____5">creating a platform package</a> explains that the CPM plugin files will be dumped into the <code>bin</code> folder of the CPM. Checking the folder and <code>pm.log</code> we see that is the case:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655646089559/u6UG2LGP-.png" alt="image.png" />

<p>The distribution of CPM plugin files is similar to that of what we see with the <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.2/en/Content/PASIMP/ConfigurePSMUniversalConnector.htm">deployment of Universal Connectors on multiple PSMs</a>. This works for the first deployment of a new platform but what about consecutive deployments for updated platforms or CPM plugin files?</p>
<h1>Updating platforms and CPM plugin files</h1>
<p>Although we manually imported the platform through the PVWA we know that an <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.2/en/Content/WebServices/ImportPlatform.htm?tocpath=Developer%7CREST%20APIs%7CPlatforms%7C_____3">API endpoint to do the same exists</a>. The best idea when needing to update a platform's settings or it's CPM plugin files would be to make the needed updates, re-create the platform package, and use the import platform package API endpoint to deploy the platform.</p>
<p>Unfortunately trying to import a package for a platform that shares the <code>PolicyID</code> or <code>PolicyName</code> of an existing platform is not possible. As the platform of an account is defined through an account property you could first <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/12.2/en/Content/SDK/rest-api-delete-target-platform.htm?tocpath=Developer%7CREST%20APIs%7CPlatforms%7CTarget%20Platforms%7C_____5">delete the platform</a> and then import it but at the very least any Master Policy exceptions are also deleted and must be manually re-created.</p>
<p>We saw that as part of the import process our platform's CPM plugin files were stashed in their own sub-folder under <code>ImportedPlatforms</code> in the <code>PasswordManagerShared</code> safe where they then ended up in the <code>bin</code> folder of the CPM. What happens when we upload a new version of an existing file directly into the Vault?</p>
<h2>Uploading new CPM plugin files into the Vault</h2>
<p>For the CPM plugin files we should download one of the files out of PrivateArk, make an inconsequential change to it, and observe the results.</p>
<p>Inside of <code>RealVNCProcess.ini</code> we add a new comment to the file:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655651635246/twRuL9VPF.png" alt="image.png" />

<p>and then upload it back into the safe:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655651704375/tmJX_31f9.png" alt="image.png" />

<p>After waiting some minutes, searching the <code>pm.log</code> for any log entry comes up with nothing:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655652070549/tE-YE1_sI.png" alt="image.png" />

<p>and inside the <code>bin</code> folder <code>RealVNCProcess.ini</code> is still the same:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655652156470/Yc1mxN-tW.png" alt="image.png" />

<p>Out of curiosity, lets see what happens when we restart the CPM service:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655652317839/035PS6dMR.png" alt="image.png" />

<p>Kicking the CPM service worked but having to restart the CPM service every time we want to deploy new CPM plugin files -- which could be countless times per day with our continuous deployment workflow -- across all our CPMs probably isn't what we want.</p>
<p>Looking again in <code>pm.log</code> when the plugin files are downloaded successfully, we always seeing a proceeding log message about a new policy being found. Can we trigger the downloading of new CPM plugin files by changing a superfluous value in the CPM policy file <code>Policy-RealVNCServiceMode.ini</code> and uploading that?</p>
<h2>Manually triggering the download of new CPM plugin files by the CPM</h2>
<p>We make another change to <code>RealVNCProcess.ini</code>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655652890586/yxmqzcMhl.png" alt="image.png" />

<p>and upload it to the Vault. We then head to the <code>Policies</code> folder in the <code>PasswordManagerShared</code> safe and download a copy of <code>Policy-RealVNCServiceMode.ini</code>.</p>
<p>But before we make any change to <code>Policy-RealVNCServiceMode.ini</code> lets simply upload that same exact copy back into the safe. This is worth trying because more than likely CyberArk is not comparing the content of files to know if there is an update rather just looking at the <code>Last Modified</code> date of the file -- which changes when the file is uploaded again.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1655653308426/g_PI0qh5L.png" alt="image.png" />

<p>Simply re-uploading <code>Policy-RealVNCServiceMode.ini</code> and thus it having a new <code>Last Modified</code> date was enough to trigger CyberArk to distribute our updated CPM plugin file.</p>
<h1>Putting it all together in a continuous deployment workflow</h1>
<p>I am using GitHub Actions with a self-hosted Windows runner as PACLI/PoShPACLI needs to communicate with the Vault.</p>
<p>FYI: All the code can be found in the below repos.</p>
<p><a class="embed-card" href="https://github.com/aaearon/cyberark-extensions-continuous-deployment">https://github.com/aaearon/cyberark-extensions-continuous-deployment</a></p>
<p><a class="embed-card" href="https://github.com/aaearon/Deploy-PASExtensions">https://github.com/aaearon/Deploy-PASExtensions</a></p>

<h2>Setting up the GitHub repository</h2>
<p>Still staying with the RealVNC CPM plugin, we create a repository on GitHub to store the platform files. All the plugin's files are stored in a folder that matches the platform's ID -- <code>RealVNCServiceMode</code> in our case -- in the <code>platforms</code> folder.</p>
<pre><code class="language-plaintext">PS C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery&gt; Get-ChildItem

    Directory: C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----           7/13/2022  4:27 PM                .git
d----           6/19/2022  1:40 PM                .github
d----           7/10/2022 11:19 AM                platforms
-a---           7/13/2022  4:27 PM            621 deploy.ps1
-a---           6/19/2022 12:16 PM           1070 LICENSE
-a---           7/13/2022  4:27 PM           1950 README.md

PS C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery&gt; Get-ChildItem .\platforms\RealVNCServiceMode\

    Directory: C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery\platforms\RealVNCServiceMode

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a---           7/10/2022 11:19 AM            235 LICENSE
-a---           7/13/2022  4:27 PM           6642 Policy-RealVNCServiceMode.ini
-a---           7/11/2022  2:40 PM            673 Policy-RealVNCServiceMode.xml
-a---           7/10/2022 11:19 AM           2566 realvnc.ps1
-a---           7/10/2022 11:19 AM           4177 RealVNCProcess.ini
-a---           7/10/2022 11:19 AM           1432 RealVNCPrompts.ini
</code></pre>
<p><em>Note that</em> <code>Policy-RealVNC.ini</code> <em>was renamed to</em> <code>Policy-RealVNCServiceMode.ini</code><em>!</em></p>
<p>Inside the root of the repository, I have created a small PowerShell script <a href="https://github.com/aaearon/cyberark-extensions-continuous-deployment/blob/main/deploy.ps1"><code>deploy.ps1</code></a> to be invoked by our GitHub Actions workflow that calls a custom function I wrote -- <code>Update-PASPlatformFiles.ps1</code>, part of the <a href="https://github.com/aaearon/Deploy-PASExtensions">Deploy-PASExtensions</a> PowerShell module -- that does the heavy lifting and uses PACLI/PoShPACLI to get the platform files -- regardless if they were changed -- in the Vault as part of the continuous deployment workflow:</p>
<pre><code class="language-powershell">param (
    [string]$PACLIClientPath,
    [PSCredential]$VaultCredential,
    [string]$VaultAddress,
    [string]$BasePlatformFolder
)

Import-Module PoShPACLI
Import-Module .\deploy-pasextensions\Deploy-PASExtensions\Deploy-PASExtensions.psd1

Set-PVConfiguration -ClientPath $PACLIClientPath
Start-PVPacli
New-PVVaultDefinition -vault \(VaultAddress -address \)VaultAddress -ErrorAction SilentlyContinue
Connect-PVVault -user \((\)VaultCredential.UserName) -password \((\)VaultCredential.Password)

(Get-ChildItem $BasePlatformFolder).FullName | Update-PASPlatformFiles

Disconnect-PVVault
Stop-PVPacli
</code></pre>
<p>The script is pretty simple. It imports the needed modules, builds up the PACLI session, and for each platform in <code>$BasePlatformFolder</code> updates it's files and then tears down the PACLI session.</p>
<p>The last step is <a href="https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository">adding the password of the user you will connect to the Vault via PACLI with as a secret to the GitHub repository</a>. Use <code>VAULT_PASSWORD</code> as the secret name otherwise you will need to adapt the workflow below with the correct secret name.</p>
<h2>Installing the GitHub Actions self-hosted runner</h2>
<p>Using the GitHub Actions documentation we install a <a href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners">self-hosted runner</a> and <a href="https://docs.github.com/en/actions/hosting-your-own-runners/adding-self-hosted-runners#adding-a-self-hosted-runner-to-a-repository">add it to the repository we created above</a>.</p>
<p>It also makes sense to make sure PACLI and <a href="https://github.com/pspete/PoShPACLI">PoShPACLI</a> are installed and working on the same machine the self-hosted runner is on.</p>
<h2>Creating the GitHub Actions workflow</h2>
<p>This is where the magic happens. In the workflow is where we define our jobs, the steps they consist of, and which actions trigger the workflow. Whereas GitHub Actions is extremely versatile and would allow us to trigger a workflow upon a merged pull request, for now we keep it simple and have the workflow trigger on each push.</p>
<pre><code class="language-yaml">name: Deploy platform files to CyberArk

on:
  push:
  workflow_dispatch:

jobs:
  deploy:
    name: Deploy platform files to the Vault
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v3

      - name: Checkout Deploy-PASExtensions repo
        uses: actions/checkout@v3
        with:
          repository: aaearon/deploy-pasextensions
          path: deploy-pasextensions

      - name: Uploads platform files to the Vault
        shell: powershell
        run: |
          $env:HOMEDRIVE = ""
          \(env:HomePath = "\){{ runner.temp }}"
          \(VaultCredential = New-Object System.Management.Automation.PSCredential ("Administrator", (ConvertTo-SecureString -String \){{ secrets.VAULT_PASSWORD }} -AsPlainText -Force))
          .\deploy.ps1 -PACLIClientPath 'C:\PACLI\Pacli.exe' -VaultCredential $VaultCredential -VaultAddress 192.168.0.50 -BasePlatformFolder '.\platforms\'
</code></pre>
<p>We need to overwrite <code>\(env:HOMEDRIVE</code> and <code>\)env:HomePath</code> as these do not seem to exist inside the runner and thus thrown an error. A <code>PSCredential</code> object is created with the <code>VAULT_PASSWORD</code> secret stored in the GitHub repository and is passed as an argument to <code>deploy.ps1</code> along with other arguments that match my target CyberArk environment.</p>
<p>At this point we can push everything to GitHub.</p>
<h1>Making a change that will kick off the continuous deployment workflow</h1>
<p>With the GitHub repository set up, self-hosted runner installed, and GitHub Actions workflow defined we are ready to see if it works.</p>
<p>We make two small changes to the <code>RealVNCServiceMode</code> platform, commit them, and then push: Change <code>ImmediateInterval</code> to 6 and add the <code>Banana</code> property as optional.</p>
<pre><code class="language-plaintext">PS C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery&gt; git show 36aa8ad2ec2fb1afe12c6f34bcbb794551ab0ae4
commit 36aa8ad2ec2fb1afe12c6f34bcbb794551ab0ae4 (HEAD -&gt; main, origin/main, origin/HEAD)
Author: Tim Schindler &lt;tim@iosharp.com&gt;
Date:   Wed Jul 13 17:01:15 2022 +0200

    add banana property and change immediateinterval

diff --git a/platforms/RealVNCServiceMode/Policy-RealVNCServiceMode.ini b/platforms/RealVNCServiceMode/Policy-RealVNCServiceMode.ini
index 93fbcbf..b78d453 100644
--- a/platforms/RealVNCServiceMode/Policy-RealVNCServiceMode.ini
+++ b/platforms/RealVNCServiceMode/Policy-RealVNCServiceMode.ini
@@ -2,7 +2,7 @@ PolicyID=RealVNCServiceMode                   ;Mandatory, unique identifier for
 PolicyName=Real VNC    Service Mode via PowerShell             ;Short description
 SearchForUsages=No                             ;Expected values: yes/no
 PolicyType=Regular                     ;Expected values: regular, usage, group
-ImmediateInterval=5                    ;In minutes
+ImmediateInterval=6                    ;In minutes
 Interval=1440                          ;In minutes
 MaxConcurrentConnections=3                             ;Expected values: integer
 AllowedSafes=.*                                                ;Regular expression of Safes pattern. Must be set if Reconciliation is enabled.
diff --git a/platforms/RealVNCServiceMode/Policy-RealVNCServiceMode.xml b/platforms/RealVNCServiceMode/Policy-RealVNCServiceMode.xml
index 549e6ba..0097911 100644
--- a/platforms/RealVNCServiceMode/Policy-RealVNCServiceMode.xml
+++ b/platforms/RealVNCServiceMode/Policy-RealVNCServiceMode.xml
@@ -7,7 +7,7 @@
                                        &lt;Property Name="Address" /&gt;
                                &lt;/Required&gt;
                                &lt;Optional&gt;
-
+                                       &lt;Property Name="Banana" /&gt;
                                        &lt;Property Name="Description" /&gt;
                                &lt;/Optional&gt;
                        &lt;/Properties&gt;
PS C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery&gt;
</code></pre>
<p>Upon seeing the <a href="https://github.com/aaearon/cyberark-extensions-continuous-deployment/actions/runs/2664355134">successful execution of the GitHub Actions workflow</a> lets look in the PVWA to see if our changes are reflected:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1657724945486/s56Kstvnw.png" alt="image.png" />

<p>Without having to do anything -- no restart of a CPM, no <code>iisreset</code> on the PVWAs, no changes to any platform in an attempt to force a 'refresh' of <code>Policies.xml</code> -- the changes in our commit take effect.</p>
<h1>Going forward</h1>
<p>We can see that it is (relatively) straightforward to have continuous delivery for platforms, including their CPM plugin files. Leveraging how imported platforms' plugin files are distributed automatically to all CPMs means we only have a single target system for our workflow -- the Vault.</p>
<p>Improving the GitHub Actions workflow to be more flexible and only deploying those files that have changed or only deploy upon a closed pull request and not using the Vault Administrator account when connecting via PACLI can lead to mature continuous deployment for all of our platforms -- enabling the ability to treat them "as code."</p>
]]></content:encoded></item><item><title><![CDATA[Load balancing CyberArk (now Idira) Privileged Session Manager for SSH (PSMP) with HAProxy and Expect]]></title><description><![CDATA[This is one post in a series focusing on load balancing various CyberArk (now Idira) components using HAProxy with a focus on application/service-based health checking.
Load balancing CyberArk Privile]]></description><link>https://timschindler.blog/load-balancing-cyberark-privileged-session-manager-for-ssh-psmp-with-haproxy-and-expect</link><guid isPermaLink="true">https://timschindler.blog/load-balancing-cyberark-privileged-session-manager-for-ssh-psmp-with-haproxy-and-expect</guid><category><![CDATA[#cybersecurity]]></category><category><![CDATA[Docker]]></category><category><![CDATA[infrastructure]]></category><category><![CDATA[Security]]></category><category><![CDATA[cyberark]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Mon, 14 Feb 2022 15:37:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644596138127/yccF2Cgv8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is one post in a</em> <a href="https://timschindler.blog/series/cyberark-haproxy"><em>series focusing on load balancing various CyberArk (now Idira) components using HAProxy with a focus on application/service-based health checking.</em></a></p>
<p>Load balancing <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PASIMP/Introduction-to-PSMP.htm">CyberArk Privileged Session Manager for SSH (often referred to as PSMP)</a> with application-based health checking using HAProxy is a bit more complicated than doing the same with <a href="https://timschindler.blog/application-health-checking-and-load-balancing-cyberark-privileged-vault-web-access-with-haproxy">CyberArk Privileged Web Access Vault</a> or the <a href="https://timschindler.blog/load-balancing-cyberark-privileged-session-manager-html5-gateway-with-haproxy">PSM HTML5 Gateway</a> but still accomplishable with the advanced features HAProxy provides.</p>
<p>Using Expect we can build a simple script that tests the functionality of the PSMP by proxying a connection to a target server and ensuring we get the expected password prompt, providing a known, good credential to authenticate with, and ultimately seeing the expected bash prompt of the target server. We configure can HAProxy to execute it as part of it's health check in order to achieve application-based health checking.</p>
<p>As we are already familiar with HAProxy from <a href="https://timschindler.blog/series/cyberark-haproxy">load balancing other CyberArk components</a> we can focus our energy on building the script used as part of the health check.</p>
<h1>What is Expect?</h1>
<p><a href="https://core.tcl-lang.org/expect/index">Expect</a> is '... a tool for automating interactive applications such as telnet, ftp, passwd, fsck, rlogin, tip, etc.' It '... "talks" to other interactive programs according to a script. Following the script, Expect knows what can be expected from a program and what the correct response should be. An interpreted language provides branching and high-level control structures to direct the dialogue. In addition, the user can take control and interact directly when desired, afterward returning control to the script.' [0] In fact, Expect is used by CyberArk Central Password Manager's <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PASIMP/Plug-in-Terminal-Plugin-Controller.htm">Terminal Plugin Controller</a> as part of some of it's plugins.</p>
<p>Put it more straightforwardly: we can use Expect and it's commands to <code>spawn</code> a ssh connection to a target server using a PSMP, <code>expect</code> the login prompt as a response, and <code>send</code> keystrokes to complete the authentication. If we see the bash prompt of the target server then we can reasonably assume the PSMP service is running and healthy</p>
<p>[0] <a href="https://www.tcl.tk/man/expect5.31/expect.1.html">https://www.tcl.tk/man/expect5.31/expect.1.html</a></p>
<h1>Formulating our health check</h1>
<p>Before introducing Expect lets first lets login manually to see what prompts we get. This will help us outline the steps we need to take in a script that will operate as our health check.</p>
<pre><code class="language-bash">tim@docker01:~$ ssh tim@admin01@linux01.iosharp.lab@192.168.121
The authenticity of host '192.168.0.121 (192.168.0.121)' can't be established.
ECDSA key fingerprint is SHA256:FuAf0xfImgbvwwn3l4wTvwNUc6K7ZpdQsZmJrjUb2jc.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '192.168.0.121' (ECDSA) to the list of known hosts.
Vault Password:

This session is being recorded
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-97-generic x86_64)

Last login: Tue Feb  8 14:25:40 2022 from 192.168.0.122
admin01@linux01:~$ exit
logout
Connection to linux01.iosharp.lab closed.
Connection to 192.168.0.121 closed.
tim@docker01:~$
</code></pre>
<p>What is important to take note is what we are prompted for so that we can include them as <code>expect</code> commands in a script and where we need to provide input to move the process along so we can provide input in <code>send</code> commands.</p>
<p>If we run through the login process again, keeping in mind how it would look with Expect commands, we:</p>
<ol>
<li><p><code>spawn</code> a SSH connection to the target server proxying through a PSMP</p>
</li>
<li><p><code>expect</code> to be prompted when connecting to a machine for the first time</p>
</li>
<li><p><code>send</code> the word 'yes' to continue the process where we</p>
</li>
<li><p><code>expect</code> to be prompted for our credentials in order to authenticate to the Vault</p>
</li>
<li><p><code>send</code> the credential for the Vault user we are authenticating</p>
</li>
<li><p><code>expect</code> the prompt of the target system we are connecting to</p>
</li>
<li><p><code>send</code> exit in order to close the connection</p>
</li>
</ol>
<p>We have our steps, now we just need to put it together in a script.</p>
<h1>Creating our Expect script</h1>
<p>After installing Expect using <code>apt</code> (my Docker host is running Ubuntu - depending on what your OS is your package manager may differ) we create <code>login.check</code> and store it in our home directory.</p>
<pre><code class="language-bash">tim@docker01:~$ touch login.expect
tim@docker01:~$ vi login.expect
</code></pre>
<p>With the steps above as a basis our <code>login.check</code> should look like</p>
<pre><code class="language-bash">#!/usr/bin/expect

spawn ssh tim@admin01@linux01.iosharp.lab@192.168.0.121
expect "yes/no"
send "yes\r"
expect "Vault Password:"
send "Password1234!\r"
expect "admin01@linux01:~$ "
send "exit\r"
</code></pre>
<p>At the top, similar to a Bash script, we define a <a href="https://linuxhandbook.com/shebang/">shebang</a> so that when we run the script the OS knows to invoke it with Expect.</p>
<p>Afterwards, the first thing we do is spawn a connection to a target server using the PSMP whose health we want to validate. In anticipation of an unknown SSH host key, we <code>expect "yes\no"</code> and then <code>send "yes\r"</code>. We do not have to define the exact string we expect to receive as Expect allows us to use <a href="https://en.wikipedia.org/wiki/Regular_expression">regular expressions</a> in our <code>expect</code> commands.</p>
<p>When using <code>send</code> we need to remember to provide a <a href="https://en.wikipedia.org/wiki/Carriage_return">carriage return</a> using <code>\r</code> when we 'emulating' pressing Enter. We do the same thing with the prompt <code>Vault Password:</code> where we provide our password.</p>
<p>Finally we expect to see the bash prompt <strong>of the target server we are connecting to</strong> and then end the script by ending the SSH connection.</p>
<p>Executing our script we can see the results:</p>
<pre><code class="language-bash">tim@docker01:~$ ./login.expect
spawn ssh tim@admin01@linux01.iosharp.lab@192.168.0.121
Vault Password:
Vault Password:tim@docker01:~$
</code></pre>
<p>It doesn't work. The SSH connection to the PSMP was successful as we received the <code>Vault Password:</code> prompt however we were not able to authenticate and eventually the timeout was reached and the script quit.</p>
<p>Because we were never prompted to accept the unknown SSH host key our script got hung up at that step. We can ensure we are never prompted regarding an unknown SSH host key by passing <code>-o StrictHostKeyChecking=no</code> as an argument to our SSH command and we can remove the <code>expect</code>/<code>send</code> from our script:</p>
<pre><code class="language-bash">#!/usr/bin/expect

spawn ssh -o StrictHostKeyChecking=no tim@admin01@linux01.iosharp.lab@192.168.0.121
expect "Vault Password:"
send "Password1234!\r"
expect "admin01@linux01:~$ "
send "exit\r"
</code></pre>
<p>We execute our script once more:</p>
<pre><code class="language-bash">tim@docker01:~$ ./login.expect
spawn ssh -o StrictHostKeyChecking=no tim@admin01@linux01.iosharp.lab@192.168.0.121
Vault Password:

This session is being recorded
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-97-generic x86_64)

Last login: Tue Feb  8 15:47:06 2022 from 192.168.0.122
admin01@linux01:~\( tim@docker01:~\)
</code></pre>
<p>It works! We have a functioning basis for our health check but we should extend our script a bit more to handle some known behavior when the PSMP is <em>not</em> working.</p>
<h1>Handling known, non-working behavior in Expect</h1>
<p>Expect allows us to throw exit codes. These exit codes can be used by other processes, like HAProxy, to know if the script completed fine or with an error.</p>
<p>Our script already handles the situation where the PSMP is working fine but we should attempt to capture the behavior of a non-functional PSMP and account for it in our script. We can then throw an exit code other than <code>0</code>, which signifies the login process and thus our script encountered a problem.</p>
<p>Like we did with crafting our Expect script with a working PSMP, lets see what the behavior of a PSMP is when the service is shut off.</p>
<p>With the PSMP service disabled, lets try to connect to a target server:</p>
<pre><code class="language-bash">tim@docker01:~$ ssh tim@admin01@linux01.iosharp.lab@192.168.0.121
Vault Password:
Vault Password:
Vault Password:
tim@admin01@linux01.iosharp.lab@192.168.0.121: Permission denied (publickey,keyboard-interactive).
tim@docker01:~$
</code></pre>
<p>Despite putting in the correct password, we were prompted two more times and finally the connection closed.</p>
<p>Lets update our script to reflect this behavior when the PSMP service is not running:</p>
<pre><code class="language-bash">#!/usr/bin/expect

spawn ssh -o StrictHostKeyChecking=no tim@admin01@linux01.iosharp.lab@192.168.0.121
expect {
    "Vault Password:" { send "Password1234!\r" }
}
expect {
    "admin01@linux01:~$ " { exit 0 }
    "Vault Password:" { exit 1 }
}
</code></pre>
<p>We refactor our script a bit when extending it to handle the case where the PSMP service is not running by organizing the statements into <code>expect</code> blocks.</p>
<p>Within our first <code>expect</code> block we handle the first prompt -- <code>Vault Password:</code> and send our password when prompted.</p>
<p>In the second <code>expect</code> block we handle what comes after the first prompt. If we receive the target server's expected bash prompt -- our success case -- we close with <code>exit 0</code>. If instead we receive <code>Vault Password:</code> once more, indicating that the login process with our known, good password failed, then we throw <code>exit 1</code>. We need these error codes as HAProxy will use them to determine the health of the server.</p>
<p>With the PSMP service still stopped, lets invoke our script and check the error code:</p>
<pre><code class="language-bash">tim@docker01:~$ ./login.expect
spawn ssh -o StrictHostKeyChecking=no tim@admin01@linux01.iosharp.lab@192.168.0.121
Vault Password:
Vault Password:tim@docker01:~\( echo \)?
1
tim@docker01:~$
</code></pre>
<p>We use <code>$?</code> to get the exit code of <strong>the last command executed.</strong> As any exit code other than <code>0</code> indicates an error, this is exactly what we want. For good measure we should re-enable the PSMP service, run our script, and check the exit code:</p>
<pre><code class="language-bash">tim@docker01:~$ ./login.expect
spawn ssh -o StrictHostKeyChecking=no tim@admin01@linux01.iosharp.lab@192.168.0.121
Vault Password:
Last failed login: Tue Feb  8 20:57:39 CET 2022 from 192.168.0.115 on ssh:notty
There were 3 failed login attempts since the last successful login.

This session is being recorded
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-97-generic x86_64)

Last login: Tue Feb  8 20:00:27 2022 from 192.168.0.122
admin01@linux01:~\( tim@docker01:~\) echo $?
0
tim@docker01:~$
</code></pre>
<p>Both success and failure behavior is covered and return the appropriate exit codes. With our script in a suitable place lets introduce HAProxy into the mix.</p>
<h1>HAProxy and <code>haproxy.cfg</code></h1>
<p>What HAProxy is, it's capabilities, and what a basic and functioning <code>haproxy.cfg</code> looks like is well covered in the posts where we load balance the <a href="https://timschindler.blog/application-health-checking-and-load-balancing-cyberark-privileged-vault-web-access-with-haproxy#heading-what-is-haproxy">PVWA</a> and <a href="https://timschindler.blog/load-balancing-cyberark-privileged-session-manager-html5-gateway-with-haproxy#heading-creating-our-haproxycfg">HTML5 Gateway</a>.</p>
<p>Once again we use Docker to run HAProxy however unlike done previously we cannot simply use the an existing image and mount our <code>haproxy.cfg</code> as we intend to use a script that leverages Expect and the OpenSSH client -- two programs not found in the <a href="https://hub.docker.com/_/haproxy/">HAProxy Docker images provided by the HAProxy team</a>. We need to built our own Docker image that includes our needed dependencies.</p>
<p>This this done by creating our own <a href="https://docs.docker.com/engine/reference/builder/">Dockerfile</a> which is nowhere as daunting as sounds as we can use an existing HAProxy Docker image as a base.</p>
<p>In a new working directory, move our script and create our <code>Dockerfile</code>:</p>
<pre><code class="language-bash">tim@docker01:~$ mkdir haproxy-psmp
tim@docker01:~$ mv login.expect haproxy-psmp/
tim@docker01:~$ vi haproxy-psmp/Dockerfile
</code></pre>
<p>with the content</p>
<pre><code class="language-plaintext">FROM haproxy:latest

USER root
RUN apt-get update &amp;&amp; apt-get install -y expect openssh-client &amp;&amp; apt-get clean

USER haproxy
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
COPY login.expect /var/lib/haproxy/login.expect
</code></pre>
<p>Before we build an image based on our <code>Dockerfile</code> we need to create the <code>haproxy.cfg</code> in our working directory with frontends and backends:</p>
<pre><code class="language-plaintext">defaults
  timeout client 10s
  timeout connect 10s
  timeout server 10s

global
  insecure-fork-wanted
  external-check

frontend psmp_loadbalancer
  bind *:22
  default_backend psmp_servers

backend psmp_servers
  option tcp-check
  tcp-check expect string SSH-2.0-
  option external-check
  external-check command /var/lib/haproxy/login.expect
  server psmp01 192.168.0.122:22 check inter 31s
  server psmp02 192.168.0.121:22 check inter 31s

frontend stats
  mode http
  bind *:8404
  stats enable
  stats uri /stats
  stats refresh 10s
  stats admin if TRUE
</code></pre>
<p>Focusing on what is in our <code>backend</code>, we have two health checks: a <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#tcp-check%20expect"><code>tcp-check expect</code></a> that works similar to our Expect script in that it expects to receive a certain string -- the SSH banner -- when first connecting to a SSH server and an <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#option%20external-check"><code>option external-check</code></a> that invokes the script we created. If the first check fails, most likely to the OpenSSH service not running, the HAProxy marks the server as unhealthy and doesn't bother with subsequent ones.</p>
<p>As <code>optional external-check</code> invokes processes and thus poses a security risk, we also need to explicitly add <code>insecure-fork-wanted</code> and <code>external-check</code> under the <code>global</code> section to tell HAProxy we really want to use it.</p>
<p>We still are not ready to build our image as our script has the target PSMP hard coded. <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#external-check%20command"><code>external-check command</code></a> passes arguments four arguments to the command, the third being the address of the server the health check is being ran against. We need to update our script to take this argument.</p>
<p>As we already updating our script, we also take the opportunity to disable logging to <a href="https://www.computerhope.com/jargon/s/stdout.htm">Stdout</a> by setting <code>user_log 0</code>, create a CyberArk user <code>healthcheck</code> to explicitly authenticate to the Vault with for the purpose of our health check, and define the full path to the ssh binary. Our script now looks like:</p>
<pre><code class="language-bash">#!/usr/bin/expect
user_log 0
set psmp [lindex $argv 2]

spawn /usr/bin/ssh -o StrictHostKeyChecking=no healthcheck@admin01@linux01.iosharp.lab@$psmp
expect {
    "Vault Password:" { send "Password1234!\r" }
}
expect {
    "admin01@linux01:~$ " { exit 0 }
    "Vault Password:" { exit 1 }
}
</code></pre>
<h1>Building our Docker image and running our container</h1>
<p>Building the image is simple. We just need to run <code>docker build</code> with a tag we will remember:</p>
<pre><code class="language-bash">tim@docker01:~/haproxy-psmp$ sudo docker build -t haproxy-psmp .
Sending build context to Docker daemon  4.608kB
Step 1/6 : FROM haproxy:latest
 ---&gt; 4313a6c47541
Step 2/6 : USER root
 ---&gt; Using cache
 ---&gt; 974bba5aa714
Step 3/6 : RUN apt-get update &amp;&amp; apt-get install -y expect openssh-client &amp;&amp; apt-get clean
 ---&gt; Using cache
 ---&gt; 625afeb7fa22
Step 4/6 : USER haproxy
 ---&gt; Using cache
 ---&gt; 2bba1cf696a7
Step 5/6 : COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
 ---&gt; 8ee4cfac7f22
Step 6/6 : COPY login.expect /var/lib/haproxy/login.expect
 ---&gt; 4672cc1f4c34
Successfully built 4672cc1f4c34
Successfully tagged haproxy-psmp:latest
tim@docker01:~/haproxy-psmp$
</code></pre>
<p>Now we just start the container, publishing the ports for our backend and stats. As we include our <code>haproxy.cfg</code> and <code>login.expect</code> as part of the image we do not need to mount them.</p>
<pre><code class="language-bash">tim@docker01:~/haproxy-psmp$ sudo docker run \
    -p 8404:8404 \
    -p 23:22 \ 
    haproxy-psmp:latest
</code></pre>
<p><em>As we already have our Docker host's SSH daemon running on port 22, we need to have Docker publish HAProxy on 23.</em></p>
<p>Our container starts successfully and in the HAProxy logs we see processes exiting with code <code>0</code>, indicating the health check using our script is firing and ending successfully.</p>
<pre><code class="language-bash">tim@docker01:~/haproxy-psmp$ sudo docker run -p 8404:8404 -p 23:22 haproxy-psmp:latest
[NOTICE]   (1) : New worker (8) forked
[NOTICE]   (1) : Loading success.
[NOTICE]   (1) : haproxy version is 2.5.1-86b093a
[NOTICE]   (1) : path to executable is /usr/local/sbin/haproxy
[WARNING]  (1) : Process 12 exited with code 0 (Exit)
[WARNING]  (1) : Process 17 exited with code 0 (Exit)
[WARNING]  (1) : Process 22 exited with code 0 (Exit)
[WARNING]  (1) : Process 27 exited with code 0 (Exit)
[WARNING]  (1) : Process 32 exited with code 0 (Exit)
</code></pre>
<p>Checking our stats page, we see both PSMPs as green.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644399952658/eTq69wnsv.png" alt="Screenshot 2022-02-09 at 10.45.22.png" style="display:block;margin:0 auto" />

<p>Shutting off one of the PSMP services it goes red.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644400464862/WzBObCNI2.png" alt="Screenshot 2022-02-09 at 10.53.45.png" style="display:block;margin:0 auto" />

<p>We are still able to login through our HAProxy:</p>
<pre><code class="language-bash">tim@docker01:~/haproxy-psmp$ ssh tim@admin01@linux01.iosharp.lab@192.168.0.115 -p 23
Vault Password:

This session is being recorded
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-97-generic x86_64)

Last login: Wed Feb  9 09:55:44 2022 from 192.168.0.122
admin01@linux01:~$ whoami
admin01
admin01@linux01:~$ hostname
linux01
admin01@linux01:~$ exit
logout
Connection to linux01.iosharp.lab closed.
Connection to 192.168.0.115 closed.
tim@docker01:~/haproxy-psmp$
</code></pre>
<h1>Going forward</h1>
<p>Though the health checking is more complicated than with the PVWA and HTML5 Gateway due to requiring the usage of an external script leveraging Except but being able to run HAProxy and the script in it's own Docker container makes the entire setup more manageable.</p>
<p>We can also add additional, non-working behavior in our script, a good candidate being when we receive <code>Password:</code> instead of <code>Vault Password:</code>, indicating there maybe something wrong with the <a href="https://www.redhat.com/sysadmin/pluggable-authentication-modules-pam">pluggable authentication module</a> stack.</p>
<p>How are you checking the health of your PSMPs? Provide some feedback in the comments!</p>
]]></content:encoded></item><item><title><![CDATA[Load balancing CyberArk (now Idira) Privileged Session Manager HTML5 Gateway with HAProxy]]></title><description><![CDATA[This is one post in a series focusing on load balancing various CyberArk (now Idira) components using HAProxy with a focus on application/service-based health checking.
With the experience we gained f]]></description><link>https://timschindler.blog/load-balancing-cyberark-privileged-session-manager-html5-gateway-with-haproxy</link><guid isPermaLink="true">https://timschindler.blog/load-balancing-cyberark-privileged-session-manager-html5-gateway-with-haproxy</guid><category><![CDATA[Docker]]></category><category><![CDATA[proxy]]></category><category><![CDATA[Security]]></category><category><![CDATA[CyberSec]]></category><category><![CDATA[cyberark]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Tue, 08 Feb 2022 12:11:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1643833106852/P6b0xJ3h5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>This is one post in a</em> <a href="https://timschindler.blog/series/cyberark-haproxy"><em>series focusing on load balancing various CyberArk (now Idira) components using HAProxy with a focus on application/service-based health checking.</em></a></p>
<p>With the experience we gained from <a href="https://timschindler.blog/application-health-checking-and-load-balancing-cyberark-privileged-vault-web-access-with-haproxy">load balancing CyberArk Privileged Vault Web Access with HAProxy</a>, load balancing the Privileged Session Manager HTML5 GW (PSM HTML5 GW) while incorporating application-based health checking with HAProxy is simple. CyberArk already provides an endpoint we can use for the check so it is all about crafting our HAProxy configuration file.</p>
<h1>Setting up HAProxy</h1>
<p>Similar to how we did it with <a href="https://timschindler.blog/application-health-checking-and-load-balancing-cyberark-privileged-vault-web-access-with-haproxy#heading-setting-up-haproxy">load balancing the PVWA</a> we will use Docker and a HAProxy Docker image, passing our <code>haproxy.cfg</code> and TLS certificates in mounted volumes.</p>
<p>Lets create a directories to store our <code>haproxy.cfg</code> and certificate with it's private key.</p>
<pre><code class="language-bash">tim@docker01:~$ mkdir -p haproxy-html5gw/haproxy
tim@docker01:~$ mkdir -p haproxy-html5gw/certificates
</code></pre>
<p>And generate a self-signed certificate. For any production environment we would make sure to use an issued certificate from a trusted certificate authority.</p>
<pre><code class="language-bash">tim@docker01:~$ openssl req -x509 -newkey rsa:4096 -keyout haproxy-html5gw/certificates/tls.pem.key -out haproxy-html5gw/certificates/tls.pem -sha256 -days 365 -nodes
Generating a RSA private key
.....................................++++
.....................................................................................................................................++++
writing new private key to 'haproxy-html5gw/certificates/tls.pem.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:192.168.0.115
Email Address []:
tim@docker01:~$
</code></pre>
<p>We created the certificate and it's private key under our personal user and with the file permissions the private key is created with the HAProxy process the user runs as cannot read the private key. Already knowing that the HAProxy process runs under the user ID 99 we go ahead and change ownership of the private key.</p>
<pre><code class="language-bash">tim@docker01:~$ ls -la haproxy-html5gw/certificates/tls.pem.key
-rw------- 1 tim tim 3272 Feb  1 15:36 haproxy-html5gw/certificates/tls.pem.key
tim@docker01:~$ chown 99 haproxy-html5gw/certificates/tls.pem.key
tim@docker01:~$ ls -la haproxy-html5gw/certificates/tls.pem.key
-rw------- 1 99 tim 3272 Feb  1 15:36 haproxy-html5gw/certificates/tls.pem.key
tim@docker01:~$
</code></pre>
<h1>Creating our <code>haproxy.cfg</code></h1>
<p>In <code>haproxy.cfg</code> we define our frontend and backend sections.</p>
<p>In the frontend section we define on which address and port HAProxy accepts traffic on and the TLS certificate used to encrypt traffic between HAProxy and the client. In the backend section we define the the individual servers HAProxy will relay traffic to. We can also define a load balancing strategy, health checking, and persistent sessions.</p>
<p>Lets start with a basic <code>haproxy.cfg</code>:</p>
<pre><code class="language-bash">tim@docker01:~$ vi haproxy-html5gw/haproxy/haproxy.cfg
</code></pre>
<p>with the content</p>
<pre><code class="language-plaintext">frontend html5gw_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend html5gws

backend html5gws
  server html5gw1 192.168.0.113:443 ssl verify none
</code></pre>
<p>We define a frontend to listen on port 443 and tell it to use the certificate we generated earlier (<em>HAProxy will look for a private key with the name</em> <code>&lt;certificate filename&gt;.key</code> <em>in the same directory.</em>) With <code>default_backend</code> we define the name of the backend that traffic coming from the frontend should be relayed to.</p>
<p>In the backend section we define a single HTML5GW to relay the traffic to. In reality we will have multiple but we can define just one and still be able to implement our concepts. The server name is arbitrary and we can call it whatever we want but the backend must be named <code>html5gws</code> as we used it as the value for <code>default_backend</code> in the frontend.</p>
<p>After specifying the address and IP of the server we state to connect with ssl and again because this is a lab environment we allow for invalid TLS certificates on the backend HTML5 servers with <code>verify none</code>.</p>
<p>We are ready to start our container with the certificates folder and the folder containing our <code>haproxy.cfg</code> as mounted volumes.</p>
<pre><code class="language-bash">tim@docker01:~$ sudo docker run -p 443:443 -v /home/tim/haproxy-html5gw/haproxy:/usr/local/etc/haproxy:ro -v /home/tim/haproxy-html5gw/certificates:/usr/local/etc/certs:ro haproxy:latest
[NOTICE]   (1) : haproxy version is 2.5.1-86b093a
[NOTICE]   (1) : path to executable is /usr/local/sbin/haproxy
[WARNING]  (1) : config : missing timeouts for frontend 'html5gw_loadbalancer'.
   | While not properly invalid, you will certainly encounter various problems
   | with such a configuration. To fix this, please ensure that all following
   | timeouts are set to a non-zero value: 'client', 'connect', 'server'.
[WARNING]  (1) : config : missing timeouts for backend 'html5gws'.
   | While not properly invalid, you will certainly encounter various problems
   | with such a configuration. To fix this, please ensure that all following
   | timeouts are set to a non-zero value: 'client', 'connect', 'server'.
[NOTICE]   (1) : New worker (8) forked
[NOTICE]   (1) : Loading success.
</code></pre>
<p>Ignoring the warnings about the timeouts that we will fix later, we navigate to <code>https://192.168.0.115/guac/direct</code> and are shown an error:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643732064831/qX22TZJwH.png" alt="Screenshot 2022-02-01 at 17.14.05.png" style="display:block;margin:0 auto" />

<p>This error is fine and is not indicative of anything in our <code>haproxy.cfg</code> being wrong. When browsing directly to <code>https://192.168.0.115/guac/direct</code> our browser is making a <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET">GET request</a>, which is not supported by the HTML5 GW. Looking at the HTML5GW logs we can see the GET request being made:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643732279878/WPvNQoWHC.png" alt="Screenshot 2022-02-01 at 17.17.46.png" style="display:block;margin:0 auto" />

<p>Now that we verified that HAProxy is relaying traffic received from the frontend to the backend we can start implementing our application-based health check and sticky sessions.</p>
<h1>Adding application health checking</h1>
<p>Like we could have with the PVWA, we can enable basic health checking by adding the <code>check</code> keyword to our server defined in the backend section but we still run the risk of HAProxy relaying traffic to a HTML5 GW in a scenario where the Tomcat (or whatever web server you are using to host the HTML 5 GW Web App) server is running but the Apache Guacamole service that the HTML5 GW is based off of is not functional.</p>
<p><code>option httpchk</code> enables us to have more complex health checking. It makes a request to the server and if the HTTP response code is either 2xx or 3xx then HAProxy considers the server healthy and keeps relaying traffic to it. Without defining a URI as part of <code>option httpchk</code> the health check is done against <code>/</code>. As the HTML5 GW Web App lives under the <code>/gauc</code> path, not defining another URI may not give us true application-based health checking.</p>
<p>The <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PAS%20INST/Install_PSM_HTML5.htm?tocpath=Installation%7CInstall%20PAM%7CInstall%20PSM%7CAdvanced%20PSM%20Implementations%7CInstall%20PSM%20HTML5%20Gateway%7C_____0#HealthCheck">documentation for the PSM HTML5 GW</a> specifies a URI we should use when making our health check.</p>
<p>We need to make sure we specify <code>/guac/rest/healthcheck</code> with our <code>option httpchk</code> resulting in our <code>haproxy.cfg</code> looking like:</p>
<pre><code class="language-plaintext">frontend html5gw_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend html5gws

backend html5gws
  option httpchk GET /guac/rest/healthcheck
  server html5gw1 192.168.0.113:443 check ssl verify none
</code></pre>
<p>We also must add the <code>check</code> keyword to our server otherwise the parameters of the health check are defined via <code>option httpchk</code> but the health check will never happen and the server will always be considered healthy.</p>
<h1>Sticky sessions</h1>
<p>In the same <a href="https://timschindler.blog/application-health-checking-and-load-balancing-cyberark-privileged-vault-web-access-with-haproxy#heading-sticky-sessions">way we did it with the PVWA</a> we will use <a href="https://www.haproxy.com/blog/introduction-to-haproxy-stick-tables/">stick tables</a> again to implement sticky sessions although HAProxy offers more than one way to accomplish sticky sessions.</p>
<p>Our stick table captures client IP addresses and associates them to a backend server. The first time a client connects, HAProxy relays the traffic to a server and associates the client IP address to the server in the stick table. The next time the client IP address connects, HAProxy looks up the associated server in the stick table and relays the traffic to that server. We set a long expiration date of 36 hours for this association as our PSMs are configured to close idle PSM sessions at this point.</p>
<pre><code class="language-bash">frontend html5gw_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend html5gws

backend html5gws
  stick-table type ip size 1m expire 36h
  stick on src
  option httpchk GET /guac/rest/healthcheck
  server html5gw1 192.168.0.113:443 check ssl verify none
</code></pre>
<h1>Adding <code>stats</code></h1>
<p>We <a href="https://timschindler.blog/application-health-checking-and-load-balancing-cyberark-privileged-vault-web-access-with-haproxy#heading-finishing-it-off-with-stats">already know how useful it is having a stats page</a> that we can use to see numerous information for our frontends and backends, among that the health of the servers in the backend. We use it to see various statistics regarding our frontends and backends and as we specify <code>stats admin if TRUE</code> we can perform administrative tasks like disabling health checks on servers or marking individual servers as healthy or unhealthy.</p>
<pre><code class="language-bash">defaults
  timeout client 10s
  timeout connect 10s
  timeout server 10s

frontend html5gw_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend html5gws

backend html5gws
  stick-table type ip size 1m expire 36h
  stick on src
  option httpchk GET /guac/rest/healthcheck
  server html5gw1 192.168.0.113:443 check ssl verify none

frontend stats
  mode http
  bind *:8404
  stats enable
  stats uri /stats
  stats refresh 10s
  stats admin if TRUE
</code></pre>
<p>We also define values under <code>defaults</code> for the timeouts that HAProxy was complaining about missing when we first created our <code>haproxy.cfg</code>.</p>
<h1>Adding a second HTML5 GW</h1>
<p>So far we have been working with a single HTML5 GW but for sake of better being able to test our new <code>haproxy.cfg</code> lets add a second HTML5 GW into the mix. Because I am <a href="https://timschindler.blog/cyberark-html5-gateway-and-docker-compose">managing my HTML5 GW containers with Docker Compose</a> it is trivial to spin up a second HTML5 GW running on the same Docker host but with a different published port.</p>
<pre><code class="language-bash">defaults
  timeout client 10s
  timeout connect 10s
  timeout server 10s

frontend html5gw_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend html5gws

backend html5gws
  stick-table type ip size 1m expire 36h
  stick on src
  option httpchk GET /guac/rest/healthcheck
  server html5gw1 192.168.0.113:443 check ssl verify none
  server html5gw2 192.168.0.113:8443 check ssl verify none

frontend stats
  mode http
  bind *:8404
  stats enable
  stats uri /stats
  stats refresh 10s
  stats admin if TRUE
</code></pre>
<h1>Testing our <code>haproxy.cfg</code> with the help of <code>stats</code></h1>
<p>We start our container once more:</p>
<pre><code class="language-bash">tim@docker01:~$ sudo docker run \
    -p 8404:8404 \
    -p 443:443 \ 
    -v /home/tim/haproxy-html5gw/haproxy:/usr/local/etc/haproxy:ro \
    -v /home/tim/haproxy-html5gw/certificates:/usr/local/etc/certs:ro \
    haproxy:latest
</code></pre>
<p>Browsing to the stats we see frontend and backend -- and a bunch of green, which is a good sign!</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643830775273/YXOfdFurXh.png" alt="Screenshot 2022-02-02 at 20.39.23.png" style="display:block;margin:0 auto" />

<p>We navigate to <code>https://192.168.0.115/gauc/direct</code> and though we get an error as expected, refreshing the stats page shows all our connections being relayed to a single server.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643834071411/uZA0o8BO4.png" alt="Screenshot 2022-02-02 at 21.33.45.png" style="display:block;margin:0 auto" />

<h1>Doing more with HAProxy</h1>
<p>Our <code>haproxy.cfg</code> only scratches the surface in terms of the functionality HAProxy offers. We can adjust how often the health check happens by specifying the <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#5.2-inter"><code>inter</code> parameter</a> for our servers if we find the default not aggressive enough and explicitly pick a load balancing method with the <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#4-balance"><code>balance</code> keyword</a>.</p>
<p>As HAProxy is a Layer 4 proxy we can even load balance the <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#option%20forwardfor">Privileged Session Manager</a> and the <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PAS%20INST/Installing-the-Privileged-Session-Manager-SSH-Proxy.htm?tocpath=Installation%7CInstall%20PAM%7CInstall%20PSM%20for%20SSH%7C_____0">Privileged Session Manager for SSH</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Application health checking and load balancing CyberArk (now Idira) Privileged Vault Web Access with HAProxy]]></title><description><![CDATA[The Privileged Vault Web Access is the most straightforward component to load balance in CyberArk (now Idira)'s Privileged Access Security solution but despite this it is still easy to misconfigure on]]></description><link>https://timschindler.blog/application-health-checking-and-load-balancing-cyberark-privileged-vault-web-access-with-haproxy</link><guid isPermaLink="true">https://timschindler.blog/application-health-checking-and-load-balancing-cyberark-privileged-vault-web-access-with-haproxy</guid><category><![CDATA[Security]]></category><category><![CDATA[proxy]]></category><category><![CDATA[Docker]]></category><category><![CDATA[CyberSec]]></category><category><![CDATA[cyberark]]></category><category><![CDATA[Idira]]></category><dc:creator><![CDATA[Tim Schindler]]></dc:creator><pubDate>Sat, 29 Jan 2022 17:36:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1643477694710/NCzEBBtr2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Privileged Vault Web Access is the most straightforward component to load balance in CyberArk (now Idira)'s Privileged Access Security solution but despite this it is still easy to misconfigure one of the most important aspects: the health check.</p>
<p>It is possible to use whatever your load balancing solution's 'default' health check is but this typically leaves you with a health check that is server-based health check versus application-based. This could result in your load balancer still relaying traffic to a server where IIS is running but the PVWA service is not functional.</p>
<p>Whereas CyberArk provides documentation for other components on leveraging various endpoints to accomplish application-based health checks (<a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PSM/psm_healthcheck.htm">Privileged Session Manager</a>, <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PAS%20INST/Install_PSM_HTML5.htm?tocpath=Installation%7CInstall%20PAS%7CInstall%20PSM%7CAdvanced%20PSM%20Implementations%7CInstall%20PSM%20HTML5%20Gateway%7C_____0#HealthCheck">Privileged Session Manager HTML5 Gateway</a>), for the PVWA it offers only <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PAS%20INST/PVWA-install-multiple-PVWA-env.htm?tocpath=Installation%7CInstall%20PAS%7CInstall%20PVWA%7C_____8#Loadbalancerrequirements">the most basic load balancing requirements</a>.</p>
<p>Using HAProxy we will set up load balancing with a health check that evaluates the health of the PVWA service (our 'application') and not just IIS.</p>
<h1>What is HAProxy?</h1>
<p>From the official <a href="https://www.haproxy.org/">HAProxy</a> website it is '... a free, very fast and reliable reverse-proxy offering high availability, load balancing, and proxying for TCP and HTTP-based applications.' It can meet the basic load balancing requirements for the PVWA and also the 'notable' load balancer settings found in <a href="https://cyberark-customers.force.com/s/article/Load-Balancer-Implementation-with-PVWA-General-Guidelines">this knowledge article</a>. And just like the description implies we can use it to load balance in more complex scenarios like the load balancing PSMs.</p>
<h1>Setting up HAProxy</h1>
<p>Like most software HAProxy is provided as a Docker image. With Docker we can quickly get HAProxy up and running, passing our <code>haproxy.cfg</code> and TLS certificates in mounted volumes.</p>
<p>We will need a place to store our <code>haproxy.cfg</code> which configures HAProxy and a place to store our certificate and it's private key so lets first create those directories.</p>
<pre><code class="language-bash">tim@docker01:~$ mkdir haproxy
tim@docker01:~$ mkdir haproxy/haproxy
tim@docker01:~$ mkdir haproxy/certificates
</code></pre>
<p>We will generate a self-signed certificate as this is a lab environment but in production we would want a certificate issued from a trusted certificate authority.</p>
<pre><code class="language-bash">tim@docker01:~$ cd haproxy/certificates/
tim@docker01:~/haproxy/certificates$ openssl req -x509 -newkey rsa:4096 -keyout tls.pem.key -out tls.pem -sha256 -days 365 -nodes
Generating a RSA private key
..........++++
.......................++++
writing new private key to 'tls.pem.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:192.168.0.115
Email Address []:
tim@docker01:~/haproxy/certificates$
</code></pre>
<p>With the file permissions and ownership the private key is created with the HAProxy process inside the container will not be able to read it. We will change the file to be owned by the user ID the HAProxy runs as inside the container.</p>
<pre><code class="language-bash">tim@docker01:~/haproxy/certificates$ chown 99 tls.pem.key
</code></pre>
<h1>Creating our <code>haproxy.cfg</code></h1>
<p>In a <code>haproxy.cfg</code> you will see two primary sections: one to represent the frontend and one to represent the backend.</p>
<p>In the frontend section, we specify settings around HAProxy receiving traffic. This includes configurations like the addresses and ports HAProxy is listening for traffic on, the TLS certificate used to secure communication between HAProxy and the client, as well as options to redirect HTTP traffic to HTTPS.</p>
<p>In the backend section, we specify the servers HAProxy will relay traffic to as well as options like load balancing strategy, more sophisticated health checking, and persistent sessions.</p>
<p>We will start with something simple just to ensure it works:</p>
<pre><code class="language-bash">tim@docker01:~/haproxy/haproxy$
tim@docker01:~/haproxy/haproxy$ vi haproxy.cfg
</code></pre>
<p>with the content</p>
<pre><code class="language-plaintext">frontend pvwa_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend pvwas

backend pvwas
  server pvwa1 192.168.0.102:443 ssl verify none
  server pvwa2 192.168.0.116:443 ssl verify none
</code></pre>
<p>Our <code>haproxy.cfg</code> is extremely basic. We define a frontend to listen on port 443 and tell it to use the certificate we generated earlier (<em>we can just define the certificate and HAProxy will look for a private key with the name</em> <code>&lt;certificate filename&gt;.key</code> <em>in the same directory, which in our case is named</em> <code>tls.pem.key</code><em>.</em>) With <code>default_backend</code> we define the name of the backend that traffic coming from the frontend should be relayed to.</p>
<p>In the backend section we define the individual servers traffic will be relayed to. The names <code>pvwa1</code> and <code>pvwa2</code> for the servers are arbitrary and we can call them whatever we want but the backend must be named <code>pvwas</code> as we used it as the value for the <code>default_backend</code> in the frontend. After specifying the address and IP of the server we state to connect with ssl and again because this is a lab environment we allow for invalid TLS certificates on the backend PVWA servers with <code>verify none</code>.</p>
<p>With our certificates generated and our initial <code>haproxy.cfg</code> we can start our container, making sure to mount the local volumes.</p>
<pre><code class="language-bash">tim@docker01:~/haproxy$ sudo docker run -p 443:443 -v /home/tim/haproxy/haproxy:/usr/local/etc/haproxy:ro -v /home/tim/haproxy/certificates:/usr/local/etc/certs:ro haproxy:latest
[NOTICE]   (1) : haproxy version is 2.5.1-86b093a
[NOTICE]   (1) : path to executable is /usr/local/sbin/haproxy
[WARNING]  (1) : config : missing timeouts for frontend 'pvwa_loadbalancer'.
   | While not properly invalid, you will certainly encounter various problems
   | with such a configuration. To fix this, please ensure that all following
   | timeouts are set to a non-zero value: 'client', 'connect', 'server'.
[WARNING]  (1) : config : missing timeouts for backend 'pvwas'.
   | While not properly invalid, you will certainly encounter various problems
   | with such a configuration. To fix this, please ensure that all following
   | timeouts are set to a non-zero value: 'client', 'connect', 'server'.
[NOTICE]   (1) : New worker (8) forked
[NOTICE]   (1) : Loading success.
</code></pre>
<p>Starting the container we see a few warnings about the lack of timeouts but navigating to the PVWA through our load balancer it loads!</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643143481262/VWwoEsvzM.png" alt="Screenshot 2022-01-25 at 21.43.27.png" style="display:block;margin:0 auto" />

<p>But after logging in we immediately get an error about our session expiring.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643143561475/U2DsXmJInj.png" alt="Screenshot 2022-01-25 at 21.44.04.png" style="display:block;margin:0 auto" />

<p>Our basic <code>haproxy.cfg</code> is a bit too basic. There is no sticky sessions and we do not have a load balancing method configured so HAProxy is rotating through the PVWAs, sending the next request to the next server. Plus: we do not have ANY health checking, let alone something that could be considered an application-based health check.</p>
<h1>Adding sticky sessions and application health checking</h1>
<p>We can add basic health checks by simply adding the <code>check</code> keyword to each of servers defined in the backend section but this alone will not check the health of the PVWA rather just of IIS. We need to also add the <a href="http://cbonte.github.io/haproxy-dconv/2.5/configuration.html#4-option%20httpchk">option httpchk</a> keyword to our backend section.</p>
<p><code>option httpchk</code> allows us to define a URI. It will make a request to that URI and if the response returns a HTTP code of 2xx or 3xx then HAProxy considers the server healthy.</p>
<p>As we want HAProxy to not relay traffic to a PVWA where the PVWA service is not working or cannot connect to the Vault even though IIS is running, we need to find the right URI that will return something that is NOT 2xx or 3xx under those conditions. To find this, lets start with looking at requests made in Firefox when we browse to a functional PVWA with the idea that one or more return should something other than 2xx or 3xx when something is wrong with the PVWA.</p>
<h2>Finding a URI for our health check</h2>
<p>Lets approach it how a customer would by browsing directly to one of the PVWAs in Firefox. With the <a href="https://developer.mozilla.org/en-US/docs/Tools/Network_Monitor">Network Monitor tab in focus of Web Developer tools</a> open and filtering on just HTTP and XHR type of requests we navigate to one of the PVWAs:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643204660257/tgzZIPNh2.png" alt="Screenshot 2022-01-26 at 14.42.50.png" style="display:block;margin:0 auto" />

<p>Looking at all the requests we want to focus on the ones that return 200. In theory all of these would be suitable for the health check but it could be that 200 is still returned even though the PVWA is not working. We should purposely break the PVWA and see what is returned then.</p>
<p>Lets use the Windows Firewall and <a href="https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-firewall/create-an-outbound-port-rule">create a rule on the PVWA blocking outbound traffic on port 1858</a>:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643465552009/CRkI2XEhT.png" alt="image.png" style="display:block;margin:0 auto" />

<p>Ensuring that our rule is enabled we refresh the PVWA:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643466672766/wLF4mrv2Y.png" alt="Screenshot 2022-01-29 at 15.30.29.png" style="display:block;margin:0 auto" />

<p>Even without a connection to the Vault the PVWA still loads -- sort of. In a non-functioning state IIS still returned a 200 for the first few requests which loaded the logon screen minus the fields to put in credentials. The request to <code>https://192.168.0.102/PasswordVault/api/settings/authentication</code> failed, however, so it seems like a perfect candidate to use for our health check.</p>
<p>Lets add it as part of a <code>option httpchk</code> in our backend in <code>haproxy.cfg</code> while at the same time adding values for the timeout configurations we saw warnings for when starting HAProxy the first time:</p>
<pre><code class="language-plaintext">defaults
  timeout client 10s
  timeout connect 10s
  timeout server 10s

frontend pvwa_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend pvwas

backend pvwas
  option httpchk GET /PasswordVault/api/settings/authentication
  server pvwa1 192.168.0.102:443 check ssl verify none
  server pvwa2 192.168.0.116:443 check ssl verify none
</code></pre>
<p>In addition we had to add the <code>check</code> keyword to each of the servers in our backend that we want it's health checked. We need this otherwise our <code>option httpchk</code> is effectively worthless.</p>
<h2>Sticky sessions</h2>
<p>Sticky sessions can be accomplished a multitude of ways including via cookies or with hashing-based methods. We will use <a href="https://www.haproxy.com/blog/introduction-to-haproxy-stick-tables/">stick tables</a>.</p>
<p>We will use a stick table to capture the information of client IP addresses and associate it to a backend server. Every time a client IP address connects HAProxy will look in the stick table to see the previously-associated server and relay the traffic to it. We also set a time for this association to expire so a client is not eternally 'stuck' with a particular server.</p>
<pre><code class="language-plaintext">defaults
  timeout client 10s
  timeout connect 10s
  timeout server 10s

frontend pvwa_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend pvwas

backend pvwas
  stick-table type ip size 1m expire 15m
  stick on src
  option httpchk GET /PasswordVault/settings/authentication
  server pvwa1 192.168.0.102:443 check ssl verify none
  server pvwa2 192.168.0.116:443 check ssl verify none
</code></pre>
<h1>Finishing it off with <code>stats</code></h1>
<p>HAProxy provides a convenient stats page that we can use to see numerous information for our frontends and backends, among that the health of the servers in the backend. As a last act before we start our container with our new <code>haproxy.cfg</code> lets add a frontend for the stats page.</p>
<pre><code class="language-plaintext">defaults
  timeout client 10s
  timeout connect 10s
  timeout server 10s

frontend pvwa_loadbalancer
  bind *:443 ssl crt /usr/local/etc/certs/tls.pem
  default_backend pvwas

backend pvwas
  option httpchk GET /PasswordVault/api/settings/authentication
  server pvwa1 192.168.0.102:443 check ssl verify none
  server pvwa2 192.168.0.116:443 check ssl verify none

frontend stats
  mode http
  bind *:8404
  stats enable
  stats uri /stats
  stats refresh 10s
  stats admin if TRUE
</code></pre>
<p>We run the <code>stats</code> page on it's own frontend. It will auto refresh every 10 seconds and because we are still in a lab environment we use the <code>stats admin</code> to always return <code>TRUE</code> (<a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#4.2-stats%20admin">if we want this production we should protect it with at least basic authentication like shown in the HAProxy examples</a>) so we can perform administrative tasks like disabling health checks on servers or the entire backend, marking individual servers as healthy or unhealthy.</p>
<p>We also need to remember to publish the port for our stats page when we start our container.</p>
<h1>Running and testing our new <code>haproxy.cfg</code></h1>
<p>First we start our container like in the beginning but with the port 8404 published for our stats page:</p>
<pre><code class="language-plaintext">docker run -p 443:443 -p 8404:8404 -v /home/tim/haproxy/haproxy:/usr/local/etc/haproxy:ro -v /home/tim/haproxy/certificates:/usr/local/etc/certs:ro haproxy:latest
</code></pre>
<p>When we load the stats page we can see both the frontends and the backend we defined along with various statistics about them. On our backend we are able to administrator the individual servers or the backend as a whole:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643228411341/y9kfVQ-EK.png" alt="Screenshot 2022-01-26 at 21.17.46.png" style="display:block;margin:0 auto" />

<p>Looking at one of the PVWAs we can observe the IIS logs to see the health check:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643475261851/yKZopiNb2.png" alt="Screenshot 2022-01-29 at 17.53.58.png" style="display:block;margin:0 auto" />

<p>Stopping the IIS service or re-enabling the Windows Firewall rule we create to simulate the inability for the PVWA to connect to the Vault, the server in the backend goes red in our stats page and traffic is no longer relayed to it:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1643228710471/a9ljWOd_I.png" alt="Screenshot 2022-01-26 at 21.24.54.png" style="display:block;margin:0 auto" />

<h1>Going forward</h1>
<p>With a simple configuration we have a powerful load balancer with an application-based health check. We can effortlessly add more functionality like the <code>X-Forwarded-For</code> header using the <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#option%20forwardfor"><code>option forwardfor</code></a> keyword to <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PAS%20INST/PVWA-install-multiple-PVWA-env.htm#Loadbalancerrequirements">capture the real client IP address when accessing the PVWA behind our load balancer</a>, HTTP to HTTPS auto redirection with <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#4.2-redirect%20scheme"><code>redirect scheme</code></a>, and defining a load balance method with the <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#balance"><code>balance</code></a> method.</p>
<p>As HAProxy is a Layer 4 load balancer it is not limited to just HTTP use cases. It can be used to load balance <a href="https://cbonte.github.io/haproxy-dconv/2.5/configuration.html#option%20forwardfor">Privileged Session Manager</a> and <a href="https://docs.cyberark.com/Product-Doc/OnlineHelp/PAS/Latest/en/Content/PAS%20INST/Installing-the-Privileged-Session-Manager-SSH-Proxy.htm?tocpath=Installation%7CInstall%20PAM%7CInstall%20PSM%20for%20SSH%7C_____0">Privileged Session Manager for SSH</a>.</p>
]]></content:encoded></item></channel></rss>