I love minimalism
I truly enjoy minimalism in life. Clutter makes me anxious, it overloads my brain. I try to follow the inbox-zero strategy. I don’t own many things and avoid subscriptions like the plague. This is also reflected in how I manage my work environments, fewer gadgets, fewer levers, fewer levels of abstraction and no “magic”. Just simple ingredients cooked well.
And I do feel vindicated when I see the current landscape in cybersecurity. Right now, high-visibility tools and popular npm packages are targeted by supply chain attacks. Critical tools, like FFMPEG and VMware products, continue to receive vulnerability disclosures. Not to mention the role of AI in accelerating all of this. Through simple logic, I like to think that in reducing moving parts, I reduce my surface and thus potential attack vectors.
A good opportunity for minimalist design presented itself to me recently. I’ve been playing with local LLMs on my homelab running Proxmox. Simple models that can run on CPU to perform menial tasks. By the way, check out the excellent ik_llama.cpp project if you’re thinking of doing something similar!
In order to get decent performance, I store the model in RAM ( — mlock). As you can imagine, this container on my homelab eats up a sizeable chunk of the available memory. And because hardware isn’t cheap and I don’t wan’t to buy more, I thought: Why wouldn’t I shut down my service when I’m not using it instead? Ideally, in a non-manual way… but still simpler than bringing in Kubernetes or some other enterprise-sized monstrosity.
I started my search in the most naive way, not knowing that I was already sitting on an incredibly powerful tool and only using it to pass the butter.

We’ve all used systemd to create services to run commands on boot… but I, for one didn’t know how capable and complete it really is. Let’s break down my unit file from the simple “run on boot” setup to a complete wrapper that:
- Limits what my unit can do and what folders it can access
- Fully shuts down when there is no traffic
- Boots up when network requests come in
- Implements a reverse proxy
- Manages credentials, secrets and keys
No 3rd party software, no extra control plane, just software that ships by default on most Linux distros!
“The basic”
Tutorials on the web will usually stop at setting up the most basic unit so it starts a program on system boot. Here’s our ikllama.service:
[Unit]
Description=My llm service
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/root/ik_llama.cpp/build/bin/llama-server --model /root/models/Ministral-3-8B-Reasoning-2512-Q5_K_M.gguf -ngl 0 -rtr --no-mmap --mlock --ctx-size 65536
Restart=on-failure
RestartSec=3s
Here we’ve defined a unit that will be executed once the network is online.
You’ll notice already that this isn’t the most secure configuration. Indeed, you should avoid running services as root!
Let’s create an unprivileged user, then move the executable and the models to that user’s home. We’ll update our service with the new user info and also add a few more security sandboxing options.
[Unit]
Description=My llm service
Wants=network-online.target
After=network-online.target
[Service]
User=ikllama
KeyringMode=private
ProtectClock=yes
ProtectHostname=yes
ProtectKernel=yes
NoNewPrivileges=yes
PrivateDevices=yes
ProtectSystem=full
ExecStart=/home/ikllama/ik_llama.cpp/build/bin/llama-server --model /home/ikllama/models/Ministral-3-8B-Reasoning-2512-Q5_K_M.gguf -ngl 0 -rtr --no-mmap --mlock --ctx-size 65536
Restart=on-failure
RestartSec=3s
# Prevents access to the keyring of users
KeyringMode=private
# Prevents changes to the system clock
ProtectClock=yes
# Prevents changes to the hostname or NIS domain name of the system
ProtectHostname=yes
# Prevents explicit loading of kernel modules
ProtectKernelModules=yes
# Prevents processes from gaining new privileges
NoNewPrivileges=yes
# Limits access to pseudo-devices such as /dev/null, /dev/random, /dev/urandom, and /dev/zero
PrivateDevices=yes
# Marks some file system paths as read-only (ex: /usr, /boot, /efi, /etc)
ProtectSystem=full
You may choose to add or remove some service hardening rules since your mileage may vary depending on the application and the specific setting. When an app is untrusted, you can specify explicit ReadWritePaths that a service has access to and even lock down the processes’ CapabilityBoundingSet.
In doubt, a number of tools exist to test whether an app requires particular system calls or capabilities. systemd-analyze security, strace or getpcaps just to name a few.
Let’s scale
This is where things get exciting. Instead of starting the service at boot, let’s use socket activation to start it when a connection arrives. We’ll need to create a new service, which we’ll name ikllama-proxy.socket
[Unit]
Description=ikllama Socket Activation
[Socket]
ListenStream=80
[Install]
WantedBy=sockets.target
Two things to notice here, one is that we’re listening on port 80- which normally requires some configuration since this is a privileged port. But here systemd will act as our reverse proxy and has no issues with this!
Second is that our ikllama doesn’t bind to port 80! Which means that we need to make another systemd configuration to create our simple reverse proxy leveraging systemd-socket-proxyd. We’ll name it ikllama-proxy.service
[Unit]
Description=ikllama Socket Proxy
Requires=ikllama.service
After=ikllama.service
[Service]
DynamicUser=yes
PrivateDevices=yes
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ExecStartPre=/bin/bash -c 'for i in \$(seq 1 150); do (echo > /dev/tcp/127.0.0.1/8080) >/dev/null 2>&1 && exit 0; sleep 0.2; done; echo "ikllama backend on port 8080 did not become ready in time" >&2; exit 1'
ExecStart=/lib/systemd/systemd-socket-proxyd 0.0.0.0:8080
Next we’ll need to modify the unit section of our ikllama.service
BindsTo=ikllama-proxy.socket
After=ikllama-proxy.socket
Now when we visit 0.0.0.0:80 our service starts and we get served our app.
Note that it is also still available at port 8080. Visiting that address does not trigger the service startup though. We may also want to create a firewall rule to restrict access to that port.
It is important that your proxy service and socket have the same name so the socket reference is passed.
What about shutting down
Unfortunately, the strategy I came up with to shut down idle services is a bit less polished than previous steps.
We’ll create a new service called ikllama-monitor.service
[Unit]
Description=Network Idle Monitor for ikllama
After=network.target
[Service]
Type=simple
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=full
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ExecStart=/root/scripts/monitor.sh 80 ikllama
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
This service launches a bash script (available on github) that periodically checks for activity on a given port. If no activity is detected, the ikllama service is stopped.
Now, when I visit my llm’s webgui, the service starts (almost instantly). And a little while after I close the tab, the RAM is reclaimed in my Proxmox environment.
To try all of this for yourselves I have begun compiling a set of tools for homelab enthousiast such as myself here: https://github.com/Nerve-box/dotfiles
Contributions and suggestions are welcome!
What this means for homelab security
By reducing the control plane to purely the OS (systemd, firewall), we reduce the number of software components that need to be trusted, configured and kept patched.
Furthermore, it forces us to better understand how software runs in a given environment and to properly sandbox it, developing good security reflexes.
I hope you learned a thing or two. I sure did!
Happy homelabbing!
References
https://linux-audit.com/systemd/settings/units/
https://www.freedesktop.org/software/systemd/man/latest/systemd-socket-proxyd.html






