Remote syslog¶
A small syslog-ng receiver on control-host keeps the
firewall's logs for 90 days.
Why: OPNsense only keeps logs on its own disk, and not for long: 31 days of
system/audit logs, and a firewall (filter) log that, at default verbosity, only covered
about an hour before it rotated. So "what talked to what last night" couldn't be answered,
and when the firewall cold-booted it took its evidence with it. Shipping logs to a server
fixes both: the files sit in /mnt/appdata, which the nightly borgmatic run already
carries off-host.
A full Loki/Grafana stack would be overkill for one source. syslog-ng in a container plus a one-line pruner is about 300 MB of RAM limit in total.
Compose¶
services:
syslog-ng:
image: balabit/syslog-ng:4.12.0
container_name: syslog-ng
# --no-caps: syslog-ng otherwise tries to keep CAP_SYSLOG/CAP_NET_BIND_SERVICE, which cap_drop removes.
# The container listens on 5514/5515 (no privileged port needed); the host publishes 514 on one IP only.
command: ["-F", "--no-caps", "-f", "/etc/syslog-ng/remote.cfg"]
ports:
- "<YOUR_SERVER_IP>:514:5514/udp"
- "<YOUR_SERVER_IP>:514:5514/tcp"
- "<YOUR_SERVER_IP>:5515:5515/udp"
volumes:
- ./syslog-ng.cfg:/etc/syslog-ng/remote.cfg:ro
- /mnt/appdata/syslog:/var/log/remote
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
mem_limit: 256m
restart: unless-stopped
# syslog-ng-ctl talks to the daemon's control socket: fails if syslog-ng is wedged
healthcheck:
test: ["CMD", "syslog-ng-ctl", "healthcheck"]
interval: 60s
timeout: 10s
retries: 3
prune:
# Deletes day directories older than 90 days, once a day. busybox find/sleep only.
image: busybox:1.38.0
container_name: syslog-prune
command: ["sh", "-c", "while true; do find /var/log/remote -mindepth 2 -maxdepth 2 -type d -mtime +90 -exec rm -rf {} +; sleep 86400; done"]
volumes:
- /mnt/appdata/syslog:/var/log/remote
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
mem_limit: 32m
restart: unless-stopped
healthcheck:
test: ["CMD", "test", "-d", "/var/log/remote"]
interval: 1h
No .env needed.
syslog-ng config¶
@version: 4.12
# Only the firewall's address is accepted; everything else is dropped.
# Files: /var/log/remote/<host>/<YYYY-MM-DD>/<program>.log, pruned after 90 days by the `prune` service.
options { chain_hostnames(no); keep_hostname(yes); use_dns(no); create_dirs(yes); dir_perm(0750); perm(0640); stats(freq(0)); };
source s_net {
network(transport("udp") port(5514) flags(syslog-protocol));
network(transport("tcp") port(5514) flags(syslog-protocol) max-connections(10));
network(transport("udp") port(5515)); # BSD/RFC3164 fallback
};
filter f_opnsense { netmask("192.0.2.1/32"); }; # your firewall's IP
destination d_files { file("/var/log/remote/opnsense/${R_YEAR}-${R_MONTH}-${R_DAY}/${PROGRAM:-unknown}.log"
template("${R_ISODATE} ${HOST} ${PROGRAM}[${PID}]: ${MESSAGE}\n")); };
log { source(s_net); filter(f_opnsense); destination(d_files); flags(final); };
Why it's built like this¶
- One day directory per day, one file per program. Retention is then just "delete
directories older than N days". No logrotate, no signals to the daemon.
-mindepth 2 -maxdepth 2only matches the<host>/<date>level, so it can never delete the host directory itself. - Filter by source address (
netmask), so only the firewall can write here, and publish the port on one host IP, not0.0.0.0. Syslog over UDP has no authentication, so the network is the access control. Add a firewall rule so only the router can reach514on that host too. - High port inside, 514 outside. The container listens on
5514, so it needs no capabilities at all (cap_drop: [ALL]+--no-caps); Docker does the 514 → 5514 mapping. R_macros name the files by received time (the container's clock, UTC by default), so a firewall with a wrong clock can't scatter logs into odd dates.flags(syslog-protocol)on 5514 parses RFC 5424, which keeps the program name and structured fields intact. The plain 5515 listener is there in case the sender is set to BSD/RFC 3164 format.
OPNsense side¶
System → Settings → Logging → Remote, add a destination:
| Field | Value |
|---|---|
| Transport | UDP(4) |
| Applications | all, or pick: audit, system, filter, gateways, DHCP, configd |
| Levels | info and above |
| Hostname | <YOUR_SERVER_IP> |
| Port | 514 |
| RFC5424 | ticked |
Logs then appear as /mnt/appdata/syslog/opnsense/<date>/filterlog.log, audit.log,
configd.py.log and so on, about 30 MB a day here. A program name containing / creates
a subdirectory, which is only cosmetic.
Cut the noise at the source
Most of the firewall log was pass rules nobody reads: "log packets matched by the default pass rules" (Firewall → Settings → Advanced) and logging on the DNS-redirect port forwards. Turn those off and keep logging on block rules, which are the part that matters when you investigate. The same 90 days of disk then hold a lot more useful history.
The firewall health poller (see OPNsense maintenance) reads these files too, for example to alert when CrowdSec bans an internal address.
Reading the logs¶
cd /mnt/appdata/syslog/opnsense/$(date -u +%F)
tail -f filterlog.log
grep -c ',block,' filterlog.log # blocked packets today
grep -rl 'admin' /mnt/appdata/syslog/opnsense/*/audit.log # days with GUI/admin activity