Skip to content

jaredhendrickson13/pfsense-api

Repository files navigation

pfSense-API

Build OpenAPI PHPlint Pylint

Introduction

pfSense API is a fast, safe, REST API package for pfSense firewalls. This works by leveraging the same PHP functions and processes used by pfSense's webConfigurator into API endpoints to create, read, update and delete pfSense configurations. All API endpoints enforce input validation to prevent invalid configurations from being made. Configurations made via API are properly written to the master XML configuration and the correct backend configurations are made preventing the need for a reboot. All this results in the fastest, safest, and easiest way to automate pfSense!

Requirements

Supported pfSense Versions
  • pfSense CE 2.7.1 (amd64)
  • pfSense CE 2.7.2 (amd64)
  • pfSense Plus 23.09 (community supported)

Don't see your version listed? Check the releases page. Older versions of this package may support older versions of pfSense.

This package is not supported on other architectures such as arm64 and aarch64. However, the package should still install and operate on these systems. Compatibility on unsupported systems is not guaranteed and is at your own risk.


  • pfSense API requires a local user account in pfSense. The same permissions required to make configurations in the webConfigurator are required to make calls to the API endpoints.
  • While not an enforced requirement, it is strongly recommended that you configure pfSense to use HTTPS instead of HTTP. This ensures that login credentials and/or API tokens remain secure in-transit.

Installation

To install pfSense API, simply run the following command from the pfSense shell:

pkg -C /dev/null add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-2.7-pkg-API.pkg && /etc/rc.restart_webgui

To uninstall pfSense API, run the following command:

pfsense-api delete

To update pfSense API to the latest stable version, run the following command:

pfsense-api update

To revert to a previous version of pfSense API (e.g. v1.1.7), run the following command:

pfsense-api revert v1.1.7

Notes:

  • While not always necessary, it's recommended to change the installation command to reference the package built for your version of pfSense. You can check the releases page for available versions.
  • In order for pfSense to apply some required web server changes, it is required to restart the webConfigurator after installing the package.
  • If you do not have shell access to pfSense, you can still install via the webConfigurator by navigating to ' Diagnostics > Command Prompt' and enter the commands there.
  • When updating pfSense, you must reinstall pfSense API afterwards. Unfortunately, pfSense removes all existing packages and only re-installs packages found within pfSense's package repositories. Since pfSense API is not an official package in pfSense's repositories, it does not get reinstalled automatically.
  • The pfsense-api command line tool was introduced in v1.1.0. Refer to the corresponding documentation for earlier releases.

webConfigurator Settings & Documentation

After installation, you will be able to access the API user interface pages within the pfSense webConfigurator. These will be found under System > API. The settings tab will allow you change various API settings such as enabled API interfaces, authentication modes, and more. Additionally, the documentation tab will give you access to an embedded documentation tool that makes it easy to view the full API documentation in context to your pfSense instance.

Notes:

  • Users must hold the page-all or page-system-api privileges to access the API page within the webConfigurator.

Authentication & Authorization

By default, pfSense API uses the same credentials as the webConfigurator. This behavior allows you to configure pfSense from the API out of the box, and user passwords may be changed from the API to immediately add additional security if needed. After installation, you can navigate to System > API in the pfSense webConfigurator to configure API authentication. Please note that external authentication servers like LDAP or RADIUS are not supported with any API authentication method at this time. To authenticate your API call, follow the instructions for your configured authentication mode:

Local Database (default)

Uses the same credentials as the pfSense webConfigurator. To authenticate API calls, pass in your username and password using basic authentication. For example:

curl -u admin:pfsense https://pfsense.example.com/api/v1/firewall/rule

JWT

Requires a bearer token to be included in the Authorization header of your request. These are time-based tokens that will expire after the configured amount of time. To configure the JWT expiration, navigate to System > API within the webConfigurator and ensure the the Authentication Mode is set to JWT. Then you should have the option to configure the JWT Expiration value. Alternatively, you can use the /api/v1/system/api endpoint to update the jwt_exp value. To receive a JWT, you must make a POST request to the /api/v1/access_token endpoint. This endpoint will always require the use of the Local Database authentication type to receive the JWT.

For example:

curl -u admin:pfsense -X POST https://pfsense.example.com/api/v1/access_token



Once you have your JWT, you can authenticate your API calls by adding it to the request's authorization header. For example:

curl -H "Authorization: Bearer xxxxx.xxxxxx.xxxxxx" -X GET https://pfsense.example.com/api/v1/system/arp
API Token

Uses standalone tokens generated via API or webConfigurator. These are better suited to distribute to systems as they are revocable and will only allow API authentication; not webConfigurator or SSH authentication (like the local database credentials). To generate or revoke credentials, navigate to System > API within the webConfigurator and ensure the Authentication Mode is set to API token. Then you should have the options to configure API Token generation, generate new tokens, and revoke existing tokens. After generating a new API token, the actual token will display at the top of the page on the success banner. This token will only be displayed once so ensure it is stored somewhere safe. Alternatively, you can generate new API tokens using the /api/v1/access_token endpoint. This endpoint will always require the use of the Local Database authentication type to receive the API token.

Once you have your API token, you may authenticate your API call by specifying your client-id and client-token within an Authorization header, these values must be separated by a space. For example:

curl -H "Authorization: CLIENT_ID_HERE CLIENT_TOKEN_HERE" -X GET https://pfsense.example.com/api/v1/system/arp

Authorization

pfSense API uses the same privileges as the pfSense webConfigurator. The required privileges for each endpoint are stated within the API documentation.

Login Protection

By default, all API requests will be monitored by pfSense's Login Protection feature. This will allow API authentication attempts to be logged and temporarily blocked if too many failed authentication attempts are made by any one client. It is strongly recommended that this feature be used at all times to prevent brute force attacks on API endpoints. This feature can be disabled by within the webConfigurator system-wide under System > Advanced or only for API requests under System > API.

Content Types

pfSense API can handle a few different content types. Please note, if a Content-Type header is not specified in your request, pfSense API will attempt to determine the content type which may have undesired results. It is recommended you specify your preferred Content-Type on each request.

While several content types may be enabled, application/json is the recommended content type. Supported content types are:

application/json

Parses the request body as a JSON formatted string. This is the recommended content type.

Example:

curl -u admin:pfsense -H "Content-Type: application/json" -d '{"service": "sshd"}' -X POST https://pfsense.example.com/api/v1/services/restart
application/x-www-form-urlencoded

Parses the request body as URL encoded parameters.

Example:

curl -u admin:pfsense -H "Content-Type: application/x-www-form-urlencoded" -X DELETE "https://pfsense.example.com/api/v1/firewall/alias?id=EXAMPLE_ALIAS"



Note: this content type only has the ability to pass values of string, integer, or boolean data types. Complex data types like arrays and objects cannot be parsed by the API when using this content type. It is recommended to only use this content type when making GET or DELETE requests.

Queries

pfSense API contains an advanced query engine to make it easy to query specific data from API calls. For endpoints supporting GET requests, you may query the return data to only return data you are looking for. To query data, you may add the data you are looking for to your payload. You may specify as many query parameters as you need. In order to match the query, each parameter must match exactly, or utilize a query filter to set criteria. If no matches were found, the endpoint will return an empty array in the data field.

Targeting Objects

You may find yourself only needing to read objects with specific values set. For example, say an API endpoint normally returns this response without a query:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

If you want the endpoint to only return the objects that have their type value set to type1 you could add {"type": "type1"} to your payload. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

Additionally, if you need to target values that are nested within an array, you can add {"extra__tag": 100} to recursively target the tag value within the extra array. Note the double underscore separating the parent and child keys. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    }
  ]
}
Query Filters

Query filters allow you to apply logic to the objects you target. This makes it easy to target data that meets specific criteria:

Starts With

The startswith filter allows you to target objects whose values start with a specific substring. This will work on both string and integer data types. Below is an example response without any queries:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

If you wanted to target objects whose names started with Other, you could use the payload {"name__startswith": "Other"}. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    }
  ]
}

Ends With

The endswith filter allows you to target objects whose values end with a specific substring. This will work on both string and integer data types. Below is an example response without any queries:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

If you wanted to target objects whose names ended with er Test, you could use the payload {"name__endswith" "er Test"}. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

Contains

The contains filter allows you to target objects whose values contain a specific substring. This will work on both string and integer data types. Below is an example response without any queries:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

If you wanted to target objects whose names contain ther, you could use the payload {"name__contains": "ther"}. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

Less Than

The lt filter allows you to target objects whose values are less than a specific number. This will work on both numeric strings and integer data types. Below is an example response without any queries:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
} 

If you wanted to target objects whose tag is less than 100, you could use the payload {"extra__tag__lt": 100}. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    }
  ]
}

Less Than or Equal To

The lte filter allows you to target objects whose values are less than or equal to a specific number. This will work on both numeric strings and integer data types. Below is an example response without any queries:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

If you wanted to target objects whose tag is less than or equal to 100, you could use the payload {"extra__tag__lte": 100}. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    }
  ]
}

Greater Than

The gt filter allows you to target objects whose values are greater than a specific number. This will work on both numeric strings and integer data types. Below is an example response without any queries:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

If you wanted to target objects whose tag is greater than 100, you could use the payload {"extra__tag__gt": 100}. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

Greater Than or Equal To

The lte filter allows you to target objects whose values are greater than or equal to a specific number. This will work on both numeric strings and integer data types. Below is an example response without any queries:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 0,
      "name": "Test",
      "type": "type1",
      "extra": {
        "tag": 0
      }
    },
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}

If you wanted to target objects whose tag is greater than or equal to 100, you could use the payload {"extra__tag__gte": 100}. This returns:

{
  "status": "ok",
  "code": 200,
  "return": 0,
  "message": "Success",
  "data": [
    {
      "id": 1,
      "name": "Other Test",
      "type": "type2",
      "extra": {
        "tag": 100
      }
    },
    {
      "id": 2,
      "name": "Another Test",
      "type": "type1",
      "extra": {
        "tag": 200
      }
    }
  ]
}
Obtaining Object IDs

You may notice some API endpoints require an object id to update or delete objects. These IDs are not stored values, rather pfSense uses the object's array index value to locate and identify objects. Unless specified otherwise, API endpoints will use the same array index value (as returned in the data response field) to locate objects when updating or deleting. Some important items to note about these IDs:

  • pfSense starts arrays with an index of 0. If you use a loop counter to determine the ID of a specific object, you must start that counter at 0.
  • These IDs are dynamic. For example, if you have 3 static route objects stored (IDs 0, 1, and 2) and you delete the object with ID 1, pfSense will resort the array so the object with ID 2 will now have an ID of 1.
  • API queries will retain the object's ID in the response. In the event that the data response field is no longer a sequential array due to the query, the data field will be represented as an associative array with the array items` key being the objects ID.

Requirements for queries:

  • API call must be a successful GET request and return 0 in the return field.
  • Endpoints must return an array of objects in the data field ( e.g. [{"id": 0, "name": "Test"}, {"id": 1, "name": "Other Test"}]).
  • At least two objects must be present within the data field to support queries.

Limitations

There are a few key limitations to keep in mind while using this API:

  • pfSense's XML configuration was not designed for quick simultaneous writes like a traditional database. It may be necessary to delay API calls in sequence to prevent unexpected behavior such as configuration overwrites.
  • By design, values stored in pfSense's XML configuration can only be parsed as strings, arrays or objects. This means that even though request data requires data to be of a certain type, it will not necessarily be stored as that type. Data read from the API may be represented differently than the format it was requested as.