Apparmor and multiple instances of the same service

Posted on May 12, 2015

Getting started

Let’s say you want to have 3 instances of redis. One for each redis using app. Getting 3 instances app with systemd’s nifty Instantiated Services is easy.

[Unit]
Description=Redis
After=network.target
PartOf=redis.target

[Service]
Type=simple
User=redis
Group=redis
PrivateTmp=true
PIDFile=/var/run/redis/%i.pid
ExecStart=/usr/sbin/redis-server /etc/redis/%i.conf
Restart=on-failure

[Install]
WantedBy=multi-user.target redis.target

“systemctl start redis@discourse” will launch us an instance with “discourse.conf”. Other systemctl commands just work as fine. “systemctl enable redis@discourse” , “systemctl status redis@discourse” . Now to stop all redis instances? “systemctl stop redis.target”

The next stop on the todo will be the apparmor profile. The intuitive apparmor profile for redis might be something like:

#include <tunables/global>

/usr/sbin/redis-server flags=(attach_disconnected) {
  #include <abstractions/base>
  #include <abstractions/nameservice>

  network inet  stream,
  network inet6 stream,

  /etc/redis/*.conf r,

  /var/lib/redis/** rw,
  /var/log/redis/*.log rw,

  /{var/,}run/redis/* rwlk,

  owner /proc/*/maps r,
  owner /proc/*/smaps r,
  owner /proc/*/stat r,

  /proc/sys/net/core/somaxconn r,
  /proc/sys/vm/overcommit_memory r,

  /sys/kernel/mm/transparent_hugepage/enabled r,
}

But this has one big problem, every instance can read the files of every other instance. Less ideal isnt it? Luckily we have another trick up our sleeves.

Splitting it up

[Unit]
Description=Redis
After=network.target
PartOf=redis.target

[Service]
Type=simple
User=redis
Group=redis
PrivateTmp=true
AppArmorProfile=redis.%i
PIDFile=/var/run/redis/%i.pid
ExecStart=/usr/sbin/redis-server /etc/redis/%i.conf
Restart=on-failure

[Install]
WantedBy=multi-user.target redis.target

The important addition compared to the original systemd service file is “AppArmorProfile=redis.%i”. This will make systemd launch the service in that apparmor profile if it exists. If it doesn’t exist the service isnt started. The apparmor side of the solution has 2 parts. Part 1 has the abstraction with all the common bits and pieces. While part 2 has the service specific parts.

“/etc/apparmor.d/abstractions/redis-server”

#include <abstractions/base>
#include <abstractions/nameservice>

/{var/,}run/redis/* rwlk,

network inet  stream,
network inet6 stream,

owner /proc/*/maps r,
owner /proc/*/smaps r,
owner /proc/*/stat r,

/proc/sys/net/core/somaxconn r,
/proc/sys/vm/overcommit_memory r,

/sys/kernel/mm/transparent_hugepage/enabled r,

“/etc/apparmor.d/redis.discourse”

#include <tunables/global>

profile redis.discourse flags=(attach_disconnected) {
  #include <abstractions/redis-server>

  /etc/redis/discourse.conf r,
  /var/lib/redis/discourse/** rw,
  /var/log/redis/discourse.log rw,
}

Create a copy of “/etc/apparmor.d/redis.discourse” for every further instance and adapt the paths. Done.

What if?

So we got it all set up but the service file does not have the ApparmorProfile line set. Arguably shipping the service file with that line is tricky. If the profile does not exist the service wont start. Neither if apparmor is not enabled at all. But we can use the technique from the Per service ulimits post to hook us into the startup.

So instead of patching redis@.service we do create “/etc/systemd/system/redis@.service.d/apparmor.conf” with the following content:

[Service]
AppArmorProfile=redis.%i

“systemctl daemon-reload” and voila the next start of redis we are all set.

Easy hm?