Centralising logs with rsyslog
I manage quite a number of servers at this point, and something that's been on my mind for a while now is centralising all the log files generated by them. By this, specifically I mean that I want to automatically gather all logs generated by all the systems I manage into a single place in real time.
While there are enterprise-grade log management setups such as the ELK stack (elasticsearch, logstash, and kibana), as far as I'm aware they are all quite heavy and given my infrastructure is Raspberry Pi based (seriously, they use hardly any electricity at all compared to a regular desktop PC), with such a setup I would likely need multiple Pis to run it.
With this in mind, I'm opting for a different kind of log management system, which I'm basing on rsyslog
(which is installed by default in most Linux distros) and lnav (which I've blogged about before: lnav basics tutorial), which runs much lighter, requiring only a fraction of a Raspberry Pi to operate, which is good since the Raspberry Pi I've dedicated to monitoring the rest of the infrastructure currently also handles:
- Continuous Integration: Laminar (this will eventually be a Docker container on my Hashicorp Nomad cluster)
- Collectd (Collectd is really easy to setup and runs so light, I love it)
I'm sure you might be asking yourself what the purpose of this is. My reasoning is fourfold:
- Having all the logs in one place makes them easier to analyse all at once, without having to SSH into many different servers
- If a box goes down, then I can read the logs from it before start attempting to fix it, giving me a heads up as to what the problem is (this, in conjunction with my collectd monitoring system)
- On the Raspberry Pis I manage, this prolongs the life of the microSD cards by reducing the number of writes thereto
- I gain a little bit of security, in that if a box is compromised, then unless the attacker also gains access to my logging server, then they can't erase their tracks as easily as might otherwise have done
With all this in mind, I thought that it's about time I actually did something about this. I've found that while the solution is actually really quite simple, it's not particularly easy to find, so I thought I'd post about it here.
In my setup, I'm going to be using a Raspberry Pi 4 4GB RAM I've dubbed eldarion, which is the successor to an earlier Raspberry Pi 3B+ that died some years prior I called elessar
as the server upon which I centralise my logs. It has a 120GB SATA SSD attached in a case that used to house a WD PiDrive (they don't sell those anymore :-/) that I had lying around, which I've formatted with Btrfs.
Before we begin, let's outline the setup we're aiming for with a diagram to avoid confusion:
eldarion
will host the rsyslog server (which is essentially just a reconfiguration of the existing rsyslog server it is most likely already running), while other servers connect using the syslog protocol via a TCP connection, which is encrypted with TLS, using the GnuTLS engine (the default built into rsyslog). TLS here is important, since logs are naturally rather sensitive as I'm sure you can imagine.
To follow along here, you will need a valid Let's Encrypt certificate. It just so happens that I have a web server hosting my collectd graph panel interface, so I'm using that.
Of course, rsyslog can be configured in arbitrarily complex ways (such as having clients send logs to servers that they themselves forward to yet other servers), but at least for now I'm keeping it (relatively) simple.
Preparing the server
To start this process, we want to ensure the logs for the local system are stored in the right place. In my case, I have my SSD mounted to /mnt/eldarion-data2
, so I want to put my logs in /mnt/eldarion-data2/syslog/localhost
. There are 2 ways of accomplishing this:
- Reconfigure rsyslog to save logs elsewhere
- Be lazy, and bind mount the target location to
/var/log
Since I'm feeling lazy today, I'm going to go with option 2 here. It's also a good idea if a program is badly written and decides it's a brilliant idea to write logs directly to /var/log
itself instead of going through syslog.
If you're using DietPi, before you continue, do sudo dietpi-software
and remove the existing logging system.
A bind mount is like a hard link of a directory, in that it makes a directory appear in multiple places at once. It acts as a separate "filesystem" though I assume to allow for avoiding infinite loops. They are also the tech behind volumes in Docker's backend containerd
.
Open /etc/fstab
for editing, and something like this on a new line:
/mnt/eldarion-data2/syslog/localhost /var/log none auto,defaults,bind 0 0
..where /mnt/eldarion-data2/syslog/localhost
is the location we want the data to be stored, and /var/log
is the location we want to bind mount it to. Save and close /etc/fstab
, and then mount the bind mount like so. Make sure /var/log
is empty before mounting!
sudo mount /var/log
Next, we need to install some dependencies:
sudo apt install rsyslog rsyslog-gnutls
For some strange reason, TLS support is in a separate package on Debian-based systems. You'll need to investigate package names and translate this command for your distribution, of course.
Configuring the server
Now we have that taken care of, we can actually configure our server. Open /etc/rsyslog.conf
for editing, and at the top put this:
# The $Thing syntax is apparently 'legacy', but I can't find how else we're supposed to do this
$DefaultNetstreamDriver gtls
$DefaultNetstreamDriverCAFile /etc/letsencrypt/live/mooncarrot.space/chain.pem
$DefaultNetstreamDriverCertFile /etc/letsencrypt/live/mooncarrot.space/cert.pem
$DefaultNetstreamDriverKeyFile /etc/letsencrypt/live/mooncarrot.space/privkey.pem
# StreamDriver.Mode=1 means TLS-only mode
module(load="imtcp" MaxSessions="500" StreamDriver.Mode="1" StreamDriver.AuthMode="anon")
input(type="imtcp" port="514")
$template remote-incoming-logs,"/mnt/eldarion-data2/syslog/hosts/%HOSTNAME%/%PROGRAMNAME%.log"
*.* ?remote-incoming-logs
You'll need to edit these bits to match your own setup:
/etc/letsencrypt/live/mooncarrot.space/
: Path to the live
directory there that contains the symlinks to the certs your Let's Encrypt client obtained for you
/mnt/eldarion-data2/syslog/hosts
: The path to the directory we want to store the logs in
Save and close this, and then restart your server like so:
sudo systemctl restart rsyslog.service
Then, check to see if there were any errors:
sudo systemctl status rsyslog.service
Lastly, I recommend assigning a DNS subdomain to the server hosting the logs, such as logs.mooncarrot.space
in my case. A single server can have multiple domain names of course, and this just makes it convenient if we every move the rsyslog server elsewhere - as we won't have to go around and edit like a dozen config files (which would be very annoying and tedious).
Configuring a client
Now that we have our rsyslog server setup, it should be relatively straightforward to configure a client box to send logs there. This is a 3 step process:
- Configure the existing
/var/log
to be an in-memory tmpfs
to avoid any potential writes to disk
- Add a cron script to wipe
/var/log
every hour to avoid it getting full by accident
- Reconfigure (and install, if necessary) rsyslog to send logs to our shiny new server rather than save them to disk
If you haven't already confgiured /var/log
to be an in-memory tmpfs
, it is relatively simple. If you're unsure whether it is or not, do df -h
.
First, open /etc/fstab
for editing, and add the following line somewhere:
tmpfs /var/log tmpfs size=50M,noatime,lazytime,nodev,nosuid,noexec,mode=1777
Then, save + close it, and mount /var/log
. Again, make sure /var/log
is empty before mounting! Weird things happen if you don't.
sudo mount /var/log
Secondly, save the following to /etc/cron.hourly/clear-logs
:
#!/usr/bin/env bash
rm -rf /var/log/*
Then, mark it executable:
sudo chmod +x /etc/cron.hourly/clear-logs
Lastly, we can reconfigure rsyslog. The specifics of how you do this varies depending on what you want to achieve, but for a host where I want to send all the logs to the rsyslog server and avoid saving them to the local in-memory tmpfs
at all, I have a config file like this:
#################
#### MODULES ####
#################
module(load="imuxsock") # provides support for local system logging
module(load="imklog") # provides kernel logging support
#module(load="immark") # provides --MARK-- message capability
###########################
#### GLOBAL DIRECTIVES ####
###########################
$IncludeConfig /etc/rsyslog.d/*.conf
# Where to place spool and state files
$WorkDirectory /var/spool/rsyslog
###############
#### RULES ####
###############
$DefaultNetstreamDriverCAFile /etc/ssl/isrg-root-x1-cross-signed.pem
$DefaultNetstreamDriver gtls
$ActionSendStreamDriverMode 1 # Require TLS
$ActionSendStreamDriverAuthMode anon
*.* @@(o)logs.mooncarrot.space:514 # Forward everything to our rsyslog server
#
# Emergencies are sent to everybody logged in.
#
*.emerg :omusrmsg:*
The rsyslog config file in question this needs to be saved to is located at /etc/rsyslog.conf
. In this case, I replace the entire config file with the above, but you can pick and choose (e.g. on some hosts I want to save to the local disk and and to the rsyslog server).
Un the above you'll need to change the logs.mooncarrot.space
bit - this should be the (sub)domain that you pointed at your rsyslog server earlier. The number after the colon (514
) is the port number. The *.*
tells it to send everything to the remote rsyslog server.
Before we're done here, we need to provide the rsyslog client with the CA certificate of the server (because, apparently, it isn't capable of ferreting around in /etc/ssl/certs
like everyone else is). Since I'm using Let's Encrypt here, I downloaded their root certificate like this and it seemed to do the job:
sudo curl -sSL https://letsencrypt.org/certs/isrg-root-x1-cross-signed.pem -o /etc/ssl/isrg-root-x1-cross-signed.pem
Of course, one could generate their own CA and do mutual authentication for added security, but that's complicated, lots of effort, and probably unnecessary for my purposes as far as I can tell. I'll leave a link in the sources and further reading on how to do this if you're interested.
If you have a different setup, it's the $DefaultNetstreamDriverCAFile
in the above you need to change to point at your actual CA certificate.
With that all configured, we can now restart the rsyslog client:
sudo systemctl restart rsyslog.service
...and, of course, check to see if there were any errors:
sudo systemctl status rsyslog.service
Finally, we also need to configure logrotate to rotate all these new log files. First, install logrotate if the logrotate
command doesn't exist:
sudo apt install logrotate
Then, place the following in the file /etc/logrotate.d/centralisedlogging
:
/mnt/eldarion-data2/syslog/hosts/*/*.log {
rotate 12
weekly
missingok
notifempty
compress
delaycompress
}
Of course, you'll want to replace /mnt/eldarion-data2/syslog/hosts/
with the directory you're storing the logs from the remote server in, and also customise the log rotation. For example, the 12
there is the number of old log files to keep, and weekly
can be swapped for daily
or even monthly
if you like.
Conclusion
This has been a very quick whistle-stop tour of setting up an rsyslog server to centralise your logs. We've setup our rsyslog server to use a TLS encrypted connection to receive logs, which 1 or more clients can send logs to. We've also configured /var/log
on both the server and the client to avoid awkward issues.
Moving forwards, I recommend reading my lnav basics tutorial blog post, which should be rather helpful in analysing the resulting log files.
lnav
was not helpful however when I asked it to look at all the log files separately with sudo lnav */*.log
, deciding to treat them as "generic logs" rather than "syslog logs", meaning that it didn't colour them properly, and also didn't allow for proper filter. To this end, it may be benefical to store all the logs in 1 file rather than in separate files. I'll keep an eye on this, and update this post if figure out how to convince lnav
to treat them properly.
Another slightly snag with my approach here is that for some reason all the logs from elsewhere also end up in the generic /var/log/syslog
file (hence how I found a 'workaround' the above issue), resulting in duplicated logs. I have yet to find a solution to this issue, but I'm also not sure whether I want to keep the logs in 1 big file or in many smaller files yet.
These issues aside, I'm pretty satisfied with the results. Together with my existing collectd-based monitoring system (which I'll blog about how I've set that up if there's any interest - collectd is really easy to use), this is another step towards greater transparency into the infrastructure I manage.
In the future, I want to investigate generating notifications alerts for issues in my infrastructure. These could come either from collectd, or from rsyslog, and I envision them going to a variety of places:
- Email (a daily digest perhaps?)
- XMPP (I've bridged to it from shell scripts before)
Given that my infrastructure is just something I run at home and I don't mind so much if it's down for a few hours, my focus here is not on notifying my as soon as possible, but notifying myself in a way that doesn't disturb me so I can check into it in my own time.
If you found this tutorial / guide useful, please do comment below! It's really cool and motivating to see that the stuff I post on here helps others out.
Sources and further reading