Skip to content

Inetum-Poland/jamf-recovery-lock-rotation

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Jamf Recovery Lock Rotation

License: Apache 2.0 GitHub Marketplace Platform MDM: Jamf Pro Lint ShellCheck Security Dry Run GitHub Release

Composite GitHub Action for rotating Recovery Lock passphrases on Jamf Pro–managed Apple Silicon Mac computers via the Jamf Pro API.
Passphrases are generated from bundled or custom wordlists. Credentials are never logged.

About

Recovery Lock secures the macOS Recovery environment on Apple Silicon devices. Regular rotation of these passphrases reduces exposure risk if a credential is compromised.

This action:

  • authenticates to Jamf Pro using OAuth client credentials (JAMF_CLIENT_ID / JAMF_CLIENT_SECRET),
  • retrieves device Management IDs from Jamf Pro inventory, with optional scoping via a Smart Computer Group,
  • issues the SET_RECOVERY_LOCK MDM command,
  • exposes rotated_count and failed_count outputs for downstream workflow steps or reporting.

The runner executes recovery-lock-rotation.sh with zsh (installed automatically on ubuntu-latest if needed), enabling consistent execution and local testing on macOS.

Potential use cases

  • Scheduled rotation (e.g. monthly or quarterly) across all enrolled Mac computers with valid Jamf Pro Management IDs
  • Targeted rotation for a specific Smart Computer Group (e.g. “All Managed Computers” or “Recovery Lock Rotation Group”)
  • Dry-run workflows to validate API roles, group scoping, and inventory without sending MDM commands
  • Conditional pipelines that branch on failed_count (e.g. trigger alerts if any device fails)

Jamf Pro requirements

  • API client / role with the following privileges:
    • Read Computers
    • Read Smart Computer Groups
    • View MDM command information in Jamf Pro API
  • Jamf Pro instance URL (trailing slash optional, normalized internally)

Important

Secrets JAMF_CLIENT_ID and JAMF_CLIENT_SECRET are not Action inputs: set them on the job or step env (typically sourced from repository secrets).

Usage

Add a workflow job that sets the Jamf API Client environment variables, then invoke this Action with at least jamf_url.

Note

No actions/checkout is required unless you use wordlist_path to point at a file in your own repository.

jobs:
  rotate-recovery-lock:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
      JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
    steps:
      - uses: Inetum-Poland/jamf-recovery-lock-rotation@v1
        id: jamf_recovery_lock_rotation
        with:
          jamf_url: ${{ vars.JAMF_URL }}

Where v1 or vX.X.X is the tag or SHA you pin to.

Inputs

Input Description Required Default
jamf_url Jamf Pro base URL (e.g. https://example.jamfcloud.com). yes
rotation_scope all = every computer with a managementId in inventory, or the exact smart computer group name for a scoped run. no all
clear_passwords If true, skips the wordlist and sends SET_RECOVERY_LOCK with an empty newPassword (clears Recovery Lock). Respects dry_run. no false
dry_run If true, logs intended work but does not call the MDM commands API. no false
show_passwords_in_dry_run With dry_run: true, logs at WARN: generated passphrases when rotating, or explicit clear intent with IDs when clear_passwords is true. Ignored when dry_run is not true. no false
log_level One of: debug, info, warn, error. no info
wordlist Bundled list relative to the action root, e.g. wordlists/eff_short_wordlist_1.txt. Ignored if wordlist_path is set. no (empty)
wordlist_path Absolute path on the runner (use after actions/checkout, e.g. ${{ github.workspace }}/path/to/list.txt). Overrides wordlist. no (empty)
word_count Number of random words per passphrase. no 4
delimiter Joins words in the passphrase. no -
inventory_id_batch_size Batch size for id=in=(…) when resolving smart-group members against v3 inventory. no 80

About secrets and jamf_url

  • JAMF_CLIENT_ID and JAMF_CLIENT_SECRET: define on env at job or step level (for example secrets.JAMF_CLIENT_ID). They are intentionally not Action inputs.
  • jamf_url: often stored as a repository variable (vars.JAMF_URL) since it is usually non-secret.

About wordlist vs wordlist_path

  • github.action_path exists only inside the composite Action. Callers cannot build paths to bundled files from with: alone. Use wordlist for any file shipped with this Action (e.g. wordlists/eff_large_wordlist.txt).
  • Use wordlist_path with ${{ github.workspace }}/… after actions/checkout for a list stored in your repo.

If both wordlist and wordlist_path are empty, the script default applies (bundled wordlists/eff_large_wordlist.txt next to the script), unless clear_passwords: true (wordlist is not used).

Outputs

Output Description
rotated_count Devices for which SET_RECOVERY_LOCK succeeded: new passphrase, clear (clear_passwords), or simulated in dry run.
failed_count Devices skipped or failed (partial failures still produce a non-zero count).

The underlying script exits with 1 (configuration), 2 (hard API/auth failure), or 3 (partial failure: some devices failed, some succeeded).

Examples

These mirror the repository workflows examples/workflows/rotate-recovery-lock-scheduled.yml, examples/workflows/rotate-recovery-lock-manual.yml, examples/workflows/clear-recovery-lock-manual.yml and examples/workflows/rotate-recovery-lock-dry-run.yml.

Scheduled rotation + manual run (Rotate Recovery Lock)

name: Rotate Recovery Lock

on:
  schedule:
    - cron: '0 2 1 * *'   # 02:00 UTC on the 1st of each month
  workflow_dispatch:

jobs:
  rotate-recovery-lock:
    name: Rotate Recovery Lock
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
      JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
    steps:
      - name: Run Jamf Recovery Lock Rotation
        id: jamf_recovery_lock_rotation
        uses: Inetum-Poland/jamf-recovery-lock-rotation@v1
        with:
          jamf_url: ${{ vars.JAMF_URL }}
          rotation_scope: ${{ vars.ROTATION_SCOPE }}
          # dry_run: 'true'

      - name: Report counts
        shell: bash
        run: |
          echo "rotated_count=${{ steps.jamf_recovery_lock_rotation.outputs.rotated_count }}"
          echo "failed_count=${{ steps.jamf_recovery_lock_rotation.outputs.failed_count }}"

Last day of each quarter (alternative cron)

GitHub Actions schedule uses UTC. The last calendar day of each quarter is Mar 31, Jun 30, Sep 30, and Dec 31. Standard five-field cron cannot express “last day of month” in one line, so use two entries: one for months that end on the 30th, one for months that end on the 31st.

on:
  schedule:
    # 02:00 UTC on the last day of each quarter
    - cron: '0 2 30 6,9 *'   # Jun 30, Sep 30
    - cron: '0 2 31 3,12 *'  # Mar 31, Dec 31
  workflow_dispatch:

Combine with the same jobs: block as in the example above (rotate-recovery-lock job and steps).

Manual clear of Recovery Lock (Clear Recovery Lock (Manual))

Use clear_passwords: 'true' to remove Recovery Lock via an empty newPassword (no wordlist). Prefer a narrow rotation_scope (smart group) and a dry_run first; the full file lives at examples/workflows/clear-recovery-lock-manual.yml.

name: Clear Recovery Lock (Manual)

on:
  workflow_dispatch:

jobs:
  rotate-recovery-lock:
    name: Clear Recovery Lock
    runs-on: ubuntu-latest
    timeout-minutes: 30

    env:
      JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
      JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}

    steps:
      - name: Run Jamf Recovery Lock Rotation
        id: jamf_recovery_lock_rotation
        uses: Inetum-Poland/jamf-recovery-lock-rotation@v1
        with:
          jamf_url: ${{ vars.JAMF_URL }}
          rotation_scope: ${{ vars.ROTATION_SCOPE }}
          clear_passwords: 'true'
          dry_run: ${{ vars.DRY_RUN }}

      - name: Report counts
        shell: bash
        run: |
          echo "rotated_count=${{ steps.jamf_recovery_lock_rotation.outputs.rotated_count }}"
          echo "failed_count=${{ steps.jamf_recovery_lock_rotation.outputs.failed_count }}"

Dry run (Rotate Recovery Lock (Dry Run))

Same job and step id as above; workflow file: rotate-recovery-lock-dry-run.yml. Uses repository variables for toggles:

name: Rotate Recovery Lock (Dry Run)

on:
  workflow_dispatch:

jobs:
  rotate-recovery-lock:
    name: Rotate Recovery Lock (Dry Run)
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      JAMF_CLIENT_ID: ${{ secrets.JAMF_CLIENT_ID }}
      JAMF_CLIENT_SECRET: ${{ secrets.JAMF_CLIENT_SECRET }}
    steps:
      - name: Run Jamf Recovery Lock Rotation
        id: jamf_recovery_lock_rotation
        uses: Inetum-Poland/jamf-recovery-lock-rotation@v1
        with:
          jamf_url: ${{ vars.JAMF_URL }}
          rotation_scope: ${{ vars.ROTATION_SCOPE }}
          dry_run: 'true'
          show_passwords_in_dry_run: ${{ vars.SHOW_PASSWORDS_IN_DRY_RUN }}
          log_level: ${{ vars.LOG_LEVEL }}

      - name: Report counts
        shell: bash
        run: |
          echo "rotated_count=${{ steps.jamf_recovery_lock_rotation.outputs.rotated_count }}"
          echo "failed_count=${{ steps.jamf_recovery_lock_rotation.outputs.failed_count }}"

Smart computer group scope

Use rotation_scope with the exact smart group name (or drive it from vars.ROTATION_SCOPE as in the workflows above).

Bundled wordlist other than the default

        with:
          jamf_url: ${{ vars.JAMF_URL }}
          wordlist: wordlists/eff_short_wordlist_1.txt

Custom wordlist from the caller repository

    steps:
      - uses: actions/checkout@v6

      - name: Run Jamf Recovery Lock Rotation
        id: jamf_recovery_lock_rotation
        uses: Inetum-Poland/jamf-recovery-lock-rotation@v1
        with:
          jamf_url: ${{ vars.JAMF_URL }}
          wordlist_path: ${{ github.workspace }}/security/recovery-lock-wordlist.txt

Contribution

Contributions are welcome! To contribute, create a fork of this repository, commit and push changes to a branch of your fork, and then submit a pull request. Your changes will be reviewed by a project maintainer.

Contributions don’t have to be code; we appreciate any help in answering issues.


Credits

Jamf Recovery Lock Rotation was created by the Apple Business Unit at Inetum Polska Sp. z o.o.

Jamf Recovery Lock Rotation is licensed under the Apache License, version 2.0.

About

GitHub Action for macOS Recovery Lock rotation in Jamf Pro with secure passphrase generation and no infrastructure required.

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Contributors

Languages