Cisco Secure Client + Umbrella, Part 3: Deployment

Putting the pieces together to install Cisco Secure Client on your macOS fleet will look something like this.

Cisco Secure Client + Umbrella, Part 3: Deployment

It's time. You're ready to move from Cisco OpenDNS Umbrella Roaming Client 3.x to Cisco Secure Client with the Umbrella Module. You've got your MDM profiles and all the installer files you need to install the app. Putting the pieces together will look something like this.

Condition

Begin with a check to see if your target Macs are running Cisco Secure Client - your target path is /Applications/Cisco/Cisco Secure Client.app. Nothing fancy here.

One thing to note - while you can install Cisco Secure Client on top of Cisco OpenDNS Roaming Client, this app is also taking over for Cisco AnyConnect VPN. If you want to opt for a slower but safer approach to your installation logic, consider checking to see if there's an active VPN session before you proceed:

### Do not proceed if a VPN connection is live ###
if /opt/cisco/secureclient/bin/vpn status | grep "Connected"; then
  echo "Connection is live, delay to next check-in"
  exit 1
fi

# Check old version of AnyConnect for VPN connection #
if /opt/cisco/anyconnect/bin/vpn status | grep "Connected"; then
  echo "Connection is live, delay to next check-in"
  exit 1
else
  echo "No connection, proceed with install/update"
  exit 0
fi
### End VPN Connection check ###

Check for a CSC or AnyConnect VPN session.

In the above example, we're invoking Cisco's vpn binary's status command to see if the VPN is currently connected If so, we exit 1 to come back later, otherwise we exit 0 to proceed.

Installation

Stage all of your MDM profiles first. With those set, attach your two primary installation files:

  • Your copy of the Cisco Secure Client DMG, possibly converted to read-only with an edited ACTransforms.xml file
  • Your custom-configured Choices.xml file, configured to install only the components you want

You'll notice I didn't say you need your OrgInfo.json file from the Umbrella portal. To make this installer scalable, we'll feed that content into our script as variables so that the installer can be used for multiple organizations.

Variables

For our first block of code, let's look at our list of variables:

############ VARIABLES ############
###################################
# Installer variables
cs="Cisco Secure Client - AnyConnect VPN and Umbrella - Universal (5.1.2.42)"
dmgName="cisco-secure-client-macos-5.1.2.42-predeploy-k9.dmg"
volumeName="Cisco Secure Client 5.1.2.42"
pkgName="Cisco Secure Client.pkg"
xmlFile="secureClient5_install_choices_vpn_and_umbrella.xml"

# OrgInfo.plist variables - structured to allow local override of gloabl variables if needed
local_organizationId="$adgl_ciscoSecure_organizationId"
local_fingerprint="$adgl_ciscoSecure_fingerprint"
local_userId="$adgl_ciscoSecure_userId"

############ /VARIABLES ###########
###################################

Variables

In order, we're defining:

  1. cs - the name of our working directory (custom software). In the finished script below, this will be called with the default Addigy ansible directory path, change this as needed for your MDM.
  2. dmgName - the name of the uploaded DMG file.
  3. volumeName - the name of the volume that appears when the DMG is mounted.
  4. pkgName - the name of the installation PKG inside the DMG.
  5. xmlFile - the name of your choices.xml file.

Next, the three parts of the OrgInfo.json file:

  1. local_organizationID - Your target organization ID.
  2. local_fingerprint - The unique fingerprint for the file.
  3. local_userId - The user ID of the Cisco account that generated the OrgInfo.json file.

In this example, we're taking some Addigy global variables (I prefix these variables with adgl for "Addigy global" to keep tabs on them) and feeding them into separate, local variables. This way, for testing purposes, we could override these values to anything we want without modifying any other part of the installer script, or moving our test devices between policies. Feel free to customize this to fit your MDM's variable functionality.

Safety Bailout

Next, we'll include a bailout option to stop the installer if any of the OrgInfo.json values are missing. This is mostly important if you're running the script supported by global variables and haven't manually provided static values in the code:

# SAFETY BAILOUT #
# Halt if variables are not present
if [[ "$local_organizationId" == "" ]]; then
  echo "[FAIL] No org ID detected, unable to proceed with installation."
  echo "Note that this installer cannot be triggered successfully via Golive at the moment and must be sent as part of a policy deployment."
  exit 1
fi
if [[ "$local_fingerprint" == "" ]]; then
  echo "[FAIL] No fingerprint detected, unable to proceed with installation."
  echo "Note that this installer cannot be triggered successfully via Golive at the moment and must be sent as part of a policy deployment."
  exit 1
fi
if [[ "$local_userId" == "" ]]; then
  echo "[FAIL] No user ID detected, unable to proceed with installation."
  echo "Note that this installer cannot be triggered successfully via Golive at the moment and must be sent as part of a policy deployment."
  exit 1
fi
# /SAFETY BAILOUT #

Safety Bailout

Here, we go through each variable associated with the OrgInfo.json file - if any of them are blank, the script outputs which value is blank and throws an exit 1. This can be invaluable in Addigy, as global variables are not respected if run via GoLive (as of May 2024, though this function is "coming soon"). This way, should one of your technicians run the script by GoLive, it will fail before it takes any major actions and provide useful output so they can discern what happened.

Create OrgInfo.json

Now that we know our OrgInfo.json values are valid, let's build that file:

# Deploy JSON config to /opt/cisco/secureclient/Umbrella/

DIR="/opt/cisco/secureclient/Umbrella"
if [ -d "$DIR" ]; then
    echo "$DIR exists"
else
    echo "Creating directory"
    mkdir -p "$DIR"
    chmod 777 "$DIR"
fi

pathPlist="/opt/cisco/secureclient/Umbrella/OrgInfo.json"

# If plist exists, delete it and start over
if [[ -f "$pathPlist" ]]; then
    echo "$pathPlist exists"
    ###FILE FOUND, so let's delete it
    rm "$pathPlist"
  else
    echo "$pathPlist not found"
fi

# Creating file
echo "Creating OrgInfo file"
	cat >> "$pathPlist" <<EOF
{
    "organizationId" : "$local_organizationId",
    "fingerprint" : "$local_fingerprint",
    "userId" : "$local_userId"
}
EOF
	# Set the correct permissions
	chmod 644 "$pathPlist"

echo "OrgInfo file created"

Create your OrgInfo.json file

In this section, we're checking to see if our destination directory exists, creating it if needed, and giving it wide-open permissions (feel free to use more restrictive permissions as you move from testing to production). We then define the name and path of the JSON file, delete any existing files at that path for a clean start, and use cat to write the file in the correct format. Finally, we set the permissions for the file and we're all set to go.

Fun piece of trivia, in my testing the destination directory can be either /opt/cisco/secureclient/Umbrella/ or /opt/cisco/secureclient/umbrella/, both seem to work just fine.

Optional: Duo Certificate Override

In our environment, we didn't opt to include the Duo components baked into the Cisco Secure Client installer. If you are using it, though, you may want to drop this extra section into your installer during initial testing:

# Create flagfile to ignore Duo certificate
flagFile="/Library/Application Support/Duo/Duo Device Health/DisableMacOS11CertManagement"
DIR2="/Library/Application Support/Duo/Duo Device Health"
if [ -d "$DIR2" ]; then
   echo "$DIR2 exists"
else
    echo "Creating directory"
    mkdir -p "$DIR2"
    chmod 777 "$DIR2"
fi
touch "$flagFile"
echo "Cert flagFile created"

Optional code to not fail the installation if the Duo certificate isn't detected

If the expected Duo certificate is not detected while you install Cisco Secure Client with the Duo components checked, your installation attempt will fail. Creating this flag file will allow your installation to continue. If you already have the certs in production, omit this code to ensure the app doesn't install without all the required Duo components.

Mount the DMG and install

We're finally ready to run the installer package:

# Attach
hdiutil attach -nobrowse "/Library/Addigy/ansible/packages/$cs/$dmgName"
# Install
sudo installer -pkg "/Volumes/$volumeName/$pkgName" -applyChoiceChangesXML "/Library/Addigy/ansible/packages/$cs/$xmlFile" -target /
# Detach
sudo hdiutil detach "/Volumes/$volumeName/"

Install the app

We'll use hdiutil to mount the DMG with an attach command, run our installer with the applyChoiceChangesXML argument pointed at our Choices.xml file, then unmount the DMG to clean up after ourselves. All that buildup for such a simple installation command.

Kill the old Umbrella Menu

This is another optional part of the installer, but it can be considerate to try and clean up the old Umbrella process if it's still running:

# Pkill old copy of Umbrella
 if pgrep -x "UmbrellaMenu"; then
    echo "Orphaned UmbrellaMenu detected, closing"
      if [[ -f /Applications/OpenDNS\ Roaming\ Client/rcuninstall ]]; then
      echo "Running Umbrella 3.x uninstaller"
      /Applications/OpenDNS\ Roaming\ Client/rcuninstall
      fi
    killall UmbrellaMenu
    sudo pkill UmbrellaMenu
  fi

Clean up the old UmbrellaMenu process

This checks if the OpenDNS Umbrella Roaming client GUI is still running. If it is, this will try to run the bundled uninstallation script if it's still on the Mac and close the associated, orphaned process.

Code summary

Congratulations - that was a marathon. If you've done your job between all the required MDM profiles and insatllation details, you should now have a quiet installer for Cisco Secure Client. Your code will overwrite OpenDNS Roaming Client while respecting any active AnyConnect VPN sessions, minimizing user disruption.

Users will still receive a Managed Login Items Notification Center notification when the installer runs, but that should be the only interactive part of the process. Best of luck getting your fleets updated for the April 2025 cutoff!

Here's the full code for the installer script below - note that the Duo section has been commented out.

############ VARIABLES ############
###################################
# Installer variables
cs="Cisco Secure Client - AnyConnect VPN and Umbrella - Universal (5.1.2.42)"
dmgName="cisco-secure-client-macos-5.1.2.42-predeploy-k9.dmg"
volumeName="Cisco Secure Client 5.1.2.42"
pkgName="Cisco Secure Client.pkg"
xmlFile="secureClient5_install_choices_vpn_and_umbrella.xml"

# OrgInfo.plist variables - structured to allow local override of gloabl variables if needed
local_organizationId="$adgl_cisco_organizationId"
local_fingerprint="$adgl_cisco_fingerprint"
local_userId="$adgl_cisco_userId"

############ /VARIABLES ###########
###################################

# SAFETY BAILOUT #
# Halt if variables are not present
if [[ "$local_organizationId" == "" ]]; then
  echo "[FAIL] No org ID detected, unable to proceed with installation."
  echo "Note that this installer cannot be triggered successfully via Golive at the moment and must be sent as part of a policy deployment."
  exit 1
fi
if [[ "$local_fingerprint" == "" ]]; then
  echo "[FAIL] No fingerprint detected, unable to proceed with installation."
  echo "Note that this installer cannot be triggered successfully via Golive at the moment and must be sent as part of a policy deployment."
  exit 1
fi
if [[ "$local_userId" == "" ]]; then
  echo "[FAIL] No user ID detected, unable to proceed with installation."
  echo "Note that this installer cannot be triggered successfully via Golive at the moment and must be sent as part of a policy deployment."
  exit 1
fi
# /SAFETY BAILOUT #

# Deploy JSON config to /opt/cisco/secureclient/Umbrella/

DIR="/opt/cisco/secureclient/Umbrella"
if [ -d "$DIR" ]; then
    echo "$DIR exists"
else
    echo "Creating directory"
    mkdir -p "$DIR"
    chmod 777 "$DIR"
fi

pathPlist="/opt/cisco/secureclient/Umbrella/OrgInfo.json"

# If plist exists, delete it and start over
if [[ -f "$pathPlist" ]]; then
    echo "$pathPlist exists"
    ###FILE FOUND, so let's delete it
    rm "$pathPlist"
  else
    echo "$pathPlist not found"
fi

# Creating file
echo "Creating OrgInfo file"
	cat >> "$pathPlist" <<EOF
{
    "organizationId" : "$local_organizationId",
    "fingerprint" : "$local_fingerprint",
    "userId" : "$local_userId"
}
EOF
	# Set the correct permissions
	chmod 644 "$pathPlist"

echo "OrgInfo file created"

# Create flagfile to ignore Duo certificate
# flagFile="/Library/Application Support/Duo/Duo Device Health/DisableMacOS11CertManagement"
# DIR2="/Library/Application Support/Duo/Duo Device Health"
# if [ -d "$DIR2" ]; then
#   echo "$DIR2 exists"
# else
#    echo "Creating directory"
#    mkdir -p "$DIR2"
#    chmod 777 "$DIR2"
# fi
# touch "$flagFile"
# echo "Cert flagFile created"

# Attach
hdiutil attach -nobrowse "/Library/Addigy/ansible/packages/$cs/$dmgName"
# Install
sudo installer -pkg "/Volumes/$volumeName/$pkgName" -applyChoiceChangesXML "/Library/Addigy/ansible/packages/$cs/$xmlFile" -target /
# Detach
sudo hdiutil detach "/Volumes/$volumeName/"

# Pkill old copy of Umbrella
 if pgrep -x "UmbrellaMenu"; then
    echo "Orphaned UmbrellaMenu detected, closing"
    if [[ -f /Applications/OpenDNS\ Roaming\ Client/rcuninstall ]]; then
    echo "Running Umbrella 3.x uninstaller"
    /Applications/OpenDNS\ Roaming\ Client/rcuninstall
    fi
    killall UmbrellaMenu
    sudo pkill UmbrellaMenu
  fi


exit 0

Full installation script for Cisco Secure Client 5.x