JSON logging from pfSense Applications

5 minute read



I use pfSense as my home router and firewall with the pfBlockerNG package to eliminate ads and trackers online. I love everything about it, except the reporting interface. It’s slow and clunky. I wanted to get the data into ClickHouse so I can create dashboards with Grafana. Unfortunately, pfBlockerNG only logs data to the local filesystem.

This post is for folks who want to export data in log files from pfSense to a central log server. We’ll cover how to do this for the pfBlockerNG DNSBL log, but it will work for any other service logs that don’t use syslog, like pfSense zeek.

Prequisites

Before we start, you’ll need to install and configure pfBlockerNG. If you are unfamiliar with it, here are a few good tutorials to help you install it:

You’ll need to install the syslog-ng package from the Packages section of the pfSense UI.

You will also need a central log server somewhere nearby to receive the logs.

Goals

The goal of this setup is to forward the pfBlockerNG DNSBL logs from the pfSense appliance to the central log server. The DNSBL logs live on the filesystem as /var/log/pfblockerng/dnsbl.log in CSV format. We’re going to forward them as JSON using the Common Event Expression CEE format. I use processors to extract that data and insert it into ClickHouse, but you can also use Elasticsearch or OpenSearch.

Enabling syslog-ng

Once you install syslog-ng, navigate to “Services” > “Syslog-ng”. In the “General” tab, tick the “Enable” checkbox and “Include SCL” checkbox, then click “Save.” The other options on the General tab don’t really matter for our use case.

Add the Source

Next we’ll need to add the pfb_dnsbl source to syslog-ng. The UI here isn’t intuitive even if you’re familiar with syslog-ng. Here’s a screen shot:

screenshot of the pfSense UI for adding a source

This configuration will generate the following syslog-ng snippet:

source pfb_dnsbl {
    file("/var/log/pfblockerng/dnsbl.log"
        default-facility(security)
        default-priority(notice)
        program-override("pfBlockerNG")
        flags(no-parse)
    );
};

There’s nothing too scary here, but a few important pieces:

Create a Parser

We’ll need to create our own CSV parser for the file. Again, the UI is terrible, but it looks like this:

screenshot of the pfSense UI for adding a parser

This configuration will generate the following syslog-ng snippet:

parser pfb_dnsbl {
    csv-parser(
        columns("service","time","dst","src_ip","call","action","class","sld","rule_id","hitormiss")
        delimiters(",")
        prefix(".SDATA.dnsbl.")
    );
};

Again, nothing too complicated here, but there’s a few notes:

Create a Destination

Next we’ll need a destination to send the logs. This destination is going to take the .SDATA. namespace, convert it to JSON and add a $MSGHDR to the front and forward it along. Here’s a screenshot:

screenshot of the pfSense UI for adding a parser
destination CentralJSON {
    network("10.0.1.9"
        port(514)
        transport("tcp")
        template("<${PRI}>${ISODATE} ${HOST} ${MSGHDR}$(format-json --scope sdata --rekey .* --shift-levels 2)\n")
        persist-name("central-logs-json")
    );
};

The hardest part of getting this configuration to work was sorting out the format-json parameters. The --scope parameter selects the variables that are available to JSONify. The docs were a little hard to find so that link is as much for me to be able to find it as it is for you! By specifying --scope sdata, only the .SDATA.* fields are available. If we stopped there, that part of the message would look like {".SDATA":{"dnsdbl":{...}}}. I wanted my messages to skip that .SDATA key, so I applied a --rekey .* --shift-levels 2. The rekey applies to all variables since I used .*. The --shift-levels 2 shifts the key space by 2 dots. So, .SDATA.dnsbl.* becomes dnsbl.*. The resulting messages have the {"dnsbl":{...}} format.

Putting It All Together

The last step is to combine these pieces into a log statement in syslog-ng. The pfSense UI automatically rebuilds the configuration and restarts the service when you click Save. Once you save, you should start to see the logs land on the destination server.

In the UI, the log set looks like this:

screenshot of the pfSense UI for adding log definition
log {
   source(pfb_dnsbl);
   parser(pfb_dnsbl);
   destination(CentralJSON);
};

That’s it! Now logs should be arriving in JSON format at our central log server. I setup a quick Grafana dashboard:

screenshot of the Grafana dashboard

There are a few issues with my current setup. I’m relying on the timestamp of the syslog message, so I get lots of spikes in the data. I could fix that by using the time field from the JSON itself, but this is a great start!

Comments

You can comment on this blog post by publicly replying to this post using a Mastodon or other ActivityPub/Fediverse account. Known non-private replies are displayed below.

Open Post