Continuous Deployment of CyberArk Platforms using GitHub Actions

Photo by Yancy Min on Unsplash

Continuous Deployment of CyberArk Platforms using GitHub Actions

Management of CyberArk 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.

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.

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.

Being that platforms are represented by both an XML and INI file 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 New-PASExtensions 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?

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 continuous deployment workflow.

Platforms and platform packages

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.

According to the documentation a platform package is straightforward: it is just a Zip file that contains a CPM policy file (an INI file with the name Policy-$PolicyId.ini), a PVWA settings file (a XML file with the name Policy-$PolicyId.xml), and (optionally) the CPM plugin files (executables, PowerShell scripts, TPC prompts and process files, or a combination.)

Once created, the Zip file is imported through the PVWA and the CPM plugin files are copied automatically to the CPM.

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.

Understanding the import process of a platform's package

The best place to start is downloading a plugin from the CyberArk Marketplace, importing it, and observing the behavior.

I chose the RealVNC Service Mode 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.


Upon importing the platform package for the first time we see that it is done successfully and is added under Imported Platforms:


With the knowledge we already have from programmatically changing CyberArk platform properties using PowerShell we know that the PVWA settings file Policy-RealVNC.xml was likely merged into Policies.xml in the PVWAConfig safe and the CPM policy file Policy-RealVNC.ini was dropped into the Policies folder of the PasswordManagerShared safe but what happened to the CPM plugin files made up of the PowerShell script and the prompts and process files?

As with anything to do with platforms and their files, the first place to look should be the PasswordManagerShared safe.

First lets take a quick look to make sure the CPM policy file Policy-RealVNC.ini landed in the Policies folder like we expected:


What is interesting to note is that as part of the import process the CPM policy file was renamed from Policy-RealVNC.ini to Policy-RealVNCServiceMode.ini, which is the PolicyID of the plugin.

Moving on to finding the CPM plugins files, inside the same safe we see the ImportedPlatforms folder and a sub-folder named Policy-RealVNCServiceMode that contains our CPM plugin files. The sub-folder seems to be named based off the convention Policy-$PolicyId, which is something we should keep in mind for our CD workflow:


Step 3 of creating a platform package explains that the CPM plugin files will be dumped into the bin folder of the CPM. Checking the folder and pm.log we see that is the case:


The distribution of CPM plugin files is similar to that of what we see with the deployment of Universal Connectors on multiple PSMs. This works for the first deployment of a new platform but what about consecutive deployments for updated platforms or CPM plugin files?

Updating platforms and CPM plugin files

Although we manually imported the platform through the PVWA we know that an API endpoint to do the same exists. 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.

Unfortunately trying to import a package for a platform that shares the PolicyID or PolicyName of an existing platform is not possible. As the platform of an account is defined through an account property you could first delete the platform and then import it but at the very least any Master Policy exceptions are also deleted and must be manually re-created.

We saw that as part of the import process our platform's CPM plugin files were stashed in their own sub-folder under ImportedPlatforms in the PasswordManagerShared safe where they then ended up in the bin folder of the CPM. What happens when we upload a new version of an existing file directly into the Vault?

Uploading new CPM plugin files into the Vault

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.

Inside of RealVNCProcess.ini we add a new comment to the file: image.png and then upload it back into the safe: image.png

After waiting some minutes, searching the pm.log for any log entry comes up with nothing:


and inside the bin folder RealVNCProcess.ini is still the same:


Out of curiosity, lets see what happens when we restart the CPM service:


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.

Looking again in pm.log 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 Policy-RealVNCServiceMode.ini and uploading that?

Manually triggering the download of new CPM plugin files by the CPM

We make another change to RealVNCProcess.ini:


and upload it to the Vault. We then head to the Policies folder in the PasswordManagerShared safe and download a copy of Policy-RealVNCServiceMode.ini.

But before we make any change to Policy-RealVNCServiceMode.ini 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 Last Modified date of the file -- which changes when the file is uploaded again.


Simply re-uploading Policy-RealVNCServiceMode.ini and thus it having a new Last Modified date was enough to trigger CyberArk to distribute our updated CPM plugin file.

Putting it all together in a continuous deployment workflow

I am using GitHub Actions with a self-hosted Windows runner as PACLI/PoShPACLI needs to communicate with the Vault.

FYI: All the code can be found in the below repos.

Setting up the GitHub repository

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 -- RealVNCServiceMode in our case -- in the platforms folder.

PS C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery> 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

PS C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery> 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

Note that Policy-RealVNC.ini was renamed to Policy-RealVNCServiceMode.ini!

Inside the root of the repository, I have created a small PowerShell script deploy.ps1 to be invoked by our GitHub Actions workflow that calls a custom function I wrote -- Update-PASPlatformFiles.ps1, part of the Deploy-PASExtensions 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:

param (

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

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

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


The script is pretty simple. It imports the needed modules, builds up the PACLI session, and for each platform in $BasePlatformFolder updates it's files and then tears down the PACLI session.

The last step is adding the password of the user you will connect to the Vault via PACLI with as a secret to the GitHub repository. Use VAULT_PASSWORD as the secret name otherwise you will need to adapt the workflow below with the correct secret name.

Installing the GitHub Actions self-hosted runner

Using the GitHub Actions documentation we install a self-hosted runner and add it to the repository we created above.

It also makes sense to make sure PACLI and PoShPACLI are installed and working on the same machine the self-hosted runner is on.

Creating the GitHub Actions workflow

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.

name: Deploy platform files to CyberArk


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

      - name: Checkout Deploy-PASExtensions repo
        uses: actions/checkout@v3
          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 -BasePlatformFolder '.\platforms\'

We need to overwrite $env:HOMEDRIVE and $env:HomePath as these do not seem to exist inside the runner and thus thrown an error. A PSCredential object is created with the VAULT_PASSWORD secret stored in the GitHub repository and is passed as an argument to deploy.ps1 along with other arguments that match my target CyberArk environment.

At this point we can push everything to GitHub.

Making a change that will kick off the continuous deployment workflow

With the GitHub repository set up, self-hosted runner installed, and GitHub Actions workflow defined we are ready to see if it works.

We make two small changes to the RealVNCServiceMode platform, commit them, and then push: Change ImmediateInterval to 6 and add the Banana property as optional.

PS C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery> git show 36aa8ad2ec2fb1afe12c6f34bcbb794551ab0ae4
commit 36aa8ad2ec2fb1afe12c6f34bcbb794551ab0ae4 (HEAD -> main, origin/main, origin/HEAD)
Author: Tim Schindler <>
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 @@
                                        <Property Name="Address" />
+                                       <Property Name="Banana" />
                                        <Property Name="Description" />
PS C:\Users\Tim\Projects\cyberark-extensions-continuous-delivery>

Upon seeing the successful execution of the GitHub Actions workflow lets look in the PVWA to see if our changes are reflected:


Without having to do anything -- no restart of a CPM, no iisreset on the PVWAs, no changes to any platform in an attempt to force a 'refresh' of Policies.xml -- the changes in our commit take effect.

Going forward

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.

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."