README
ΒΆ
AuthPF-API
A RESTful HTTP API for pf user rules.
Overview
AuthPF-API is a Go-based REST API that provides a secure interface for managing pf user rules on FreeBSD and OpenBSD systems. It allows users to activate and deactivate pf rules through HTTP endpoints with JWT token authentication and fine-grained permission control. The original authpf is "a user shell for authenticating gateways" -- openbsd.org and is based on SSH logins. AuthPF-API is an alternative by using HTTP/S to load/unload pf user rules.
Features
- π JWT Authentication - Secure token-based authentication
- π₯ Role-Based Access Control (RBAC) - Fine-grained permission management
- β±οΈ Automatic Expiration - Rules automatically expire after configured timeout
- π Scheduled Cleanup - Periodic cleanup of expired rules
- ποΈ Cross-Platform Build - Support for FreeBSD and OpenBSD on multiple architectures
- π§βπΌ Runs as User - Runs as Non-Root User - Elevator tool (sudo/doas) support for running pfctl commands
- ποΈ PF Macros - User defined pf macro support
- π PF Table Support - Automatically track active user IPs in a pf table (global or per-user)
Supported Platforms
Operating Systems
- FreeBSD
- OpenBSD
- macOS
- Other pf based OS with anchor support
Architectures
- amd64 (x86-64)
- arm64 (ARM 64-bit)
Requirements
- Go 1.24 or higher
- pfctl binary (usually pre-installed on BSD systems)
- Optional: sudo or doas for privilege escalation
Installation
From Source
# Clone the repository
git clone https://github.com/scd-systems/authpf-api.git
cd authpf-api
# Build for current system
make build
# Or build for all platforms
make build-all
# Run the application
./build/authpf-api-freebsd-amd64 -foreground
Using Make
# Show available targets
make help
# Build for specific platform
make build GOOS=freebsd GOARCH=amd64
# Run tests
make test
# Generate coverage report
make coverage-html
Configuration
The application is configured via a YAML configuration file. By default, it uses /usr/local/etc/authpf-api-config.yaml.
Configuration Parameters
Defaults Section
| Parameter | Description | Required |
|---|---|---|
defaults.pfctlBinary |
Path to the pfctl binary executable (e.g., /sbin/pfctl). Must be accessible by the user running the API. | Yes |
AuthPF Section
| Parameter | Description | Required |
|---|---|---|
authpf.timeout |
Set the maximum timeout for authpf anchors (e.g., 30m, 1h). Defines how long the pf rules will be active until the scheduler removes it again. | Yes |
authpf.userRulesRootfolder |
Root directory where user-specific rule files are stored (e.g., /etc/authpf/users). Each user gets a subdirectory here. | Yes |
authpf.userRulesFile |
Filename for user rules within the userRulesRootfolder (e.g., authpf.rules). This file is loaded when a user activates their anchors. | Yes |
authpf.anchorName |
Name of the PF anchor to use for rule management (e.g., authpf). Used to organize and manage rules within the packet filter. | Yes |
authpf.flushFilter |
List of flush targets for pfctl command (nat, queue, ethernet, rules, info, Sources, Reset). Specifies which rule types to clear when flushing. | Yes |
authpf.onStartup |
Specifies the startup anchor loading. Possible Values are (import, importflush). Import just loads existing anchors. Value importflush clears the anchors after importing them from pf. Default none |
No |
authpf.onShutdown |
Remove all activated user rules when the api server shuts down. Default none |
No |
authpf.pfTable |
Name of a global pf table to track active user IPs. When set, the user's IP is added to this table on anchor activation and removed on deactivation. The table must exist in pf.conf before starting authpf-api. Leave empty to disable. |
No |
Server Section
| Parameter | Description | Required |
|---|---|---|
server.bind |
IP address to bind the server to (e.g., 127.0.0.1 for localhost only). Use 0.0.0.0 to listen on all interfaces. | Yes |
server.port |
Port number for the HTTP/HTTPS server (e.g., 8080). Ensure the port is not already in use and firewall allows access. | Yes |
server.ssl.certificate |
Path to SSL certificate file (leave empty to disable SSL). Required for HTTPS connections. | No |
server.ssl.key |
Path to SSL private key file (e.g., key.pem). Must match the certificate and be readable by the server process. | No |
server.jwtSecret |
JWT secret key for token signing - MUST be changed before production deployment. Use a strong, random value for security. If not set, a random secret will be generated. | No |
server.jwtTokenTimeout |
JWT token timeout in hours (default: 8 hours if not set). Determines how long authentication tokens remain valid. | No |
server.elevatorMode |
Elevator mode for privilege escalation (none, sudo, or doas). Required when running server as non-root user (recommended). | No |
server.logfile |
Path to the server logfile (e.g., /var/log/authpf-api.log). Ensure the directory exists and is writable by the server process. | Yes |
RBAC Section
| Parameter | Description |
|---|---|
rbac.roles.<name>.permissions |
List of permission strings assigned to this role (e.g., activate_own_rules, view_other_rules). See RBAC - Permissions for all available values. |
| Parameter | Description | Required |
|---|---|---|
rbac.users.<name>.password |
Bcrypt password hash for this user. Generate with ./authpf-api -gen-user-password. |
Yes |
rbac.users.<name>.role |
Role assigned to this user. Must match a key defined under rbac.roles. |
Yes |
rbac.users.<name>.userId |
Numeric user ID used as the pf anchor suffix (e.g., authpf/username(1001)). Defaults to 0 if not set. |
No |
rbac.users.<name>.userIp |
Pin a static IP address passed as the user_ip macro to pfctl. If omitted, the remote client IP of the HTTP request is used. |
No |
rbac.users.<name>.userRulesFile |
Override the global authpf.userRulesFile for this specific user. |
No |
rbac.users.<name>.macros |
Map of arbitrary key/value pairs passed as additional -D key=value arguments to pfctl. Must also be declared in the user's pf rules file. |
No |
rbac.users.<name>.pfTable |
Override the global authpf.pfTable for this specific user. If set, this user's IP is tracked in this table instead of the global one. The table must exist in pf.conf. |
No |
Environment Variables
# Configuration file path
export CONFIG_FILE=/path/to/config.yaml
# Log level (debug, info, warn, error)
export LOG_LEVEL=info
Command-Line Flags
# Show version and exit
./authpf-api -version
# Log to stdout instead of logfile
./authpf-api -foreground
# Generate User Password (bcrypted)
./authpf-api -gen-user-password
API Endpoints
Authentication
Login
POST /login
Content-Type: application/json
{
"username": "authpf-user1",
"password": "SHA256"
}
Response (200 OK):
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
AuthPF Rules
All endpoints require JWT authentication via Authorization: Bearer <token> header.
Activate Rule
POST /api/v1/authpf/activate?timeout=30m
Authorization: Bearer <token>
Content-Type: application/json
Response (201 Created):
{
"status": "activated",
"user": "authpf-user1",
"msg": "authpf anchor is being loaded"
}
Deactivate Rule
DELETE /api/v1/authpf/activate
Authorization: Bearer <token>
Content-Type: application/json
{}
Response (202 Accepted):
{
"status": "queued",
"user": "authpf-user1",
"msg": "authpf anchor is being unloaded"
}
Get Status of all activated rules
GET /api/v1/authpf/all
Authorization: Bearer <token>
Response (200 OK):
{
"authpf-user1": {
"username": "authpf-user1",
"timeout": "30m",
"client_ip": "192.168.1.100",
"expireat": "2026-01-07T22:00:00Z"
}
}
Delete All Rules (Admin Only)
DELETE /api/v1/authpf/all
Authorization: Bearer <token>
Response (200 OK):
{
"status": "cleared"
}
Query Parameters:
| Parameter | Description |
|---|---|
authpf_username |
Activate/Deactivate the authpf rules from another user (requires activate_other_rules/deactivate_other_rules permissions in role) |
timeout |
Modify the authpf expire timeout (default 30m) |
Example:
POST /api/v1/authpf/activate?authpf_username=othername
Authorization: Bearer <token>
RBAC - Permissions
Available Permissions
| Permission | Description |
|---|---|
activate_own_rules |
Allow user to activate authpf rules |
activate_other_rules |
Allow user to activate rules from other users |
deactivate_own_rules |
Allow user to deactivate their own rules |
deactivate_other_rules |
Allow user to deactivate rules from other users |
view_own_rules |
Allow user to view their own rules status |
view_other_rules |
Allow user to view rules status from other users |
User IP and Macro Settings
User IP (userIp)
By default, authpf-api uses the remote client IP address of the HTTP request as the user_ip macro value passed to pfctl when loading a user's anchor rules. This allows pf rules to dynamically reference the connecting client's IP.
For scenarios where the client IP is dynamic, behind a NAT, or should be overridden for security reasons, you can pin a static IP per user via the userIp field in the RBAC user configuration:
rbac:
users:
authpf-user1:
role: user
userId: 1001
userIp: 192.168.0.10 # fixed IP β client IP from the request is ignored
When userIp is set, the configured value is always passed as -D user_ip=<value> to pfctl, regardless of the actual source IP of the HTTP request. If userIp is omitted, the real client IP is used automatically.
Note: During anchor import on startup (
onStartup: import/importflush), theuser_ipcannot be recovered from pf and is set toNaN/imported. This has no functional impact β deactivation works without a valid IP.
Macro Settings (macros)
In addition to the built-in user_ip and user_id macros, authpf-api supports arbitrary user-defined macros per user. These are passed as additional -D key=value arguments to pfctl when the user's anchor is loaded, making them available inside the user's pf rules file.
rbac:
users:
authpf-user1:
role: user
userId: 1001
macros:
server_1_port: 22
server_1_addr: 10.127.2.1
The above configuration results in the following pfctl invocation (simplified):
pfctl -a authpf/authpf-user1(1001) \
-D user_ip=<client_ip> \
-D user_id=1001 \
-D server_1_port=22 \
-D server_1_addr=10.127.2.1 \
-f /etc/authpf/users/authpf-user1/authpf.rules
The macros must also be declared and used inside the corresponding authpf.rules file of the user:
server_1_port = $server_1_port
server_1_addr = $server_1_addr
pass in proto tcp from $user_ip to $server_1_addr port $server_1_port
Important: When using
elevatorMode: sudo, the sudoersCmnd_Aliasmust be updated to allow the additional-Darguments. Use a broad regex pattern to cover all macro key/value pairs:/sbin/pfctl ^-a authpf/([a-zA-Z0-9_-]+)(\([0-9]+\))? -D [a-zA-Z0-9_-]+=[a-zA-Z0-9_.+-]+ -f /etc/authpf/users/[a-zA-Z0-9_-]+/authpf.rules$
PF Table Support
authpf-api can automatically track active user IPs in a pf table β similar to the original authpf(8) behaviour. This allows pf rules to reference the set of currently authenticated users by table name.
How it works
- When a user's anchor is activated, their
user_ipis added to the configured pf table viapfctl -t <table> -T add <ip>. - When a user's anchor is deactivated, their IP is removed via
pfctl -t <table> -T delete <ip>. - On startup, authpf-api verifies that all configured tables exist.
- A global table applies to all users. A per-user table overrides the global one for that specific user.
Priority
user.pfTable (if set) β authpf.pfTable (global, if set) β no table management
Configuration
Global table (applies to all users without a per-user override):
authpf:
pfTable: authpf_users
Per-user table (overrides the global table for this user):
rbac:
users:
authpf-user1:
role: user
userId: 1001
pfTable: custom_table_user1 # overrides authpf.pfTable
Required pf.conf entry
The table must be declared as persist in /etc/pf.conf before starting authpf-api:
table <authpf_users> persist
Without persist, pf will remove the table when it becomes empty, causing subsequent pfctl -T add calls to fail.
Error behaviour
| Operation | On failure |
|---|---|
| Add IP on activate | Fatal β HTTP 500 returned, anchor is not activated |
| Remove IP on deactivate | Warn only β logged, anchor is still flushed |
| Remove IP on shutdown / deactivate-all | Warn only β logged, all anchors are still flushed |
| Table existence check on startup | Fatal β server refuses to start |
Elevator (sudo) β additional rules
When using elevatorMode: sudo, the sudoers Cmnd_Alias must include the table management commands:
/sbin/pfctl ^-t [a-zA-Z0-9_]+ -T add [0-9a-zA-Z:.]+$, \
/sbin/pfctl ^-t [a-zA-Z0-9_]+ -T delete [0-9a-zA-Z:.]+$, \
/sbin/pfctl ^-t [a-zA-Z0-9_]+ -T show$
See the full updated sudo example in the Elevator Setup section below.
Setup PF
Before you can use authpf-api, please verify that the authpf anchors are set up in pf.conf
/etc/pf.conf
nat-anchor "authpf/*"
rdr-anchor "authpf/*"
binat-anchor "authpf/*"
anchor "authpf/*"
Verify also that the anchor name is the same as in the config file (authpf.anchorName: authpf)
Setup SSL for AuthPF-API
Create a Self-Signed CA Root and SSL Certificate
Create CA Root Key
openssl ecparam -genkey -name secp384r1 | openssl ec -aes256 -out rootCA.key
Self-Sign CA Root
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.crt
Create a Server Certificate and sign with the CA Root certificate
openssl ecparam -genkey -name prime256v1 -noout -out mydomain.com.key
openssl req -new -key mydomain.com.key -out mydomain.com.csr
cat > ./mydomain.com.ext << _EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = mydomain.com
DNS.2 = www.mydomain.com
_EOF
openssl x509 -req -in mydomain.com.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out mydomain.com.crt -days 397 -sha256 -extfile mydomain.com.ext
Configure authpf-api.conf
server:
ssl:
certificate: mydomain.com.crt
key: mydomain.com.key
Restart authpf-api.
Copy the rootCA.crt to the clients to verify the server.
Elevator Setup
When running authpf-api as non-root user, an elevator setup is required. AuthPF-API currently supports sudo and doas.
Configure Sudo
Sudoers File:
Cmnd_Alias AUTHPF_API_COMMANDS = /sbin/pfctl -sA, \
/sbin/pfctl ^-a authpf/([a-zA-Z0-9_-]+)(\([0-9]+\))? -D user_ip=[0-9.]+ -D user_id=[0-9]+ -f /etc/authpf/users/[a-zA-Z0-9_-]+/authpf.rules$, \
/sbin/pfctl ^-a authpf/([a-zA-Z0-9_-]+)(\([0-9]+\))? -F nat$, \
/sbin/pfctl ^-a authpf/([a-zA-Z0-9_-]+)(\([0-9]+\))? -F rules$, \
/sbin/pfctl ^-a authpf/([a-zA-Z0-9_-]+)(\([0-9]+\))? -F queue$, \
/sbin/pfctl ^-a authpf/([a-zA-Z0-9_-]+)(\([0-9]+\))? -F states$, \
/sbin/pfctl ^-a authpf/([a-zA-Z0-9_-]+)(\([0-9]+\))? -F Tables$, \
/sbin/pfctl ^-a authpf/([a-zA-Z0-9_-]+)(\([0-9]+\))? -F all$, \
/sbin/pfctl ^-t [a-zA-Z0-9_]+ -T add [0-9a-fA-F:.]+$, \
/sbin/pfctl ^-t [a-zA-Z0-9_]+ -T delete [0-9a-fA-F:.]+$, \
/sbin/pfctl ^-t [a-zA-Z0-9_]+ -T show$
%_authpf-api ALL=(root) NOPASSWD: AUTHPF_API_COMMANDS
Note: The three pf table rules are only required when
authpf.pfTableor anyrbac.users.<name>.pfTableis configured. They cover IPv4 and IPv6 addresses for add/delete, and the startup existence check for show.
Configure authpf-api.conf
server:
elevatorMode: sudo
Configure Doas
doas.conf:
permit nopass :authpf as root cmd /sbin/pfctl
Configure authpf-api.conf
server:
elevatorMode: doas
Doas Security Considerations
-
The doas setup is similar to sudo, but with some restrictions. Doas does not support regular expressions for command arguments yet. A solution can be to use the pfctl_wrapper (found under scripts/).
-
Why not use flush all (pfctl -a "authpf/user" -Fa) and run each filter flushing separately? The flush all under FreeBSD 15.0 RELEASE results in an error (pfctl: Operation not supported by device) with ExitCode 1. It's related to a NETLINK change: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=291981
Import existing Anchors
Authpf-api is able to import existing authpf anchors. Three modes are configurable:
- none
- import
- importflush
If no import is required, set to none or empty.
The "import" mode will parse the output of pfctl -sA and import all existing anchors (authpf/NAME(USERID)).
Mode "importflush" will do the same as import, but will remove/flush the anchors directly after importing.
Configure import
authpf:
onStartup: import # valid modes: none, importflush
Limitations
- It is currently not possible to detect the UserIP macro value. The UserIP will be set to "NaN/imported". It does not have any effect. Deactivation works without IP Address.
- The Expire datetime will be calculated by the
authpf.timeoutand current server time.
Development
Running Tests
# Run all tests
make test
# Run tests with verbose output
make test-verbose
# Generate coverage report
make coverage
# Generate HTML coverage report
make coverage-html
Code Quality
# Format code
make fmt
# Run linter
make lint
# Run go vet
make vet
Building
# Build for current system
make build
# Build for FreeBSD
make build-freebsd
# Build for OpenBSD
make build-openbsd
# Build for all platforms
make build-all
Logging
The application uses structured logging with zerolog. Logs can be output to:
- Logfile (default) - Configured in
server.logfile - Stdout - Use
-foregroundflag
User Password Generation
The authpf-api uses bcrypt and sha256 in combination for password hashing.
Use the -gen-user-password flag to generate a bcrypt hash for a new user password.
In the first step, the password input by -gen-user-password will be hashed by sha256 and afterwards hashed again with bcrypt.
The authentication against the API is only accepting the user password as a sha256 hash.
Interactive Mode
Generate a password hash interactively (password input is hidden):
./authpf-api -gen-user-password
Enter password:
$2a$10$N9qo8uLOic.......
Piped Mode
Generate a password hash by piping the password:
echo -n "your-password" | ./authpf-api -gen-user-password
$2a$10$N9qo8uLOickgx2ZM........
Adding Users to Configuration
Copy the generated hash and add it to your configuration file (/usr/local/etc/authpf-api.conf):
rbac:
users:
authpf-userX:
password: $2a$10$abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQR
role: user
authpf-admin:
password: $2a$10$abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQR
role: admin
User Management with authpf-api-cli
For comprehensive user and rule management, use the authpf-api-cli tool. Refer to the authpf-api-cli documentation for detailed instructions on managing users, roles, and permissions.
Troubleshooting
Connection Refused
- Verify server is running:
ps aux | grep authpf-api - Check bind address and port in configuration
- Ensure firewall allows connections
Authentication Failed
- Verify username and password are correct
- Check user exists in configuration
- Verify password hash is correct
Rule Not Loading
- Check pfctl binary path in configuration
- Verify user has permission to run pfctl
- Check authpf rules file exists and is readable
- Review logs for detailed error messages
Permission Denied
- Verify user role has required permission
- Check RBAC configuration
- Review logs for permission errors
Contributing
Contributions are welcome!
Please ensure:
- Code follows Go conventions
- All tests pass:
make test - Code is formatted:
make fmt - Linter passes:
make lint - Commit messages are descriptive
License
See LICENSE file for details.
Support
For issues, questions, or suggestions, please open an issue in the repository.