Skip to main content
RunBook Academy

← All labs in OPNsense

Lab · advanced · ~75 min

Lab: Create an alias via the OPNsense API, safely

B · Nested virtualisationC · Simulation

Objectives

  • Generate a least-privilege service account and API key in the GUI
  • Verify the key works with a read-only API call before any write
  • Create an alias via the API with the search-then-add-then-verify pattern
  • Confirm the alias is loaded into PF and visible to pfctl
  • Delete the alias via the API and confirm the rollback

Prerequisites

This lab makes you fluent with the OPNsense REST API for the most common write operation: creating an alias. You will generate a least-privilege API key, build a script that creates an alias with the disciplined search-then-add-then-verify pattern, confirm the alias is loaded into PF, and then roll it back. The discipline on display is the production one: every write is preceded by a read, every write is followed by a verify, and every change has a rollback path tested before the change is committed.

By the end you will have a script that an automation platform could call from a CI pipeline, and you will have the discipline to know what the script is doing and why each step matters.

Objective

By the end of this lab, you can:

  • Generate a least-privilege service account and API key in the GUI.
  • Verify the key works with a read-only API call before any write.
  • Create an alias via the API with the search-then-add-then-verify pattern.
  • Confirm the alias is loaded into PF and visible to pfctl.
  • Delete the alias via the API and confirm the rollback.

Requirements

  • An OPNsense instance you can reach via SSH and the API (HTTPS, default port 443).
  • A workstation with curl and jq installed.
  • Permission to read the API documentation in the GUI (System → Access → Users and System → API).
  • A secrets manager or a local file with chmod 600 permissions to hold the API key. Never commit the key to git.

Tasks

Task 1: Create the service account

In the GUI, navigate to System → Access → Users → Add.

  • Username: lab-automation-alias
  • Password: a long random value — the API never uses it, but the field is required
  • Description: Service account for alias automation (lab)
  • Shell: /sbin/nologin (this account is for API only, not interactive login)

Save the user.

Task 2: Add the user to a privilege group

The service account needs the privilege to manage aliases. The cleanest pattern is to assign the privilege directly to the user (via the Effective Privileges tab) rather than creating a new group for one user.

Navigate to System → Access → Users → lab-automation-alias → Effective Privileges. Add the privilege:

  • page-access.firewall.alias — read and write aliases

Do not add any other privilege. The principle is: the account can do exactly what the caller needs to do and nothing more.

Task 3: Generate the API key and secret

Navigate to System → Access → Users → lab-automation-alias and add an entry under API keys.

The browser downloads a single ini-formatted file containing two lines — the key and the secret:

key=w86XNZob8Oq8aC5hxh2hevLN00r0kbNarNtdpoQU781fyoeaOBQsBwkXUt
secret=puOyw0Ega3xZXeD26XVrJ5WYFepOseySWLM53pJASeTA3

The key can be read back from the GUI later; the secret cannot, because the firewall stores only a hash of it. That download is the only copy.

Move the file somewhere with restrictive permissions:

mkdir -p ~/.opnsense-lab
mv ~/Downloads/apikey.txt ~/.opnsense-lab/alias-api-key
chmod 600 ~/.opnsense-lab/alias-api-key
ls -l ~/.opnsense-lab/alias-api-key

The output should show -rw-------. Anyone who can read this file has exactly the power the automation has. Treat it like a private SSH key.

Load both halves into the shell. The file is key=value pairs, so it can be sourced directly:

set -a; . ~/.opnsense-lab/alias-api-key; set +a
KEY="$key"; SECRET="$secret"

# Substitute your own firewall address before running:
FIREWALL=https://192.0.2.1

Task 4: Verify the pair works with a read-only call

Before any write, verify the credentials work. The safest first call is a read-only one that returns information about the running firewall:

curl -sk -u "$KEY:$SECRET" "$FIREWALL/api/core/firmware/status" | jq .

-u is the whole authentication story. curl builds an Authorization: Basic <base64(key:secret)> header from it, which is exactly what OPNsense expects — the key as the username, the secret as the password. There is no bearer token and no custom header; sending the key alone in an Authorization header returns 401.

The response should be a JSON object including product_version and os_version. If the call returns 401, the key or the secret is wrong — re-check both against the downloaded file. If it returns 403, the credentials authenticated but the account lacks the privilege for that endpoint; re-check the privilege assignment.

That distinction is worth internalising now, because it saves most of the debugging later: 401 means the credentials were not accepted; 403 means they were, and the user is not allowed to do this.

Test the alias endpoint specifically:

curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias/searchItem" | jq '.total'

The response is the number of existing aliases.

Task 5: Snapshot the existing aliases

Before any write, snapshot what exists. The diff between before and after is the proof the change did what you intended.

curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias/searchItem" > /tmp/aliases-before.json

jq -r '.rows[] | "\(.name)|\(.type)|\(.description)"' \
  /tmp/aliases-before.json | sort > /tmp/aliases-before.txt
wc -l /tmp/aliases-before.txt

The output is the number of aliases on the firewall. You will compare this against the count after the lab.

Task 6: Verify the alias name does not exist yet

The disciplined pattern is to search for the alias before adding it. A duplicate name is a soft error that the API might handle silently (the response is a UUID, but the alias might not actually be created if the validation rejected the duplicate).

curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias/getAliasUUID/lab_test_alias" | jq .

getAliasUUID maps an alias name to its UUID, which makes it a convenient existence check: an alias that does not exist comes back with an empty uuid. If it comes back populated, the name is taken — pick a different one or delete the existing alias before continuing.

Task 7: Create the alias

The endpoint for adding an alias is /api/firewall/alias/addItem. Note the field name: description, not descr — the alias model defines description, and an unknown field is ignored rather than rejected, so getting it wrong saves an alias with no description and reports success.

curl -sk -u "$KEY:$SECRET" -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "alias": {
      "name": "lab_test_alias",
      "type": "host",
      "enabled": "1",
      "description": "Lab test alias — single host, safe to delete",
      "content": "192.0.2.50"
    }
  }' \
  "$FIREWALL/api/firewall/alias/addItem"

The response should be a JSON object with "result": "saved" and a UUID for the new alias:

{
  "result": "saved",
  "uuid": "f1e2d3c4-b5a6-7890-1234-567890abcdef"
}

Save the UUID — every later call addresses the alias by it:

ALIAS_UUID=$(curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias/getAliasUUID/lab_test_alias" | jq -r '.uuid')

echo "alias UUID: $ALIAS_UUID"

Now apply the change. Saving wrote the alias to the configuration; the pf table behind it does not exist yet:

curl -sk -u "$KEY:$SECRET" -X POST \
  "$FIREWALL/api/firewall/alias/reconfigure"

Task 8: Verify the alias is in the configuration

A saved response from addItem is not enough. It says the alias is in the configuration; it says nothing about whether the pf table behind it exists. Verify in three places.

Step 1: The alias is in the configuration.

curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias/getItem/$ALIAS_UUID" | jq .

The response should show the alias with all the fields you set. Check description in particular — if it is empty, the payload used the wrong field name.

Step 2: The alias is a pf table, read through the API.

curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias_util/list/lab_test_alias" | jq '.rows'

The response should list 192.0.2.50. This reads the table pf is actually holding, not the configuration — which is the whole point of the step.

Step 3: The same table, read on the firewall itself.

The pf table takes the alias’s name, so from a shell on the firewall:

pfctl -t lab_test_alias -T show

The output should show 192.0.2.50. If step 1 succeeds and steps 2 and 3 come back empty, the alias was saved but never applied — go back and run reconfigure.

Task 9: Use the alias in a temporary rule

The alias is created but not used. The production-grade verification is to use it in a rule and confirm the rule matches the expected traffic.

Navigate to the GUI and add a temporary rule that references the alias:

  • Interface: LAN
  • Source: lab_test_alias
  • Destination: any
  • Action: pass
  • Description: Lab test rule using lab_test_alias

Apply the change. From a host at the IP 192.0.2.50, generate traffic:

curl --max-time 5 https://example.com/ > /dev/null && echo "OK"

Confirm the state was created:

pfctl -s state | grep 192.0.2.50

The state confirms the rule matched (the alias resolved to the IP, the rule permitted the traffic, and PF created state).

Task 10: Delete the alias via the API

The lab is now ready for rollback. Delete the alias:

curl -sk -u "$KEY:$SECRET" -X POST \
  "$FIREWALL/api/firewall/alias/delItem/$ALIAS_UUID"

The UUID goes in the path, not the body. The response should be a JSON object with "result": "deleted".

If instead you get an error naming what is using the alias, the test rule from Task 9 is still referencing it — the API refuses to delete an alias that is in use, which is a guard rail rather than a fault. Remove the rule first (Task 12), then come back.

Then apply, so the table goes away as well as the record:

curl -sk -u "$KEY:$SECRET" -X POST \
  "$FIREWALL/api/firewall/alias/reconfigure"

Task 11: Verify the alias is gone

Wait a few seconds for the resolver to reload, then verify in all three places.

Step 1: The alias is not in the configuration.

curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias/getAliasUUID/lab_test_alias" | jq .

The uuid should come back empty.

Step 2: The pf table is gone.

pfctl -t lab_test_alias -T show

pf should report that no such table exists. An empty table that still exists means the delete saved but was not applied.

Step 3: The alias is absent from the listing.

curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias/searchItem" \
  | jq '.rows | map(select(.name == "lab_test_alias")) | length'

The result should be 0.

Task 12: Remove the test rule

The lab rule from Task 9 is still in the firewall. Remove it from the GUI:

  • Firewall → Rules → LAN → find the rule with description “Lab test rule using lab_test_alias” → delete → apply

Verify the rule is gone:

pfctl -s rules | grep lab_test_alias

The output should be empty.

Task 13: Snapshot the aliases after the lab

curl -sk -u "$KEY:$SECRET" \
  "$FIREWALL/api/firewall/alias/searchItem" > /tmp/aliases-after.json

jq -r '.rows[] | "\(.name)|\(.type)|\(.description)"' \
  /tmp/aliases-after.json | sort > /tmp/aliases-after.txt

diff /tmp/aliases-before.txt /tmp/aliases-after.txt

The diff should be empty. The alias was created and removed without leaving any residue.

Validation

  • A service account with the minimum privilege (page-access.firewall.alias) is created and an API key/secret pair is generated.
  • The pair is verified with a read-only call, using Basic auth, before any write.
  • The alias is created via the API and applied with a separate reconfigure call.
  • The alias is confirmed in the configuration and in the running pf table.
  • A temporary rule using the alias matches traffic correctly.
  • The alias is deleted via the API and confirmed gone in all three places.
  • The diff between the pre-lab and post-lab alias lists is empty.

Cleanup

The lab is largely self-cleaning. The remaining cleanup is the service account and the API key.

# Remove the service account
# GUI → System → Access → Users → lab-automation-alias → delete
# Confirm the user is removed

# Remove the local API key file
shred -u ~/.opnsense-lab/alias-api-key

# Remove the temp test directory
rm -rf ~/.opnsense-lab

To verify the service account is gone:

curl -sk -o /dev/null -w '%{http_code}\n' -u "$KEY:$SECRET" \
  "$FIREWALL/api/core/firmware/status"

The call should now return 401. The key/secret pair belonged to the deleted user, so it no longer resolves to anything.

Then clear the credentials out of the shell, so they do not survive in the environment or the history:

unset KEY SECRET key secret

What you learned

  • The OPNsense API authenticates with an API key/secret pair presented as HTTP Basic credentials — curl -u "$KEY:$SECRET", the key as username and the secret as password. There is no bearer token and no custom header, and 401 and 403 tell you two different things.
  • The pair belongs to a user account and carries that account’s privileges. Scoping is done by scoping the user.
  • The disciplined lifecycle for any write is search, add, apply, verify. The apply step is separate and easy to forget.
  • The API response is not proof that the change is live. The pf table is the proof, and the two can disagree indefinitely.
  • A least-privilege service account is the production default. Root is for break-glass.
  • The API key is a credential. Store it in a secrets manager. Rotate it. Revoke it on personnel changes.

Deliverables

  • · A service account with the minimum privilege needed for alias management
  • · An API key/secret pair saved outside source control, in a file with restrictive permissions
  • · A working API script that creates an alias with full verification
  • · A clean rollback that removes the alias and confirms PF no longer references it

Verification status

Last reviewed
2026-08-14
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.