A primer to deploying Cisco AnyConnect for macOS with Microsoft Intune

This article is not intended to be exhaustive or "best practices". I only wanted to share my findings in the hope that it'll help others to save time. Please leave a comment for suggestions or ideas!

In order to deploy Cisco AnyConnect on macOS, you'll need the following resources on the client:

  1. SystemExtension profile

  2. WebContentFilter profile

  3. Cisco AnyConnect XML profile

  4. Cisco AnyConnect package

SystemExtension profile

If you skip this section and the next, your users will get prompted to allow the System Extension or the content filter to load. Start with these ones because you want them to be on the Mac before installing the package, so it will be automatically allowed.

  1. Go to Devices > macOS > Configuration Profiles and create a new Templates > Extensions profile

  2. Under System extensions > Allowed system extensions, set the Bundle identifier as "com.cisco.anyconnect.macos.acsockext" and the Team identifier to "DE8Y96K9QP"

  3. Under Allowed system extension types, add a line to allow team identifier "DE8Y96K9QP" to provide "Network extensions".

References

WebContentFilter profile

Unfortunately, Microsoft Intune doesn't provide a way to do this in the web UI. You'll have to create an XML configuration and upload it as a new configuration profile, Templates > Custom.

Below is the configuration profile I created, but you can also use Cisco’s example.

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>PayloadContent</key> <array> <dict> <key>Enabled</key> <true/> <key>FilterType</key> <string>Plugin</string> <key>AutoFilterEnabled</key> <false/> <key>FilterBrowsers</key> <false/> <key>FilterSockets</key> <true/> <key>FilterPackets</key> <false/> <key>FilterGrade</key> <string>firewall</string> <key>FilterDataProviderBundleIdentifier</key> <string>com.cisco.anyconnect.macos.acsockext</string> <key>FilterDataProviderDesignatedRequirement</key> <string>anchor apple generic and identifier "com.cisco.anyconnect.macos.acsockext" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = DE8Y96K9QP)</string> <key>PluginBundleID</key> <string>com.cisco.anyconnect.macos.acsock</string> <key>VendorConfig</key> <dict/> <key>UserDefinedName</key> <string>Cisco AnyConnect Content Filter</string> <key>PayloadDisplayName</key> <string>Cisco AnyConnect Content Filter</string> <key>PayloadIdentifier</key> <string>com.cisco.anyconnect.webcontentfilter.42B8BA0E-57F4-4E57-872B-1F5FCB8527EA.2512DB6A-B5EA-41DB-B6C6-3A07726C214E</string> <key>PayloadType</key> <string>com.apple.webcontent-filter</string> <key>PayloadUUID</key> <string>2512DB6A-B5EA-41DB-B6C6-3A07726C214E</string> <key>PayloadVersion</key> <integer>1</integer> </dict> </array> <key>PayloadDisplayName</key> <string>Cisco AnyConnect Content Filter</string> <key>PayloadIdentifier</key> <string>com.cisco.anyconnect.webcontentfilter.42B8BA0E-57F4-4E57-872B-1F5FCB8527EA</string> <key>PayloadScope</key> <string>System</string> <key>PayloadType</key> <string>Configuration</string> <key>PayloadUUID</key> <string>42B8BA0E-57F4-4E57-872B-1F5FCB8527EA</string> <key>PayloadVersion</key> <integer>1</integer> </dict> </plist>

References

Cisco AnyConnect XML profile

For Cisco AnyConnect to pre-populate some information, you'll need to place a configuration profile at /opt/cisco/anyconnect/profile/profile.xml. You could do this with a package, but if I remember well, Intune is not great at dealing with packages that don't place an application in /Applications. So instead, we'll use a script.

Copy/paste the following in a .sh file, modify it to your needs, then upload it as a script on Intune by going to Devices > macOS > Shell scripts. Don’t run the script as the signed-in user, we want to write the file in a directory that is not writable by standard users.

Below is the one I created for test purposes. If I understand well, your ASA administrator should hand you the XML profile.

#!/bin/sh mkdir -p /opt/cisco/anyconnect/profile cat <<EOF>/opt/cisco/anyconnect/profile/profile.xml <?xml version="1.0" encoding="UTF-8"?> <AnyConnectProfile xmlns="http://schemas.xmlsoap.org/encoding/"> <ServerList> <HostEntry> <HostName>1.2.3.4</HostName> <HostAddress>https://1.2.3.4</HostAddress> </HostEntry> </ServerList> </AnyConnectProfile> EOF
Devices &gt; macOS &gt; Shell scripts

Devices > macOS > Shell scripts

Script logs can be find in `/Library/Logs/Microsoft/Intune`

You can force the agent to re-evaluate all scripts by running `sudo killall IntuneMdmAgent`.

Note on using User Identities

I haven’t tested it, but you can have Cisco use a User Identity (private key + public certificate). For this, you’ll need to first install the identity on the Mac’s keychain, most probably using a SCEP profile, then Cisco AnyConnect should be able to do the rest. The user will get prompted to allow Cisco AnyConnect to access the identity in the keychain. Your user will have to click “Always Allow” (see screenshot below). Note that if you deploy the identity in the system keychain (by using a “Computer” configuration profile), the user will have to be a local administrator to allow it. Thank you, D.Y.

Prompt fir the user to allow Cisco AnyConnect to access an identity in the keychain

Prompt fir the user to allow Cisco AnyConnect to access an identity in the keychain

Reference

Cisco AnyConnect package

You have two possibilities to install the Cisco AnyConnect package: wrap it then deploy it as a macOS line-of-business (LOB) app, or host it somewhere and use the scripting agent to curl and install it on macOS. The latter is more flexible, but the former is more integrated, and I believe easier to maintain. But it's up to you. Remember that macOS LOB apps must be signed and notarized, and contain an application installing in /Applications. Otherwise they won't install or install in a loop.

Let's do the macOS LOB way.

  1. Download the Intune App Wrapping Tool and make it executable (`chmod +x ./IntuneAppUtil`)

  2. Download the Cisco AnyConnect DMG (I get it directly from my server) and mount it to get the package

  3. Wrap the package using ./IntuneAppUtil -c /Volumes/AnyConnect\ VPN\ 4.10.00093/anyconnect-macos-4.10.00093-core-vpn-webdeploy-k9.pkg -o ~/Downloads

  4. Go to Intune, Apps > macOS

  5. "Add" and choose Other > Line-of-business app

  6. Select the ~/Downloads/anyconnect-macos-4.10.00093-core-vpn-webdeploy-k9.pkg.intunemac you just created and assign it

Screen Shot 2021-06-14 at 8.17.08 AM.png

References

Verifying the installation

After a while (don't hesitate to "Sync" device in Intune), everything will eventually be on the Mac.

You can verify the download of the app by searching for "cisco" in the Console app. Hint: the process responsible to download the package will be appstored.

Send a syncDevice from bash to an iOS device enrolled in Microsoft Intune

One thing that can be quite problematic with Microsoft Intune, is that it syncs with the device every 8 hours (every 15mn the first hour). It is usually fine, but in some scenarios you’ll want to trigger a sync programmatically.

This post will walk you through how to use Microsoft Intune’s API to trigger a syncDevice from bash, using curl. I’ll show you how to configure an application on Azure Portal to get the credentials, then how to test using Paw, and finally how to make a rudimentary script.

A word of caution: this method works for me, and it is provided “as is”, without warranty of any kind, express or implied. But feel free to add a comment below to improve the post.

Create an app on the Azure portal

First thing we need to do is to create an App on the Azure portal. We’ll choose “Client secrets” to make it easy, but you can use certificates instead (I won’t cover it).

Resources:

So, connect to the Azure portal which is tied to your Microsoft Intune, and select the right tenant.

Create an App Registration

  1. Go to Azure Active Directory

  2. Click on “App registrations”

  3. Click on “New Registration”

    1. Choose a nice name

    2. Select “Accounts in this organizational directory only (XXX only - Single tenant)”

    3. Don’t fill the Redirect URI

Screen Shot 2020-04-18 at 9.14.14 PM.png

Create a client secret

  1. Go to “Certificates & secrets”

  2. Click on “New client secret”

  3. Choose a description and save the token value (you’ll see this only once)

Screen Shot 2020-04-18 at 9.15.58 PM.png

Configure API Permissions

Go to API permissions, then add the following permissions, under “Microsoft Graph”:

  • DeviceManagementManagedDevices.Read.All (Delegated)

  • DeviceManagementManagedDevices.PrivilegedOperations.All (Delegated)

Then click on “Grant admin consent for XXX”.

Screen Shot 2020-04-18 at 10.42.25 PM.png

Write down required information

You will need the following information:

  • Client ID (aka Application ID): find it on the “Overview” tab of the App registration you just created

  • Client Secret: you wrote it down earlier when you created a new client secret. If you haven’t, go back, delete your client secret and create a new one.

  • Tenant domain: If you go back to “Azure Active Directory” then “Custom domain names”, you’ll see it written (e.g. M365x208777.onmicrosoft.com)


Configure the API browser application

I use Paw, but virtually everyone I know uses Postman. If you choose Postman, have a look at the following resources from Microsoft:

Get the Access Token

Microsoft Graph uses OAuth 2.0. Here we want to get a Bearer token which we will use for subsequent calls to the API.

To move forward, create a new request, and enter the following information:

  • POST https://login.microsoftonline.com/[TENANT-DOMAIN]/oauth2/token

  • Body > Form URL-Encoded

    • client_id: the Client ID from “App Registration > Overview” (see earlier)

    • client_secret: the Client Secret you generated earlier

    • Resource: https://graph.microsoft.com

    • grant_type: client_credentials

Then hit CMD+R and the token will be on the right, under “access_token”. Right-click on it and click on “Copy Value”. Don’t do it by double-clicking on the field then CMD+C, otherwise you’ll get an error like “CompactToken parsing failed with error code: 80049217” later on.

Screen Shot 2020-04-18 at 9.33.18 PM.png

Get the Device ID, from the list of devices

Create a new Request, with the following information:

  • GET https://graph.microsoft.com/v1.0/deviceManagement/managedDevices

  • Headers

    • Authorization: “Bearer [access_token]”

Hit “CMD+R” and you should see a list of devices on the right. What interests us is “value.id”. You may want to filter the view by “value.serialNumber” to get the device you want.

Screen Shot 2020-04-18 at 9.38.13 PM.png

Post a “syncDevice” to the device

Resource: https://docs.microsoft.com/en-us/graph/api/intune-devices-manageddevice-syncdevice?view=graph-rest-1.0

Now that you have the device ID, you can create another Request with it:

  • POST https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/[DEVICEID]/syncDevice (make sure you replace [DEVICEID] with the id you found earlier (like 9666-…)

  • Headers

    • Authorization: “Bearer [access_token]”

Hit “CMD+R” and if all goes well, you should get a “204 No Content”.

Screen Shot 2020-04-18 at 9.42.46 PM.png

Verify the device had a sync

Resource: https://docs.microsoft.com/en-us/graph/api/intune-devices-manageddevice-get?view=graph-rest-1.0

We can do a very similar call to get managedDevices, but this time specify the device ID to get a single device instead of an array of all devices. Configure the request this way:

  • GET https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/[DEVICEID] (make sure you replace [DEVICEID] with the id you found earlier (like 9666-…)

  • Headers

    • Authorization: “Bearer [access_token]”

Hit “CMD+R”. We’re interested by “lastSyncDateTime” which should be very close to now (provided the device is on and replied to the push notification). Note that this value is in GMT, so translate to your timezone.

Screen Shot 2020-04-18 at 9.51.51 PM.png

Putting it all together with curl

Install jq

jq is a very powerful JSON parser command line tool. We want to use it to better parse the response from the server. Install it in /opt/local/bin:

  1. Go to https://stedolan.github.io/jq/download/ and download jq 1.6 binary for 64-bit. You can also install it with Homebrew or MacPorts if you prefer.

  2. mkdir -p /opt/local/bin

  3. mv ~/Downloads/jq-osx-amd64 /opt/local/bin/jq

  4. chmod +x /opt/local/bin/jq

Get curl commands from Paw

Paw has a very handy feature that can generate code in many different languages and commands, including curl. to do so, click on the drop down menu top right of the console, and select “cURL” (sic). You can then paste it in your editor of choice.

Screen Shot 2020-04-18 at 10.30.20 PM.png

Search for a specific serial number

Microsoft Graph have query parameters, that will allow you to filter a query with certain parameters. Here, we would like to return all the managedDevices with a specific serial number. To do so, we can use the URL Parameter $filter=startswith(serialNumber, ‘SERIALNUMBER’). You should still get an array, but with a single managedDevice.

Putting it all together

We want to assemble the following:

  1. Get the access token (“.access_token”)

  2. Get a managedDevice from a serial number (“.value[0] .id”)

  3. send the syncDevice command

As a starter, I’ve done the following script. I leave you the task to work on the error control and make it reliable. If you can share it, all the better!

#!/bin/bash

serialNumber="FA1QHC21GRY1"

## 1. Get the Access Token (".access_token")
tokenResult=$(curl -sf -X "POST" "https://login.microsoftonline.com/M365x208777.onmicrosoft.com/oauth2/token" \
     -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' \
     --data-urlencode "client_id=ae49b634-5140-48c1-9647-4158754110be" \
     --data-urlencode "client_secret=_SbdfLpAVD-BiLMoqJcQEVN]3AQne470" \
     --data-urlencode "Resource=https://graph.microsoft.com/" \
     --data-urlencode "grant_type=client_credentials")

accessToken=$(echo "${tokenResult}" | /opt/local/bin/jq -r '.access_token')

## Get a managedDevice, from a serial number (".value[0] .id") 
managedDeviceResult=$(curl -sf "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?\$filter=serialNumber%20eq%20%27${serialNumber}%27" \
     -H "Authorization: Bearer ${accessToken}" \
     -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8')
deviceID=$(echo "${managedDeviceResult}" | /opt/local/bin/jq -r '.value[0] .id')


## send the syncDevice command
syncDeviceResult=$(curl -sf -X "POST" "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/${deviceID}/syncDevice" \
     -H "Authorization: Bearer ${accessToken}" \
     -H 'Content-Type: application/x-www-form-urlencoded; charset=utf-8' -d "")

if [ -z "${syncDeviceResult}" ]; then
    echo "syncDevice sent to ${serialNumber}"
fi

Deploying macOS Apps with Microsoft Intune

Microsoft Intune supports the deployment of applications using InstallApplication. This opens the possibility to manage Mac computers with Microsoft Intune, and automatically push Munki to provide additional functionality.

The process for that is outlined in How to add macOS line-of-business (LOB) apps to Microsoft Intune

Make sure:

As far as I know, there’s no way to make these macOS LOB apps to be installed during the setup assistant (also called: “Bootstrap package”. In practice, the delay between enrolment and the app being deployed can be quite long (I’ve seen 5 minutes while clicking on “Sync” frantically). Also, Microsoft Intune seem to be a little slow to report success or failure in the console. Perhaps time for a User voice feedback?