Skip to content

Commit df1dbc2

Browse files
Merge pull request #823 from jaredhendrickson13/next_minor
v2.7.0 Fixes & Features
2 parents 90eb8e8 + 4194563 commit df1dbc2

File tree

90 files changed

+2288
-245
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+2288
-245
lines changed

docs/INSTALL_AND_CONFIG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Overall, the REST API package is designed to be as lightweight as possible and s
88
run pfSense. It's recommended to follow Netgate's [minimum hardware requirements](https://docs.netgate.com/pfsense/en/latest/hardware/minimum-requirements.html).
99

1010
!!! Warning
11-
- The package is currently not compatible with 32-bit builds of pfSense. It is recommended to use the [legacy v1 package](https://github.com/jaredhendrickson13/pfsense-api/tree/legacy) for 32-bit systems.
11+
- The package is currently not supported on 32-bit architectures like the Netgate 3100 (SG-3100).
1212
- While the package should behave identically on 64-bit architectures other than amd64, automated testing only covers amd64
1313
builds of pfSense CE. Support on other architectures is not guaranteed.
1414

docs/QUERIES_FILTERS_AND_SORTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,15 @@ value array contains a given value for array fields.
5252
- Name: `contains`
5353
- Example: `https://pfsense.example.com/api/v2/examples?fieldname__contains=example`
5454

55+
### In (in)
56+
57+
Search for objects whose field value is within a given array of values (when the filter value is an array), or search
58+
for objects whose field value is a substring of a given string (when the filter value is a string).
59+
- Name: `in`
60+
- Examples:
61+
- `https://pfsense.example.com/api/v2/examples?fieldname__in=example`
62+
- `https://pfsense.example.com/api/v2/examples?fieldname__in[]=example1&fieldname__in[]=example2`
63+
5564
### Less Than (lt)
5665

5766
Search for objects whose field value is less than a given integer.

docs/SECURING_API_ACCESS.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Securing API Access
2+
3+
In the age of network automation, APIs on network appliances unlocks incredible efficiency and potential, but it also
4+
can also create a high-stakes attack vector. Unlike standard web applications, a compromised firewall doesn't just leak
5+
data; it can grant attackers administrative control over your network's perimeter, allowing them to rewrite traffic rules
6+
and bypass your defenses entirely. The REST API package includes several built-in security features to help protect
7+
API access and ensure that only authorized users and systems can interact with your pfSense instances.
8+
9+
## Step 1: Follow Netgate's Best Practices for Remote Access
10+
11+
If you need access to the pfSense REST API from outside your local network, it is critical that you follow Netgate's
12+
[Allowing Remote Access to the GUI¶](https://docs.netgate.com/pfsense/en/latest/recipes/remote-firewall-administration.html)
13+
guide to ensure that your pfSense instance is properly secured against unauthorized access. This includes:
14+
15+
- Enabling HTTPS for the web GUI to encrypt traffic between clients and the pfSense instance.
16+
- Using a VPN to connect to your pfSense instance remotely, rather than exposing the web GUI directly to the internet.
17+
- Configuring strong firewall rules to restrict access to the webConfigurator.
18+
19+
## Step 2: Choose an Appropriate Authentication Method
20+
21+
The authentication method you choose for API access will depend on your specific use case and security requirements
22+
for your environment. The pfSense REST API package supports several different authentication methods, and allows
23+
multiple methods to be enabled simultaneously. The three main authentication methods supported are:
24+
25+
### Basic Authentication (Local Database)
26+
27+
[Basic authentication](AUTHENTICATION_AND_AUTHORIZATION.md#basic-authentication-local-database) allows you to authenticate
28+
with the same username and password you use to log into the pfSense web GUI. This method is the default authentication
29+
method for the REST API package as it allows for out-of-the-box functionality without any additional configuration.
30+
However, basic authentication is less secure than other methods and should only be used in trusted environments and
31+
over secure connections (e.g., HTTPS or VPN).
32+
33+
**Pros**:
34+
35+
- Easy to set up and use.
36+
- No additional configuration required.
37+
38+
**Cons**:
39+
40+
- Less secure than other methods.
41+
- Credentials are sent with each request, increasing the risk of interception, especially if not using HTTPS.
42+
- Credentials may also allow web GUI and/or SSH access, which may not be desirable for API-only users.
43+
44+
!!! Note
45+
Basic authentication is not inherently insecure. In fact, with the proper user account management, strong passwords,
46+
and secure transport (HTTPS), basic authentication can be just as secure as key-based authentication methods.
47+
48+
49+
### JWT Authentication
50+
51+
[JWT authentication](AUTHENTICATION_AND_AUTHORIZATION.md#json-web-token-jwt-authentication) allows you to authenticate
52+
using JSON Web Tokens. These are short-lived, digitially signed tokens that can be used to authenticate API requests without
53+
sending a username and password with each request. JWT authentication is more secure than basic authentication and is
54+
recommended for production environments who need session-based or short-lived authentication. This is especially useful
55+
for front-end applications or scripts that need to make multiple API requests over a short period of time.
56+
57+
**Pros**:
58+
59+
- More secure than basic authentication.
60+
- Tokens can be short-lived, reducing the risk of compromise.
61+
- Tokens do not expose pfSense user credentials with each request.
62+
63+
**Cons**:
64+
65+
- Requires additional configuration to set up.
66+
- Tokens need to be refreshed before they expire.
67+
68+
### Key Authentication
69+
70+
[Key authentication](AUTHENTICATION_AND_AUTHORIZATION.md#api-key-authentication) allows you to authenticate using
71+
dedicated API keys. These keys are created specifically for API access and never require a username or password to
72+
be sent with requests. Key authentication is the most secure method and is recommended for production environments
73+
where security is a top priority. This method is especially useful for automated systems or services that need to
74+
make API requests without human intervention.
75+
76+
**Pros**:
77+
78+
- Most secure authentication method.
79+
- API keys can be easily revoked or rotated without affecting user accounts.
80+
- Does not expose pfSense user credentials with requests.
81+
- Supports configurable key-lengths and hashing algorithms for purpose-specific security needs.
82+
83+
**Cons**:
84+
85+
- Requires additional configuration to set up.
86+
- API keys need to be securely stored and managed.
87+
88+
## Step 3: Use API-specific user accounts
89+
90+
Regardless of the authentication method you choose, the REST API package uses pfSense's built-in privilege system to
91+
control access to API endpoints. This means that all credentials used for API access must belong to a pfSense user account
92+
who has been granted the appropriate API privileges. Each endpoint has its own privileges for each HTTP method supported
93+
by that endpoint. It is highly recommended that you create dedicated user accounts specifically for API access, rather
94+
than using existing user accounts that may have broader access. This helps to limit the potential impact of a
95+
compromised account and allows for better tracking of API activity. It is also highly recommended to follow the principle
96+
of least privilege when assigning API privileges to user accounts. Only grant the minimum privileges necessary for the
97+
intended use case.
98+
99+
!!! Warning
100+
The `page-all` privilege grants unrestricted access to all API endpoints and methods. Avoid assigning this privilege
101+
to user accounts unless absolutely necessary, as it significantly increases the risk of unauthorized access and
102+
potential misuse of the API.
103+
104+
## Step 4: Restrict API access to specific interfaces
105+
106+
By default, the pfSense REST API package allows requests received on any interface IP to respond to API requests. However,
107+
you can restrict the API to only respond to requests received on specific interfaces if desired. This can help limit the
108+
exposure of the API to only trusted networks or systems beyond just setting firewall rules. To configure which
109+
interfaces the API will respond to, navigate to `System` > `REST API` > `Settings` > `Allowed Interfaces` and select
110+
the desired interfaces.
111+
112+
!!! Warning
113+
This setting is not a replacement for proper firewall rules. This setting should be used in addition to firewall rules
114+
to provide a layered approach to security. Ensure that you have proper firewall rules in place to restrict access to the API
115+
to only trusted networks or systems, then ensure the API is configured to only respond on those same interfaces.
116+
117+
## Step 5: Configure API access lists
118+
119+
The REST API package includes an API access list feature that allows you to restrict API access based on source IP,
120+
network, time-of-day, and/or user. This provides an additional layer of security by allowing you to define specific rules for who
121+
can access the API, when they can access it, and from where. To configure API access lists, navigate to `System` > `REST API` >
122+
`Access Lists` and create the desired access list rules. When designing your access list rules, consider the following best practices:
123+
124+
- Only allow IPs you trust and have a legitimate use case for accessing the API.
125+
- Only allow the relevant users to access the API from their respective IPs or networks.
126+
- If possible, configure and apply a schedule to apply to the access list rules to limit access to only when necessary.
127+
128+
!!! Warning
129+
This access control list is not a replacement for proper firewall rules. This setting should be used in addition to
130+
firewall rules to provide a layered approach to security. Ensure that you have proper firewall rules in place to
131+
restrict access to the API to only trusted networks or systems, then use the access list to further restrict access
132+
based on your specific requirements.
133+
134+
## Step 6: Update Regularly
135+
136+
Ensure that you are running the latest version of the pfSense REST API package to benefit from the latest security
137+
patches and features. Regularly check for updates and apply them as soon as possible to minimize the risk of vulnerabilities.
138+
139+
!!! Tip
140+
If you are using Prometheus for monitoring in your environment, consider using the [official pfREST Prometheus exporter](https://github.com/pfrest/pfsense_exporter)
141+
to monitor for outdated pfSense REST API package versions across your fleet of pfSense instances!

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ nav:
1313
- Working with HATEOAS: WORKING_WITH_HATEOAS.md
1414
- Common Control Parameters: COMMON_CONTROL_PARAMETERS.md
1515
- GraphQL: GRAPHQL.md
16+
- Securing API Access: SECURING_API_ACCESS.md
1617
- Limitations & FAQs: LIMITATIONS_AND_FAQS.md
1718
- API Reference: https://pfrest.org/api-docs/
1819
- Advanced Topics:

pfSense-pkg-RESTAPI/files/pkg-install.in

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
#!/bin/sh
2+
23
if [ "${2}" != "POST-INSTALL" ]; then
34
exit 0
45
fi
56

7+
# Warn users installing on 32-bit systems
8+
ARCH_BITS=$(/usr/bin/getconf LONG_BIT)
9+
if [ "${ARCH_BITS}" = "32" ]; then
10+
echo "!!! WARNING: This package is not supported on 32-bit systems. !!!"
11+
fi
12+
613
# Make this package known to pfSense
714
/usr/local/bin/php -f /etc/rc.packages %%PORTNAME%% ${2}
815

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/.resources/scripts/manage.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ function delete(): void {
426426
*/
427427
function rotate_server_key(): void {
428428
$pkg_index = RESTAPISettings::get_pkg_id();
429-
config_set_path("installedpackages/package/$pkg_index/conf/keys", []);
429+
Model::set_config("installedpackages/package/$pkg_index/conf/keys", []);
430430
echo 'Rotating REST API server key... ';
431431
RESTAPIJWT::init_server_key(rotate: true);
432432
echo 'done.' . PHP_EOL;

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Auth.inc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ class Auth {
294294
# If this Auth class is being requested by the remote client, set the matched auth and break the loop
295295
if ($auth->is_requested()) {
296296
$matched_auth = $auth;
297+
self::log(
298+
level: LOG_DEBUG,
299+
message: "Client from $auth->ip_address is attempting to authenticate using $auth->verbose_name.",
300+
);
297301
break;
298302
}
299303
}

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/BaseTraits.inc

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@
33
namespace RESTAPI\Core;
44

55
use ReflectionClass;
6+
use RESTAPI\Models\RESTAPISettings;
67
use function RESTAPI\Core\Tools\get_classes_from_namespace;
78

89
/**
910
* Defines a standard set of traits for all classes defined by this package. When class use this trait they will
1011
* automatically inherit all resources included.
1112
*/
1213
trait BaseTraits {
14+
private static int $__log_level;
15+
1316
/**
1417
* Obtains the shortname of the called class.
1518
* @return string The shortname for this object's class.
1619
*/
17-
public function get_class_shortname(): string {
18-
return (new ReflectionClass($this))->getShortName();
20+
public static function get_class_shortname(): string {
21+
return (new ReflectionClass(static::class))->getShortName();
1922
}
2023

2124
/**
2225
* Obtains the fully qualified name of the called class.
2326
* @return string The FQN for this object's class.
2427
*/
25-
public function get_class_fqn(): string {
26-
return (new ReflectionClass($this))->getName();
28+
public static function get_class_fqn(): string {
29+
return (new ReflectionClass(static::class))->getName();
2730
}
2831

2932
/**
@@ -35,11 +38,32 @@ trait BaseTraits {
3538
}
3639

3740
/**
38-
* Logs an error to the syslog.
39-
* @param string $message The error message to write to the syslog
41+
* Writes a log entry to the applicable log file
42+
* @param int $level The log level to write. This should be one of the LOG_* constants defined by syslog.
43+
* @param string $message The message to write to the log file.
44+
* @param string $logfile The log file to write to. This must be a valid logging facility defined in the package's
45+
* info.xml file.
4046
*/
41-
public static function log_error(string $message): void {
42-
# Call the pfSense `log_error` function
43-
log_error($message);
47+
public static function log(int $level, string $message, string $logfile = 'restapi'): void {
48+
# If this is a system log, use the pfSense 'log_error' function instead
49+
if ($logfile === 'system') {
50+
log_error($message);
51+
return;
52+
}
53+
54+
# If the log level has not been set yet, obtain it
55+
if (!isset(self::$__log_level)) {
56+
self::$__log_level = constant(RESTAPISettings::get_pkg_config()['log_level']) ?? 4;
57+
}
58+
59+
# Do not log if the incoming level is higher than the configured log level
60+
if ($level > self::$__log_level) {
61+
return;
62+
}
63+
64+
# Otherwise, write to the applicable log file
65+
openlog($logfile, flags: LOG_PID, facility: LOG_LOCAL0);
66+
syslog(priority: $level, message: $message);
67+
closelog();
4468
}
4569
}

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Dispatcher.inc

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -275,15 +275,6 @@ class Dispatcher {
275275
sleep(30);
276276
}
277277

278-
/**
279-
* Logs an error to the syslog.
280-
* @param string $message The error message to write to the syslog
281-
*/
282-
public static function log_error(string $message): void {
283-
# Call the pfSense `log_error` function
284-
log_error($message);
285-
}
286-
287278
/**
288279
* Configures this Dispatcher to run on a schedule if the `schedule` property is set.
289280
* @return CronJob|null Returns the CronJob created for this Dispatcher if a `schedule` is defined. Returns `null`

pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Core/Endpoint.inc

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -412,14 +412,6 @@ class Endpoint {
412412
);
413413
}
414414
}
415-
416-
# Do not allow `many` Endpoints that are assigned a Model with a parent Model
417-
if ($this->many and $this->model->parent_model_class) {
418-
throw new ServerError(
419-
message: 'Endpoints cannot enable `many` when the assigned Model has a parent Model',
420-
response_id: 'ENDPOINT_MANY_WHEN_MODEL_HAS_PARENT',
421-
);
422-
}
423415
}
424416

425417
/**
@@ -633,13 +625,19 @@ class Endpoint {
633625
private function check_acl(): void {
634626
# Allow the API call if the ignore_acl flag is set
635627
if ($this->ignore_acl) {
628+
$this->log(level: LOG_DEBUG, message: "Ignoring REST API ACL for $this->url as ignore_acl is set.");
636629
return;
637630
}
638631

639632
# Check if the client is allowed to access this Endpoint according to the REST API Access List
640633
if (
641634
!RESTAPIAccessListEntry::is_allowed_by_acl(ip: $this->client->ip_address, username: $this->client->username)
642635
) {
636+
$this->log(
637+
level: LOG_WARNING,
638+
message: "Denied {$this->client->username}@{$this->client->ip_address} access to $this->url for REST " .
639+
'API access list violation.',
640+
);
643641
throw new ForbiddenError(
644642
message: 'The requested action is not allowed by admin policy',
645643
response_id: 'ENDPOINT_CLIENT_NOT_ALLOWED_BY_ACL',
@@ -651,7 +649,10 @@ class Endpoint {
651649
* Checks if the API is enabled before allowing the call.
652650
*/
653651
private function check_enabled(): void {
652+
$client_ip = $_SERVER['REMOTE_ADDR'];
653+
654654
if (!$this->restapi_settings->enabled->value and !$this->ignore_enabled) {
655+
$this->log(level: LOG_WARNING, message: "Denied $client_ip access to $this->url as REST API is disabled.");
655656
throw new ServiceUnavailableError(
656657
message: 'The REST API is currently not enabled.',
657658
response_id: 'ENDPOINT_REST_API_IS_NOT_ENABLED',
@@ -665,6 +666,7 @@ class Endpoint {
665666
private function check_interface_allowed(): void {
666667
# Variables
667668
$server_ip = $_SERVER['SERVER_ADDR'];
669+
$client_ip = $_SERVER['REMOTE_ADDR'];
668670
$allowed_interfaces = $this->restapi_settings->allowed_interfaces->value;
669671

670672
# Allow any interface if the allowed interfaces is empty or the ignore_interfaces flag is set
@@ -678,25 +680,27 @@ class Endpoint {
678680
}
679681

680682
# Loop through each allowed interface and check if the server IP is allowed to answer API calls
681-
foreach (
682-
$this->restapi_settings->allowed_interfaces->get_related_models()->model_objects
683-
as $allowed_interface
684-
) {
683+
foreach ($this->restapi_settings->allowed_interfaces->get_related_models()->model_objects as $allowed_if) {
685684
# Allow the server IP if it matches the current interface's IPv4 or IPv6 address
686-
if ($server_ip === $allowed_interface->get_current_ipv4()) {
685+
if ($server_ip === $allowed_if->get_current_ipv4()) {
687686
return;
688687
}
689-
if ($server_ip === $allowed_interface->get_current_ipv6()) {
688+
if ($server_ip === $allowed_if->get_current_ipv6()) {
690689
return;
691690
}
692691

693692
# Check if this interface has a virtual IP that matches the server IP that accepted the API call
694-
$vip_q = VirtualIP::query(interface: $allowed_interface->represented_as(), subnet: $server_ip);
693+
$vip_q = VirtualIP::query(interface: $allowed_if->represented_as(), subnet: $server_ip);
695694
if ($vip_q->exists()) {
696695
return;
697696
}
698697
}
698+
699699
# Throw a forbidden error if this API call was made to a non-API enabled interface
700+
$this->log(
701+
level: LOG_WARNING,
702+
message: "Denied $client_ip access to $this->url as interface IP $server_ip is not allowed to respond.",
703+
);
700704
throw new ForbiddenError(
701705
message: 'The requested action is not allowed by admin policy',
702706
response_id: 'ENDPOINT_INTERFACE_NOT_ALLOWED',

0 commit comments

Comments
 (0)