# pfSense Custom Scripts and Scheduled Tasks

> Running commands in pfSense, Shellcmd and Cron packages, PHP scripts, rc.d startup scripts, devd rules, and automating routine administrative tasks

Source: https://opennix.org/en/docs/pfsense/development/pfsense-custom-scripts/


pfSense is built on FreeBSD and provides a full command-line environment, a PHP interpreter, and startup mechanisms. This allows the creation of custom scripts for tasks not covered by the web interface: automated remote backup, log rotation, monitoring checks, and certificate renewal. The Shellcmd and Cron packages are available for managing scripts and scheduled tasks.

## Executing Commands via the Web Interface

pfSense provides two tools for interacting with the system through a browser without SSH access.

### Diagnostics - Command Prompt

The **Diagnostics > Command Prompt** page allows execution of shell commands and PHP code directly from the web interface.

**Execute Shell Command** runs arbitrary FreeBSD commands:

```bash
# Check disk space
df -h

# View running processes
ps aux | grep openvpn

# Count established connections
netstat -an | grep ESTABLISHED | wc -l
```

**Execute PHP Command** runs PHP code with access to pfSense functions:

```php
require_once("config.inc");
require_once("interfaces.inc");

// Display hostname
echo $config['system']['hostname'] . "." . $config['system']['domain'];
```

### Diagnostics - Edit File

The **Diagnostics > Edit File** page provides a text editor for viewing and modifying files on the filesystem. Useful for editing configuration files without SSH access.

## Shellcmd Package - Boot Commands

The Shellcmd package allows defining commands that execute during pfSense boot. It replaces the need to manually edit config.xml to add `<shellcmd>` tags.

### Installation

Navigate to **System > Package Manager > Available Packages**, search for **Shellcmd**, and install it.

After installation, configuration is available under **Services > Shellcmd**.

### Command Types

| Type | Execution Timing | Usage |
|---|---|---|
| **shellcmd** | End of boot process | Primary startup tasks |
| **earlyshellcmd** | Start of boot process (before services) | Kernel module loading, filesystem mounting |
| **afterfilterchangeshellcmd** | After every firewall rule change | Dynamic rules, integrations |

### Configuration Example

```text
Command: /usr/local/bin/custom-startup.sh
Type: shellcmd
Description: Custom startup script
```

Commands can also be added directly to config.xml:

```xml
<shellcmd>/usr/local/bin/custom-startup.sh</shellcmd>
<earlyshellcmd>kldload if_tap</earlyshellcmd>
```

### Best Practices

- Use absolute paths to executables
- Scripts must have executable permissions (`chmod +x`)
- Run long-running operations in the background with `&`
- Redirect output to a log for debugging: `/usr/local/bin/script.sh > /var/log/custom-startup.log 2>&1 &`

## Cron Package - Scheduled Tasks

The Cron package provides a web interface for managing cron jobs. pfSense uses the standard FreeBSD cron, but the package adds graphical management under **Services > Cron**.

### Installation

Navigate to **System > Package Manager > Available Packages**, search for **Cron**, and install it.

### Schedule Format

```text
*    *    *    *    *    command
|    |    |    |    |
|    |    |    |    +--- Day of week (0-7, 0 and 7 = Sunday)
|    |    |    +-------- Month (1-12)
|    |    +------------- Day of month (1-31)
|    +------------------ Hour (0-23)
+----------------------- Minute (0-59)
```

### Task Examples

| Task | Schedule | Command |
|---|---|---|
| Daily config backup at 2:00 AM | `0 2 * * *` | `/usr/local/bin/backup-config.sh` |
| Restart OpenVPN every 6 hours | `0 */6 * * *` | `/usr/local/sbin/pfSsh.php playback svc restart openvpn` |
| Clean tmp every Sunday | `0 3 * * 0` | `/usr/bin/find /tmp -type f -mtime +7 -delete` |
| Check disk space every hour | `0 * * * *` | `/usr/local/bin/check-disk.sh` |

## Custom PHP Scripts

PHP scripts have full access to pfSense configuration and functions. The interpreter is located at `/usr/local/bin/php`.

### Script Structure

```php
#!/usr/local/bin/php
<?php
require_once("config.inc");
require_once("functions.inc");
require_once("filter.inc");

// Access configuration
$hostname = $config['system']['hostname'];
$interfaces = $config['interfaces'];

// Example: count active firewall rules
$rules = $config['filter']['rule'];
$active_rules = array_filter($rules, function($rule) {
    return !isset($rule['disabled']);
});

echo "Active firewall rules: " . count($active_rules) . "\n";
```

### Useful Include Files

| File | Purpose |
|---|---|
| `config.inc` | Load the `$config` array |
| `functions.inc` | General utility functions |
| `filter.inc` | Firewall rule management |
| `interfaces.inc` | Interface functions |
| `openvpn.inc` | OpenVPN management functions |
| `certs.inc` | Certificate functions |
| `notices.inc` | Notification system |

## rc.d Startup Scripts

Scripts placed in `/usr/local/etc/rc.d/` are automatically executed at system boot in FreeBSD rc.d style.

### Example rc.d Script

```sh
#!/bin/sh

# PROVIDE: custom_monitor
# REQUIRE: NETWORKING
# BEFORE: LOGIN

. /etc/rc.subr

name="custom_monitor"
rcvar="${name}_enable"
command="/usr/local/bin/custom-monitor.sh"
command_args="&"

load_rc_config $name
run_rc_command "$1"
```

To activate, add the following to `/etc/rc.conf.local`:

```sh
custom_monitor_enable="YES"
```

## devd Rules

devd (device daemon) responds to system events such as device connections and network interface changes. Custom devd rules are placed in `/etc/devd/`.

```text
notify 0 {
    match "system"    "IFNET";
    match "subsystem" "igb0";
    match "type"      "LINK_UP";
    action "/usr/local/bin/link-up-handler.sh";
};
```

After modifying rules, restart devd:

```bash
service devd restart
```

## Common Automation Tasks

### Automated Remote Backup

```sh
#!/bin/sh
# /usr/local/bin/backup-config.sh
# Back up config.xml to a remote server via SCP

REMOTE_USER="backup"
REMOTE_HOST="backup.example.com"
REMOTE_DIR="/backups/pfsense"
DATE=$(date +%Y%m%d-%H%M)

cp /cf/conf/config.xml /tmp/config-${DATE}.xml
scp -i /root/.ssh/backup_key /tmp/config-${DATE}.xml \
  ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/
rm /tmp/config-${DATE}.xml

logger -t backup "Configuration backed up to ${REMOTE_HOST}"
```

### Log Rotation

```sh
#!/bin/sh
# /usr/local/bin/rotate-custom-logs.sh
# Rotate custom log files

LOG_DIR="/var/log/custom"
MAX_FILES=7

for logfile in ${LOG_DIR}/*.log; do
    if [ -f "${logfile}" ]; then
        cp "${logfile}" "${logfile}.$(date +%Y%m%d)"
        : > "${logfile}"
    fi
done

# Remove old files
find ${LOG_DIR} -name "*.log.*" -mtime +${MAX_FILES} -delete
```

### Gateway Monitoring Check

```sh
#!/bin/sh
# /usr/local/bin/check-gateway.sh
# Check gateway reachability and send alert

GATEWAY="10.0.0.1"
ALERT_EMAIL="admin@example.com"

if ! ping -c 3 -W 5 ${GATEWAY} > /dev/null 2>&1; then
    echo "Gateway ${GATEWAY} unreachable at $(date)" | \
      mail -s "pfSense: Gateway Down" ${ALERT_EMAIL}
    logger -t gateway-check "Gateway ${GATEWAY} is unreachable"
fi
```

### Certificate Expiry Check Script

```php
#!/usr/local/bin/php
<?php
// /usr/local/bin/check-cert-expiry.php
// Check certificate expiration dates

require_once("config.inc");
require_once("certs.inc");

$warning_days = 30;

foreach ($config['cert'] as $cert) {
    $info = openssl_x509_parse(base64_decode($cert['crt']));
    if ($info === false) continue;

    $expires = $info['validTo_time_t'];
    $days_left = floor(($expires - time()) / 86400);

    if ($days_left < $warning_days) {
        $msg = "Certificate '{$cert['descr']}' expires in {$days_left} days";
        log_error($msg);
        // Notification can be added here
    }
}
```

## Warnings About Custom Modifications

Custom changes in pfSense carry specific risks. Failure to follow these guidelines may render the system inoperable after an upgrade.

| Risk | Description | Recommendation |
|---|---|---|
| Lost on upgrade | Files outside `/usr/local/` and `/root/` are overwritten during pfSense upgrades | Place scripts in `/usr/local/bin/` or `/root/` |
| API incompatibility | pfSense PHP functions may change between versions | Verify script operation after every upgrade |
| Security | Scripts run with root privileges | Minimize privileges, validate input |
| config.xml | Direct config.xml editing can break the structure | Use the PHP API (`write_config()`) instead of direct editing |
| Shellcmd/Cron packages | A failing shellcmd script can delay boot | Test scripts before adding them to startup |

Before every pfSense upgrade, create a full backup and maintain a list of all custom modifications.

## Related Sections

- [pfSense API and Automation](/docs/pfsense/development/pfsense-api-automation/) - REST API, Ansible, Terraform, and programmatic management via HTTP
- [pfSense Backup](/docs/pfsense/backup/pfsense-backup-recovery/) - built-in backup and restore tools for configuration management
- [pfSense Packages](/docs/pfsense/packages/) - installing and managing Shellcmd, Cron, and other extension packages

