# Writing scripts for remote actions on Mac

{% hint style="info" %}
If you need help performing the operations described in this article, please contact your Nexthink Certified Partner.
{% endhint %}

This page details the process of preparing Nexthink remote action scripts on Mac. The scripts are written in Bash, a command-line shell and scripting language supported by many UNIX-like operating systems, and then signed with a certificate for security purposes. Bash scripts are suited for automating tasks and configuration management; They allow remote actions to be run on employee devices.

{% hint style="warning" %}
Remote actions are currently not supported on macOS devices with Mobile Accounts enabled. In this mode, an action may appear successful, but no change is actually applied.
{% endhint %}

The primary use cases for remote actions include on-demand data collection from devices, self-healing tasks, and modifying configuration settings.

The article assumes that the reader is familiar with Bash scripting.

Refer to the [Remote actions group in Community](https://community.nexthink.com/s/group/0F92p000000kI80CAE) documentation for more information.

{% hint style="info" %}
For more information about executing scripts securely, refer to the [Remote Actions security best practices](https://docs.nexthink.com/security/security-best-practices/remote-actions-security-best-practices) on Nexthink Security.
{% endhint %}

## Creating the script

### Generic scripts and input variables

Generic scripts are useful when a digitally signed script requires customization. Modifying a signed script breaks its signature, but a generic script can be customized by parameters, keeping the signature intact.

Declare parameters at the beginning of a script for Bash positional parameters and enclose them between two special comments as follows:

```
# NXT_PARAMETERS_BEGIN
Parameter1=$1
Parameter2=$2
Parameter3=$3
# NXT_PARAMETERS_END
```

When uploading the script during the remote action configuration, the system takes the parameters between the special Nexthink comments in a Bash script. It lists them in the **Parameters** section below the script text. Provide actual values to the parameters in the text input boxes displayed to the right of each parameter name.

{% hint style="info" %}
Actual values are always passed to the script as text. If the script declares parameters with a type other than **string**, ensure that you provide values the script can convert to their expected type.
{% endhint %}

### Creating output variables

Executing a script may generate output that you want to store as on-demand data. Nexthink provides a Bash script (`nxt_ra_script_output.sh`) that is installed on an employee device at the same time as Collector. The script includes functions to write results to the data layer.

To use the functions in the Nexthink script for remote action output, add the following header at the beginning of your Bash scripts:

```
#!/bin/bash
. "${NEXTHINK}"/bash/nxt_ra_script_output.sh
```

All write methods accept two arguments: the name of the output and the value to write. For instance, if you want to return the number of files in a directory to the data layer, the variable `nfiles` in your script contains that number. To write the value of `nfiles` through output with the name `FileNumber` to the data layer, call the function to write unsigned integers:

```
nxt_write_output_uint32 'FileNumber' $nfiles
```

The Remote Actions editor recognizes calls to write outputs in the script and lists the output variables in the Outputs section. Set the output's label to indicate how to refer to it in investigations and metrics.

The ending of each `write` method indicates the type of output. Because Bash is a loosely typed language, the type of output is interpreted depending on the context. The following is a list of available methods:

| nxt write method                 | Constraints                                                                        |
| -------------------------------- | ---------------------------------------------------------------------------------- |
| nxt\_write\_output\_string       | 0 - 1024 bytes (output truncated if bigger)                                        |
| nxt\_write\_output\_bool         | true / false                                                                       |
| nxt\_write\_output\_uint32       | <ul><li>Min: 0</li><li>Max: 4 294 967 295</li></ul>                                |
| nxt\_write\_output\_float        | <ul><li>Min: -3.4E+38</li><li>Max: 3.4E+38</li></ul>                               |
| nxt\_write\_output\_size         | <ul><li>Min: 0</li><li>Max: 3.4E+38</li></ul>                                      |
| nxt\_write\_output\_ratio        |                                                                                    |
| nxt\_write\_output\_bitrate      |                                                                                    |
| nxt\_write\_output\_duration     | <ul><li>Min: 0 ms</li><li>Max: 49 days</li><li>Precision in milliseconds</li></ul> |
| nxt\_write\_output\_date\_time   | YYYY-MM-DD HH:MM:SS                                                                |
| nxt\_write\_output\_string\_list | 0 - 1024 bytes (output truncated if bigger)                                        |

### **Defining output fields**

When developing scripts, ensure you always:

* **Predefine the names of all output fields:** This ensures that the script populates the correct fields in the output table during execution. Without predefined field names, the script may fail due to an unknown output schema.
  * Output field names should always be in `string` form.
  * Example: `nxt_write_output_string 'output_field_name' "$output_value"`
* **Define the number of output fields:** Your script should always specify a fixed number of output fields. A fixed schema ensures predictable results and compatibility.
  * Avoid dynamic fields. Dynamic output structures cause inconsistency and potential processing errors in the platform.
  * Avoid using loops to define output fields when you run the script.

### Implementing campaigns

Combine remote actions with campaigns to help employees solve issues independently. Campaigns let you inform employees about the detection of an issue and guide them through its resolution.

To display a campaign on the desktop of an employee who is interacting with a device:

* The campaign must have a **Remote action** trigger and be published.
* The script of the remote action can be executed either:
  * In the context of the employee, if the action does not need any special privileges.
  * In the context of the local system account, if the action needs administrative privileges.

### **Obtaining a campaign identifier**

The methods to run a campaign from a remote action require a campaign identifier to be passed as an argument. You can use both the campaign NQL ID (recommended) and the campaign UID (classic option).

```
Support for the NQL ID as an identifier requires Collector version 23.5 or later.
```

To pass the campaign identifier to a remote action, declare a parameter in the script of the remote action for each required campaign. Use the NQL ID (or UID) as the actual value for the parameter when editing the remote action.

For more information on how to get the NQL ID or the UID of a campaign, refer to the [Triggering a campaign](https://docs.nexthink.com/platform/user-guide/campaigns/working-with-campaigns/triggering-campaigns) documentation.

### **Running a campaign from the script of a remote action**

{% hint style="info" %}
You can find error logs under `/Library/Logs/nxtcod.log`. This may vary according to your file structure.
{% endhint %}

The following functions extend the scripting capabilities of remote actions.

```
nxt_run_campaign( id )
```

The function runs the campaign matching the NQL ID (preferred) or UID passed as a parameter and saves the answers internally.

* The function returns 0 if the campaign status is received.
* The function returns 1 in all other cases, and the error is reported in the logs.

Calling the function pauses the execution of the remote action until the employee either completes the campaign or dismisses it.

```
nxt_run_campaign_with_timeout( id timeout)
```

The function runs the campaign matching the NQL ID (preferred) or UID with the timeout in seconds (0 < T < 1 week) passed as a parameter, and saves the answers internally.

* The function returns 0 if the campaign status is received.
* The function returns 1 in all other cases, and the error is reported in the logs.

Calling the function pauses the execution of the remote action until the employee either completes the campaign, dismisses it or fails to finalize the campaign before the timeout.

```
nxt_run_standalone_campaign( id )
```

The function runs the campaign matching the NQL ID (preferred) or UID passed as a parameter and saves the answers internally.

* The function returns 0 if the campaign status is received.
* The function returns 1 in all other cases, and the error is reported in the logs.

Calling the function triggers the start of the campaign and continues the execution of the remote action without waiting for the employee's answers.

```
nxt_get_campaign_status( res_var )
```

The function:

* Returns 0 if the campaign status is stored successfully in `res_var`.
* Returns 1 otherwise.

It then extracts the campaign status as a string value and saves it in the given variable `res_var` . If the function returns with `0` result, `res_var` has one of the following values :

* **fully**: the employee has fully answered the campaign questions.
* **timeout**: the campaign was timed out before the user finished answering.
* **postponed**: the employee agreed to participate in the campaign.
* **declined**: the employee declined to participate in the campaign.
* **connectionfailed**: the script was unable to connect to the Collector component that controls campaign notifications.
* **notificationfailed**: the script was unable to display the campaign successfully due to one of the following:
  * The campaign definition could not be retrieved from the platform because of a non-existent campaign or a non-published campaign.
  * Another campaign is already being displayed to the employee.
  * A non-urgent campaign cannot be shown due to the focus protection or do-not-disturb rules of Collector. Refer to the [Limiting the reception rate of campaigns](https://docs.nexthink.com/platform/user-guide/campaigns/managing-campaigns/creating-campaigns/limiting-the-reception-rate-of-campaigns) documentation for more information.
* Empty if the last campaign failed.

```
nxt_get_response_answer( res_var question_key)
```

Query the last campaign using a question label as a string parameter query\_key. The function extracts the answer as a string value and saves it in the given variable `res_var`.

* Returns 0 if the campaign answer is stored successfully in `res_var`.
* Returns 1 otherwise.

### Encoding the script

To write Bash scripts for remote actions:

* Encode the files that contain the script in UTF-8, without BOM.
* End each line in the code with the usual character in UNIX systems: `LF`.

Ensure proper encoding to avoid errors or nonfunctioning scripts.

***

## Code examples

<details>

<summary>Calling a campaign</summary>

In this example, the remote action executes a basic campaign call by its ID, outputting a status message if successful or an error message if unsuccessful.

```bash
if nxt_run_campaign "#my_campaign_nql_id"; then
    nxt_get_campaign_status status
    if [[ status == "fully" ]]; then
        echo "Campaign succeeded"
    else
        echo "Status is $status"
    fi
else
    echo "Campaign failed"
fi
```

</details>

<details>

<summary>Accessing the responses</summary>

In this example, the remote action calls for campaign answer data and outputs it as an array. Each answer is represented by its corresponding numbered option. Bash uses 0 as its lowest option, while Zsh uses 1.

```bash
nxt_get_campaign_status status
echo "The response status is $status"
nxt_get_response_answer answersArray key1
echo ${answersArray[1]}
```

</details>

<details>

<summary>Running a campaign with a timeout</summary>

In this example, the remote action is set to run a campaign that times out and closes after a specified time, using seconds as input.

```bash
# timeout is in seconds (100s or 00:01:40)

if nxt_run_campaign_with_timeout "#my_campaign_nql_id" 100; then
    nxt_get_campaign_status status
    if [[ status == "fully" ]]; then
        echo "Campaign succeeded"
    else
        echo "Status is $status"
    fi
else
    echo "Campaign failed"
fi
```

</details>

<details>

<summary>Running a non-blocking campaign</summary>

In this example, the remote action does not require user input and continues executing after triggering a campaign. The user can dismiss the campaign at any point. This is used mainly to provide users with information rather than to obtain data.

Refer to the [Conducting engaging campaigns](https://docs.nexthink.com/platform/user-guide/remote-actions/managing-remote-actions/broken-reference) documentation for more information.

```bash
if nxt_run_standalone_campaign "#my_campaign_nql_id"; then
    nxt_get_campaign_status status
    if [[ status == "fully" ]]; then
        echo "Campaign succeeded"
    else
        echo "Status is $status"
    fi
else
    echo "Campaign failed"
fi
```

</details>

***

## Signing the script

{% hint style="info" %}
For security reasons, Nexthink requires Bash scripts for remote actions on macOS to be digitally signed with the `codesign` tool. Apple stores the signature in the extended attributes of a file. Pack your signed script as a `tar.gz` file to preserve its extended attributes. Nexthink web interface only accepts files with the `tar.gz` extension when importing scripts for remote actions that target macOS.

Nexthink recommends signing all scripts in a production environment. Unsigned scripts should only be used in testing environments.
{% endhint %}

<details>

<summary>Option 1 : Relying on the System Certificate Store</summary>

**Creating a signing certificate**

{% hint style="warning" %}
Starting from MacOS Sequoia (version 15), instead of deploying the signing certificate itself as trusted on all devices (as is done on Windows), the Root Certificate Authority (CA) of the signing certificate must be deployed due to changes introduced by Apple. Public Root CAs pre-installed with the operating system will not work.
{% endhint %}

You can deploy one of the following options to all devices where remote actions are executed.

* Self-signed Root CA dedicated for Remote Actions
* Your corporate Root CA

Below is an example of how to create a self-signed Root CA and use it to generate a code signing certificate for signing remote actions:

1. Launch Keychain Access on your Mac device.
2. Go to Keychain Access > Certificate Assistant > **Create a Certificate Authority…**

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-dc7c404523f4ac259f406d5a02d22d6d99dcfa43%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

3. Enter the name of your Certificate Authority.
4. Choose **Code Signing** for the **User Certificate**.
5. Enter an email address (as a point of contact for administrative purposes).
6. Click **Create** to generate the certificate.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-37204b35e8569178392ce8d0421967bbd1ca08c6%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

Below is an example of how to create the code signing certificate itself:

1. Launch Keychain Access on your Mac device.
2. Go to Keychain Access > Certificate Assistant > **Create a Certificate…**

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-7f0670f5e0f6a8841c687965470b211ab7ab194a%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

3. Enter the name of your certificate.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-6d3b8fc498fe1ad349468e9fa5432a0ab67e91ef%2Fimage%20(223).png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

4. Select the Root CA as the issuer for this certificate.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-9ee3ce525fb1ed0d8eaf71723205327a4d9ea684%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

5. Click **Create** to generate the certificate and get a confirmation as shown below.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-5a36ff8387afd506fa3238a1106597eca3939821%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

Once the certificate is created, it is visible in the Keychain Access application. Right-click on the certificate, then click on Get Info.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-c00eb7769dc9f9adbd153a788d1d1d321fa9ffe5%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

Scroll down to the bottom and copy-paste the SHA-1 from there. Remove the space from the value to use it for signing the remote action script.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-abca3a911b648407e3e090bc7f2abd62fec91b7e%2FScreenshot%202025-01-24%20at%2014.05.10.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

Use the code signing certificate to sign a script:

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-817c17d6399f08d03076f1d9034d63193603eb3c%2Fimage%20(20).png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

Check that the previous script signing steps have succeeded by executing the command as shown below.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-b420bbcbd3ceacd0c78830003a2e4a6a42df6725%2Fimage%20(21).png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

**Packaging**

Package the remote action script with a .tar archive and .gzip compression. The extension ".tar.gz" is mandatory.

```
tar -czvf ./<your script name>.tar.gz ./<your script name>.sh
```

If the script file is signed, the tar utility will pack its extended attributes as well. This way, the system can transport the code signature and a script file.

**Limitations**

* Only one script can be put into one archive.
* A script file should be placed in the root package folder; the `./myscript/myscript.sh` path is incorrect.
* A script must have the .sh extension.
* A script filename must be UTF-8 encoded, which is the default on macOS.

**Example**

Example of packaging the `test.sh` script:

```
tar -czvf ./example_ra_script.tar.gz ./example_ra_script.sh
```

The resulting `example_ra_script.tar.gz` is the remote action script file.

{% hint style="info" %}
Nexthink recommends using this script to simplify the signing and packaging process.
{% endhint %}

```
#!/bin/bash
#
# script_signing.sh
# 
# Copyright (C) 2023 by Nexthink S.A., Switzerland. Any usage, copy or partial copy of
# this code without the explicit agreement of Nexthink S.A. is prohibited and will be
# pursued to the full extend of the law.
#
# The arguments for the script:
# - input script filename
# - output archive filename
# - Certificate owner
# - Prefix
#
 
 
# Error handling
set -euo pipefail
trap "echo unrecoverable error !" ERR
 
 
# Check codesign
if [[ ! -x /usr/bin/codesign ]]
then
    echo "Error: this script requires that codesign is installed"
    exit 2
fi
 
# Check tar
if [[ ! -x /usr/bin/tar ]]
then
    echo "Error: this script requires that tar is installed"
    exit 2
fi
 
 
# Check arguments
if [[ $# -lt 4 ]]
then
    echo "Usage: script_signing.sh inputScriptFilename outputArchiveFilename CertificateOwner prefix"
    echo "Example: ./script_signing.sh script.sh script.tar.gz \"John Doe\" com.john.remote-action.macos."
    exit 1
fi
 
/usr/bin/codesign -s "$3" --timestamp --prefix="$4" --force "$1"
/usr/bin/tar czf "$2" "$1"
```

**Testing the signed script on the Machine Where the Root CA was generated**

When testing a signed script on the same machine where the Root CA was created, you must take the following steps.

**Copying the Root CA to the System Keychain and marking it as Trusted**

By default, Apple places the generated Root CA in the **"login"** Keychain. However, you must install it in the **"System"** Keychain to ensure proper functionality.

To copy the certificate to the System Keychain, follow these steps:

1. Open **Keychain Access**.
2. Export the Root CA certificate by right-clicking it and selecting **Export**.\
   In the pop-up window, choose the **Certificate (.cer)** file format.<br>

   <figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-bf04fea6b4fb1d58cfd33bb437f810eca9a23c43%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>
3. Open the **Terminal** application.
4. Run the following command to import the Root CA into the System Keychain:\
   `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain Certificate.cer`
5. Once imported, verify that the certificate appears in the **System** Keychain:
   * Return to **Keychain Access**.
   * Select the **System** section in the left sidebar.
   * Use the search bar in the top-right corner to locate the certificate.
   * In the results table, confirm that the **Keychain** column displays **System**.
6. Mark the Root CA certificate as Trusted by right-clicking on it, and selecting **Get Info**.
   * In the **Trust** section, adjust the settings to mark the Root CA as trusted.<br>

     <figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-0f09b8fac8453661f0bbfb21a25d61ea2e1be93e%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>

**Deploying the Root CA of the signing certificate to endpoints**

{% hint style="info" %}
As of MacOS Sequoia version 15, rather than deploying the signing certificate as trusted on all devices like on Windows, the Root Certificate Authority (CA) of the signing certificate must be deployed instead.

Collector Cloud 24.10.2.10 and Collector on-prem 24.10.30.26 support remote action script trusted source validation based on the deployed Root CA as an alternative to the deployed signing certificate.
{% endhint %}

You can deploy the Root CA using several methods, mainly by creating configuration profiles using JAMF, as described below.

**Exporting the certificate created with the Root CA**

1. Choose the *Certificate (.cer)* file format in the pop-up window.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-bf04fea6b4fb1d58cfd33bb437f810eca9a23c43%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-ff55aeadf9204d90d172efe78554d96cbdec04ee%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

2. Deploy to all endpoints using the configuration profile above. The Root CA is marked as trusted automatically.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-7ecea3f95d36a43668255544d5a85ad13f2dc4b0%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-0f09b8fac8453661f0bbfb21a25d61ea2e1be93e%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

**Verifying that the certificate was properly imported to an endpoint**

Copy and unpack your signed remote action script to the endpoint.

```
tar -xzvf example_ra_script.tar.gz
```

Verify your signature.

```
codesign -vvvv -R="anchor trusted" example_ra_script.sh
```

If the Root CA of the signing certificate is properly imported, you should see the following output:

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-43f3bd35069b437963326a5863d0743a6942f42f%2FScreenshot%202025-02-10%20at%2009.19.33.png?alt=media" alt=""><figcaption></figcaption></figure>

</details>

<details>

<summary>Option 2 : Relying on Certificate Pinning</summary>

{% hint style="info" %}
Starting from Collector version 2025.9.1.x, the MacOS Collector supports additional execution policies to control which scripts can be executed. This section explains the details.
{% endhint %}

**New execution policies**

Two new execution policies have been introduced on macOS:

* **signed\_pinned**
* **signed\_pinned\_or\_nexthink**

In addition, a new installer option `cert_fingerprints` has been added.

**The `cert_fingerprints` installer option**

The `cert_fingerprints` option allows defining an allow list of script signature thumbprints (separated by a single `,` character, without spaces around). The script signature thumbprint must be the thumbprint of the **leaf certificate** of the signature.

You can extract the thumbprint of the leaf certificate of a signed script file using the following script:

```
#!/bin/bash
#
# extract_signature_leaf_certificate_thumbprint.sh
#
# Copyright (C) 2025 by Nexthink S.A., Switzerland. Any usage, copy or partial copy of
# this code without the explicit agreement of Nexthink S.A. is prohibited and will be
# pursued to the full extend of the law.
#
# The arguments for the script:
# - input script filename
#

set -euo pipefail

if [ $# -lt 1 ]; then
  echo "Usage: $0 <path-to-signed-binary>" >&2
  exit 1
fi

sigpath="$1"

tmpdir=$(mktemp -d) || { echo "Failed to create temp dir" >&2; exit 1; }

(
  cd "$tmpdir" || exit 1
  codesign -d --extract-certificates "$sigpath" >/dev/null

  leaf=$(ls codesign0* 2>/dev/null | head -n 1)
  if [ -n "$leaf" ] && [ -f "$leaf" ]; then
    openssl x509 -in "$leaf" -inform DER -noout -fingerprint -sha256
  else
    echo "Leaf certificate not found" >&2
    exit 2
  fi
)

rm -rf "$tmpdir"
```

{% hint style="danger" %}
The `cert_fingerprints` option only applies when using the new execution policies (`signed_pinned` and `signed_pinned_or_nexthink`). It has no effect on other execution policies.
{% endhint %}

**Policy behaviors**

**signed\_pinned**

* Unsigned scripts are not allowed. They fail during execution, and Collector reports the relevant status.
* Nexthink Library RA scripts: execution is allowed only if `cert_fingerprints` contains the thumbprint of the leaf certificate of the signature. Otherwise, execution is blocked, and status is reported.
* Other scripts: execution is allowed only if `cert_fingerprints` contains the thumbprint of the leaf certificate of the signature. Otherwise, execution is blocked, and status is reported.

**signed\_pinned\_or\_nexthink**

* Unsigned scripts are **not allowed**. They fail during execution, and Collector reports the relevant status.
* Nexthink Library RA scripts: execution is always allowed (not blocked by policy).
* Other scripts: execution is allowed **only if** `cert_fingerprints` contains the thumbprint of the leaf certificate of the signature. Otherwise, execution is blocked, and status is reported.

**Summary table of execution policies**

| Policy                                   | Unsigned scripts | Nexthink Library RA scripts                                     | Other signed scripts                                            | Effect of cert\_fingerprints |
| ---------------------------------------- | ---------------- | --------------------------------------------------------------- | --------------------------------------------------------------- | ---------------------------- |
| signed\_trusted (existing)               | Blocked          | Blocked                                                         | Allowed if signed by trusted cert                               | No effect                    |
| signed\_trusted\_or\_nexthink (existing) | Blocked          | Always allowed                                                  | Allowed if signed by trusted cert                               | No effect                    |
| signed\_pinned                           | Blocked          | Allowed if thumbprint is in allow list (of `cert_fingerprints`) | Allowed if thumbprint is in allow list (of `cert_fingerprints`) | Required                     |
| signed\_pinned\_or\_nexthink             | Blocked          | Always allowed                                                  | Allowed if thumbprint is in allow list (of `cert_fingerprints`) | Required                     |

**Creating a code signing certificate for Certificate Pinning**

With Certificate pinning, any type of code signing certificate is supported :

* A Code Signing certificate created on Apple's Developer site
* A Code Signing certificate created using Apple's Certificate Assistant
* A Code Signing certificate with a private Root CA
* A Code Signing certificate with a public Root CA
* and any other types as well

If you want to use a Code Signing certificate with a private Root CA, you can find the concrete steps for it [above](#option-1-relying-on-the-system-certificate-store).

**Packaging the signed remote action script, known limitations**

After your code signing certificate is created, double click on it to open it in the Keychain Access application. Right-click on the certificate, then click on Get Info.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-c00eb7769dc9f9adbd153a788d1d1d321fa9ffe5%2Fimage.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

Scroll down to the bottom and copy-paste the SHA-1 from there. Remove the space from the value to use it for signing the remote action script.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-abca3a911b648407e3e090bc7f2abd62fec91b7e%2FScreenshot%202025-01-24%20at%2014.05.10.png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

Use the code signing certificate to sign a script:

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-817c17d6399f08d03076f1d9034d63193603eb3c%2Fimage%20(20).png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

Check that the previous script signing steps have succeeded by executing the command as shown below.

<figure><img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-b420bbcbd3ceacd0c78830003a2e4a6a42df6725%2Fimage%20(21).png?alt=media" alt="" width="563"><figcaption></figcaption></figure>

**Packaging**

Package the remote action script with a .tar archive and .gzip compression. The extension ".tar.gz" is mandatory.

```
tar -czvf ./<your script name>.tar.gz ./<your script name>.sh
```

If the script file is signed, the tar utility will pack its extended attributes as well. This way, the system can transport the code signature and a script file.

**Limitations**

* Only one script can be put into one archive.
* A script file should be placed in the root package folder; the `./myscript/myscript.sh` path is incorrect.
* A script must have the .sh extension.
* A script filename must be UTF-8 encoded, which is the default on macOS.

**Example**

Example of packaging the `test.sh` script:

```
tar -czvf ./example_ra_script.tar.gz ./example_ra_script.sh
```

The resulting `example_ra_script.tar.gz` is the remote action script file.

{% hint style="info" %}
Nexthink recommends using this script to simplify the signing and packaging process.
{% endhint %}

```
#!/bin/bash
#
# script_signing.sh
# 
# Copyright (C) 2023 by Nexthink S.A., Switzerland. Any usage, copy or partial copy of
# this code without the explicit agreement of Nexthink S.A. is prohibited and will be
# pursued to the full extend of the law.
#
# The arguments for the script:
# - input script filename
# - output archive filename
# - Certificate owner
# - Prefix
#
 
 
# Error handling
set -euo pipefail
trap "echo unrecoverable error !" ERR
 
 
# Check codesign
if [[ ! -x /usr/bin/codesign ]]
then
    echo "Error: this script requires that codesign is installed"
    exit 2
fi
 
# Check tar
if [[ ! -x /usr/bin/tar ]]
then
    echo "Error: this script requires that tar is installed"
    exit 2
fi
 
 
# Check arguments
if [[ $# -lt 4 ]]
then
    echo "Usage: script_signing.sh inputScriptFilename outputArchiveFilename CertificateOwner prefix"
    echo "Example: ./script_signing.sh script.sh script.tar.gz \"John Doe\" com.john.remote-action.macos."
    exit 1
fi
 
/usr/bin/codesign -s "$3" --timestamp --prefix="$4" --force "$1"
/usr/bin/tar czf "$2" "$1"
```

**Deploying the Root CA of the signing certificate to endpoints**

The certificate doesn't have to be deployed on the endpoints with Certificate Pinning. Just simply configure the `cert_fingerprints`  Collector installer option on all endpoints as described above.

</details>

***

## Maintaining the script

### Comparison and validation

Before deploying a remote action script, it is possible to compare it with other scripts prepared by Nexthink. This step is optional but recommended if it is your first time preparing a script.

1. In the Nexthink Library, select **Content**.
2. Filter by **Remote action**.
3. Navigate to the **Remote Actions** management page.
4. Select any remote action script installed directly from the Nexthink Library that matches the target operating system.
5. Export the script and compare its syntax with your script’s.

### Script termination and timeout

When launching subprocesses from your remote action script and the remote action script terminates or times out, Collector terminates the subprocesses automatically. If you want the subprocess to continue executing after the remote action is terminated, ensure that your script detaches the subprocess using the & character, for example:

```
some_script.sh -arg1 -arg2 &
```

#### Zsh command interpreter

Starting with Collector version 6.27.2, you can run scripts written for the Zsh Unix shell. You must add the following line of code at the very beginning of the shell script:

```
#!/bin/zsh
```

This is a character sequence known as a [shebang](https://en.wikipedia.org/wiki/Shebang_\(Unix\)) (external link).

When the system triggers a remote action on a Mac, Collector checks the first line of code and executes the rest of the instructions using the specified interpreter. A script without a *shebang* will be executed using the Bash command interpreter.

{% hint style="info" %}
Nexthink recommends always using a *shebang* in shell scripts and sticking to the standard interpreters.
{% endhint %}

<details>

<summary>ARCHIVED - Certificate generation, signing, deployment, verification (old approach<strong>, not compatible with macOS Sequoia and newer</strong>)</summary>

#### **Certificate generation approach without a Root CA**

This certificate generation approach will continue working on all devices, but it will require user interaction to mark it as trusted on the device.

* Launch Keychain Access on your Mac device.
* Go to Keychain Access > Certificate Assistant > **Create a Certificate…**

<img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-e418da8393e0720e43c81f62dbddfabcded75768%2FUntitled.png?alt=media" alt="" data-size="original">

1. Enter the name of your certificate.

* Choose **Code Signing** for the **Certificate Type**.
* For testing purposes, you can leave **Let me override defaults** unchecked.<br>

<img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-fd29eb6278cb833e6e790f7cac17c92404d8cf92%2FUntitled-1.png?alt=media" alt="" data-size="original">

* Click **Create** and **Done**. The system has generated the certificate.

<img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-683cd69ba7f35c6e1b50afcf5c3b2fb5c7e70b05%2FUntitled-1.png?alt=media" alt="" data-size="original">

#### Signing

Sign the remote action scripts using the standard macOS codesign utility.

```
codesign -s <your certificate identity> --timestamp --prefix=<code signature identifier prefix> --force <script file name>
```

Parameters:

```
-s <your certificate identity>
```

The identity of your code signing certificate is in the Keychain. Generally, it is a certificate subject common name or a certificate hash. Refer to the [codesign manual page](https://ss64.com/mac/codesign.html) from SS64 for more information.

```
--timestamp
```

A trusted timestamp for a signature.

```
--prefix
```

A prefix to a code signature identifier. It attaches your company identity to an identifier and helps to make an identifier unique. See the code signature identifier generation rules on the [codesign manual page](https://www.unix.com/man-page/osx/1/codesign/) documentation from UNIX.

```
--force
```

This forces a code signature to be rewritten if it already exists.

#### **Example**

Example of a test certificate for *example\_ra\_script.sh* remote action script:

```
codesign -s "RA scripts code signing certificate" --timestamp --prefix=com.my-organisation.remote-action.macos. --force example_ra_script.sh
```

The signature for the script file is generated in the file system extended attributes associated with the file. Use the codesign utility to get the code signature details and validate the signature:

<img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-ba18bb82386f520a47e0f751814fce18e1f1137b%2FUntitled.png?alt=media" alt="" data-size="original">

#### **Deploying the signing certificate to endpoints**

This deployment approach will continue working, but it will require user interaction.

<img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-20761f69de42332a3eddab166f762c44e8f04b08%2FUntitled.png?alt=media" alt="" data-size="original">

Choose the *Certificate (.cer)* file format for a public certificate in the pop-up window.

#### Importing a certificate to the Keychain on endpoints

* Import your code-signed certificate into the System keychain to use remote action scripts with a *Trusted Publisher* execution policy.
* Double-click a .cer file and choose the **System** option in the **Keychain** drop-down menu.

<img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-fe5fc3d908671e7054702d0952718c9d3f8cf0fa%2FUntitled-1.png?alt=media" alt="" data-size="original">

* Enter the root password to import the certificate.

<img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-80d87c3f73327585589fc9d2cecbc260ebb00b31%2FUntitled.png?alt=media" alt="" data-size="original">

To automate these tasks, use the [security utility](https://ss64.com/mac/security-cert.html) (external link) or your automation framework.

#### Verify that the certificate was properly imported to an endpoint

* Copy and unpack your signed remote action script to the endpoint.

```
tar -xzvf example_ra_script.tar.gz
```

* Verify your signature.

```
codesign -vvvv -R="certificate leaf trusted" example_ra_script.sh
```

If the signature is properly imported, you should see the following output:

<img src="https://268444917-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FxJSUDk9NTtCHYPG5EWs3%2Fuploads%2Fgit-blob-06a3ab5d7ec64340d1b9e96377f6a0ae7a20c608%2FUntitled-1.png?alt=media" alt="" data-size="original">

</details>
