Automation & CI/CD
NetDefense supports two complementary automation patterns: static token authentication for CI/CD pipelines that need non-interactive access, and structured output for scripting and downstream tool integration.
Personal Access Tokens for CI/CD
Section titled “Personal Access Tokens for CI/CD”The interactive browser login (ndcli auth login) does not work in headless environments like GitHub Actions or GitLab CI — there is no browser to redirect to. Personal Access Tokens (PATs) solve this: you create a token once on your workstation, store it as a CI secret, and every pipeline run uses it automatically via the NDCLI_TOKEN environment variable.
See the Personal Access Tokens guide for a full overview of token scopes, expiration, and the permission model.
Quick setup
Section titled “Quick setup”-
Create a token on your workstation (requires interactive login):
Terminal window ndcli auth token create --name "github-ci" --scope rw --expiry 90dCopy the displayed
ndpat_...value — it is shown only once. -
Add the token as a CI secret. In GitHub Actions: Settings > Secrets and variables > Actions > New repository secret. Name it
NDCLI_TOKEN. -
Reference the secret in your pipeline using
NDCLI_TOKEN: ${{ secrets.NDCLI_TOKEN }}(GitHub) or$NDCLI_TOKEN(GitLab). -
Run any
ndclicommand. The token is picked up automatically — no login prompt.
CI provider examples
Section titled “CI provider examples”name: Sync Firewall Configon: push: branches: [main]
jobs: sync: runs-on: ubuntu-latest steps: - name: Install ndcli run: | brew tap netdefense-io/tap brew trust netdefense-io/tap brew install ndcli
- name: List devices env: NDCLI_TOKEN: ${{ secrets.NDCLI_TOKEN }} run: | ndcli device list -f json
- name: Sync configuration env: NDCLI_TOKEN: ${{ secrets.NDCLI_TOKEN }} run: | ndcli sync apply --org my-org --yesstages: - deploy
sync-config: stage: deploy image: debian:stable-slim before_script: - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates - >- curl -s https://api.github.com/repos/netdefense-io/ndcli-releases/releases/latest | grep "browser_download_url.*linux_amd64.tar.gz" | cut -d '"' -f 4 | xargs curl -L -o ndcli.tar.gz - tar xzf ndcli.tar.gz && install -m0755 ndcli /usr/local/bin/ndcli script: - ndcli device list -f json - ndcli sync apply --org my-org --yes variables: NDCLI_TOKEN: $NDCLI_TOKEN # Set in GitLab CI/CD > Variables only: - main#!/usr/bin/env bash# Works in any CI system that supports environment variables.# Set NDCLI_TOKEN before running this script.
set -euo pipefail
if [ -z "${NDCLI_TOKEN:-}" ]; then echo "Error: NDCLI_TOKEN is not set." >&2 exit 1fi
ndcli device list -f jsonndcli sync apply --org my-org --yesToken rotation
Section titled “Token rotation”Tokens have a finite lifetime (90 days by default). To rotate without pipeline downtime:
-
Create the replacement token locally:
Terminal window ndcli auth token create --name "github-ci-v2" --scope rw --expiry 90d -
Update the CI secret with the new token value.
-
Verify the pipeline passes with the new token.
-
Revoke the old token:
Terminal window ndcli auth token revoke github-ci --yes
For automated rotation in scripts (e.g. from a privileged rotation system), you can script steps 1 and 4 using JSON output:
#!/usr/bin/env bash# Requires interactive login — run with a user session, not NDCLI_TOKEN.set -euo pipefail
OLD_NAME="${1:?Usage: $0 <old-token-name> <new-token-name>}"NEW_NAME="${2:?Usage: $0 <old-token-name> <new-token-name>}"
# Create the new token (capture the token value from JSON output)NEW_TOKEN=$(ndcli auth token create --name "$NEW_NAME" --scope rw --expiry 90d -f json | jq -r '.token')
echo "New token created: $NEW_NAME"echo "Token value: $NEW_TOKEN"echo ""echo "Update your CI secret to: $NEW_TOKEN"echo "Then run: ndcli auth token revoke $OLD_NAME --yes"JSON output for scripting
Section titled “JSON output for scripting”The list, describe and status commands support multiple output formats via the -f flag. Use -f json to get machine-readable output suitable for jq, Python scripts, or other downstream tools. Commands that change something may print a confirmation line instead of JSON, so rely on their exit status, and see Errors in scripts for failures and partial failures.
Output formats
Section titled “Output formats”| Format | Flag | Use case |
|---|---|---|
| Table | -f table (default) | Human-readable aligned columns for terminal use. |
| Simple | -f simple | Compact key-value output for quick inspection. |
| Detailed | -f detailed | Expanded view with all fields and relationships. |
| JSON | -f json | Machine-readable output for scripting and automation. |
JSON output example
Section titled “JSON output example”ndcli device list -f json{ "devices": [ { "uuid": "e2eb98b8-0ed3-11f1-8792-66a021937fa2", "name": "fw-hq-primary", "status": "ENABLED", "organization": "example-org", "organizational_units": ["production"], "version": "2.4.1", "heartbeat": "2026-02-21T03:17:31Z", "synced_at": "2026-02-21T03:16:43Z" } ], "total": 8}Scripting with jq
Section titled “Scripting with jq”# Get names of all devices not syncedndcli sync status -f json | jq -r '.items[] | select(.in_sync == false) | .device_name'
# Count devices per OUndcli device list -f json | jq -r '.devices[].organizational_units[]' | sort | uniq -c
# Get overlay IPs for all VPN membersndcli network member list corporate-vpn -f json | jq -r '.members[] | "\(.device): \(.overlay_ip)"'Errors in scripts
Section titled “Errors in scripts”A failed command exits non-zero and prints its error on standard error. With -f json the error is a document of its own, {"error": {"code": "...", "message": "..."}}, and code is there when the server gave one. Branch on the exit status, and on code to tell failures apart:
if ! ndcli snippet create web-servers --type ALIAS --file ./web-servers.json -f json 2> err.json; then jq -r '.error.code // "no code"' err.jsonfiSee Errors for the text format.
A partial failure is not a failed command. sync apply exits 0 when only some devices fail, so read errors in its JSON; device approve-all exits 0 whatever fails, and ends with an Approved: N, Failed: M line.
Best practices
Section titled “Best practices”Use org-scoped RO tokens for read-only CI jobs.
If a pipeline only reads device lists or sync status, restrict the token: --scope ro --org <your-org>. A compromised RO token cannot modify any configuration.
Use separate tokens per CI system and environment.
Create one token per pipeline (e.g. github-ci, gitlab-staging, gitlab-prod). This limits the blast radius of a leak and makes it easy to identify unauthorized use via last_used_at.
Align token expiry with your rotation policy.
If you rotate every 90 days, set --expiry 90d. This ensures tokens expire even if rotation is missed.
Monitor last_used_at to find stale tokens.
Run ndcli auth token list periodically, with an interactive login so that it lists every token on the account (a token restricted to one organization lists only the tokens restricted to that same organization). Tokens that have never been used or show a very old last_used_at are candidates for revocation.
Fail the pipeline explicitly if the token is missing.
At the top of every automation script, check that NDCLI_TOKEN is set and fail loudly if it is not — otherwise the script may fall back to prompting for interactive login and hang.
if [ -z "${NDCLI_TOKEN:-}" ]; then echo "Error: NDCLI_TOKEN is not set." >&2 exit 1fi