Skip to content

Test What a Service Restriction Actually Blocks

“Runs in a container” and “uses little memory” do not explain which access a workload has lost. Choose a boundary, state the prohibited action, and test that action without changing a working service.

Outcome: run a short-lived user service that can create a Unix-domain socket but cannot create a new IPv4 socket. You need a Linux host with systemd, a working user manager, Python 3 and support for systemd's syscall-based restrictions. Complete the service-exposure lesson first. Do not use sudo for this lab.

Different controls answer different questions

  • A service account provides a separate user identity, whose file/device permissions still need review.
  • A systemd restriction narrows a particular service's permitted behavior. Each setting has a defined scope and platform requirements.
  • A container can isolate namespaces and limit resources, but normally shares the host kernel. Mounts, devices and management sockets can grant substantial host access.
  • A VM normally runs its own guest kernel behind a virtualization boundary. The host, virtual devices, configuration and implementation remain part of the trust model.

These are not a universal ranking. A memory limit constrains resource use; it does not by itself prevent reading a private key available to the process. A workload running as your user may read your user's files unless another enforced restriction prevents it.

Read systemd.exec for execution restrictions and systemd.resource-control for resource controls. Features depend on the installed systemd/kernel, architecture and manager context; a setting name alone is not proof of enforcement.

Check the prerequisites

On the Pi, inspect without changing anything:

1
2
3
systemctl --version
systemctl --user is-system-running
command -v python3

A working user bus is required. “Failed to connect to bus” means this session/environment is unsuitable for the lab. An unhealthy manager needs investigation. Do not enable lingering, weaken AppArmor, disable a firewall or change kernel security policy to force the exercise to run.

First verify that ordinary Python can create an unbound IPv4 socket. This does not connect or listen:

python3 -c 'import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.close(); print("Baseline: IPv4 socket creation allowed")'

If this already fails, stop. You cannot attribute a later denial to the new service restriction without an allowed baseline.

Run one bounded transient service

Run this whole block on the Pi as your ordinary user. It creates only a new transient user service, not a persistent unit or a modification to SSH/network services:

systemd-run --user --wait --pipe --collect --service-type=exec \
    -p RuntimeMaxSec=10s -p TimeoutStopSec=2s \
    -p NoNewPrivileges=yes -p RestrictAddressFamilies=AF_UNIX \
    /usr/bin/python3 -c '
import errno
import socket

with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM):
    print("Allowed: Unix-domain socket creation")

try:
    blocked = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
except OSError as error:
    if error.errno not in (errno.EAFNOSUPPORT, errno.EPERM, errno.EACCES):
        raise
    print("Blocked: IPv4 socket creation (errno %s)" % error.errno)
else:
    blocked.close()
    raise SystemExit("Unexpected IPv4 access: restriction not demonstrated")

with open("/proc/self/status") as status:
    no_new_privs = next(line for line in status if line.startswith("NoNewPrivs:"))
if no_new_privs.split()[1] != "1":
    raise SystemExit("NoNewPrivileges was not demonstrated")
print("Confirmed: NoNewPrivs=1")
'

Expect three messages: Unix-domain socket allowed, IPv4 socket blocked, and NoNewPrivs=1. The command must also exit successfully. A startup error, an unexpected exception or “Unexpected IPv4 access” is not a passing test.

RestrictAddressFamilies=AF_UNIX limits new socket creation to that family. It is not a complete network sandbox: it does not revoke already-passed descriptors, and permitted Unix sockets may reach local services. NoNewPrivileges prevents gaining privileges through mechanisms such as set-user-ID execution; it does not erase privileges already held. Keep those limits explicit.

Verify lifetime and reversal

The systemd-run manual explains transient services, waiting, pipe output and collection. The tool prints the generated unit name. Normally the short test exits and --collect unloads its unit; the runtime limit bounds a hung test.

Inspect any remaining unit using the exact printed name with systemctl --user status. If you must interrupt it, use systemctl --user stop with that exact name, not a wildcard or a working service's name. Do not install or enable a unit for this exercise. A collected unit need not remain available for status inspection; retained journal messages are not proof it is still running.

The restriction belongs to the transient process, not your login shell. Rerun the ordinary Python baseline afterward to confirm your shell's socket creation is still allowed. No host-wide rule needs to be disabled to undo the lab.

Transfer the idea to a real service

List the real application's required files, network families, devices and privileges first. Apply one justified restriction, verify required behavior, then test a harmless prohibited action. Some daemons need IPv4/IPv6 and would break under this exact example; do not copy it wholesale into their units.

Use the existing systemd service guide for lifecycle configuration and sandboxing reference for further options. CVE case studies illustrate why a boundary's implementation also matters; a lab is not proof against every exploit.

Checkpoint: why does MemoryMax not stop a file read? Why can an allowed Unix socket still be security-sensitive?

Next: restore disposable data and verify it. Return to Security.

The transient-service lab passed on the verification Pi: IPv4 creation was denied with errno 97, Unix sockets remained available, and the ordinary shell's baseline worked afterward. See the exact environment and limits.

Donate