DEV Community

Avery Lin
Avery Lin

Posted on

Derive a systemd Sandbox from AI-Generated Service Code Instead of Trusting Its Permissions

Why this is worth reading: AI-generated service code usually gets reviewed for whether it starts, not for what it asks the kernel to allow after startup. That misses the actual danger on a small free server, where a service written by a free model tends to fail open by opening extra network families, writing outside its own directory, or keeping privileges it never uses. You can turn that vague worry into a reproducible gate: scan the generated code for three concrete signals, translate those signals into a systemd sandbox drop-in, and verify the result with systemd-analyze security instead of trusting the unit file that arrived with the prompt. When you use MonkeyCode's free model access to draft a service for a free server, this is the review step to run before the first systemctl start. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Review what the code asks for, not what the model claims

Start from the generated repository, not from the model's explanation of what the service does. A model can produce a plausible README that says "runs as a low-privilege user" while the actual code calls chmod, writes to /etc, or binds a socket with broad access. On a free server, those mistakes are not an abstract supply-chain concern; they directly affect the small amount of RAM, disk, and public network surface you have.

You need three lists before writing any hardening:

  1. Filesystem writes - where does the service actually write data, caches, locks, or logs?
  2. Network access - which address families and destinations does it use?
  3. Privilege transitions - does it call sudo, chmod, chown, subprocess, or system()?

A first-pass scanner can collect those signals without executing anything. The scanner is intentionally dumb. It is not a security proof; it is a prompt for the systemd unit you will build.

#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="${1:?usage: derive-sandbox.sh /path/to/repo}"

printf '%s\n' "## writes"
# Files opened for append/truncate/write in common languages.
grep -RIlE \
  -e "open\\([^,]+,[^)]*['\"][wa]" \
  -e 'fs\.writeFile|fs\.writeFileSync|fs\.createWriteStream' \
  -e 'open\\([^,]+,[^)]*O_WRONLY|O_RDWR' \
  -e 'Path\..*\.write_(text|bytes)' \
  -e '\.to_csv\(|joblib\.dump|pickle\.dump' \
  "$REPO_DIR" 2>/dev/null | sort -u || true

printf '%s\n' "## network"
grep -RIoE \
  -e 'https?://[a-zA-Z0-9./_-]+' \
  -e 'socket\.?\(|createConnection\(|connect\(' \
  -e 'requests\.(get|post|put|patch|delete)|urllib\.request|urlopen' \
  -e 'fetch\(|axios\.(get|post|put)' \
  "$REPO_DIR" 2>/dev/null | sort -u || true

printf '%s\n' "## privilege transitions"
grep -RIoE \
  -e 'sudo|chmod|chown|setuid|setgid|setcap' \
  -e 'subprocess|os\.system|exec\(|system\(' \
  "$REPO_DIR" 2>/dev/null | sort -u || true
Enter fullscreen mode Exit fullscreen mode

Run it against the generated repo:

./derive-sandbox.sh ~/repo
Enter fullscreen mode Exit fullscreen mode

Treat the output as a list of namespaces you need to constrain, not as a complete audit. Many dangerous patterns will still hide behind string concatenation, dynamic imports, generated config, or shell expansion. That is exactly why the next step matters: you do not manually inspect every line, you shrink the kernel's room to make mistakes.

Map the signals to systemd directives

Once the scan returns a set of writes, sockets, and process calls, convert each item into a narrow sandbox rule. Keep the rule minimal, then add back only the concrete path or destination the service needs after it fails.

Signal you found Likely requirement First-pass systemd directive
Writes only to data/, /tmp/cache, or ./var writable data directory PrivateTmp=yes and ReadWritePaths=/var/lib/myapp /run/myapp mapped to the real paths
No writes outside the app directory read-only base system ProtectSystem=strict and ProtectHome=tmpfs
Opens sockets other than a normal TCP client broad network access RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 plus IPAddressDeny=any, then allow only the exact IP/CIDR
Calls subprocess, sudo, chmod, chown process and filesystem privileges NoNewPrivileges=yes, CapabilityBoundingSet= empty, RestrictNamespaces=yes
Loads code into writable or executable memory runtime JIT or memory safety risk MemoryDenyWriteExecute=yes unless the runtime genuinely requires JIT

The important move is to start from a deny-by-default stance. The generated code may already include a unit file, but you should not accept its ProtectSystem=full claim if the service writes to /etc or calls sudo. Build the sandbox from observed behavior, then let systemd enforce it.

Apply the drop-in and verify

Assume the service runs as myapp and writes only to /var/lib/myapp and /run/myapp, needs outbound HTTPS to a single API at 203.0.113.10, and must not bind a privileged port. Create a drop-in that encodes exactly those limits:

# /etc/systemd/system/myapp.service.d/10-sandbox.conf
[Service]
User=myapp
Group=myapp
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=tmpfs
ReadWritePaths=/var/lib/myapp /run/myapp
UMask=0077
CapabilityBoundingSet=
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
IPAddressDeny=any
IPAddressAllow=203.0.113.10
RestrictNamespaces=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
RestrictRealtime=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectHostname=yes
ProtectClock=yes
Enter fullscreen mode Exit fullscreen mode

If the service needs to bind port 80 or 443 directly, add only the capability that is necessary:

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
Enter fullscreen mode Exit fullscreen mode

Otherwise keep the capability set empty and run the process behind a local reverse proxy on a high port. If outbound DNS resolution is required, add your resolver address to IPAddressAllow= as well; do not open all traffic just because DNS failed once.

Reload and measure the unit before trusting it:

sudo systemctl daemon-reload
sudo systemctl restart myapp
systemd-analyze security myapp
Enter fullscreen mode Exit fullscreen mode

systemd-analyze security returns an exposure score. Use that number only as a comparison before and after your drop-in, not as a pass/fail certificate. The score can improve even when a generated service still has a dangerous eval hidden in a string; the score cannot replace observing the process.

Limitations and who should not use this

The static scanner misses anything assembled at runtime. A generated Python service can build a path with fmt, import a module based on environment variables, or execute a payload stored in a base64 string. The systemd drop-in cannot see those strings; it only prevents the resulting process from writing to large parts of the filesystem, opening arbitrary sockets, or calling privileged syscalls. If the service later fails because it needs a path or address you did not allow, that failure is useful information: add the narrow exception and run the check again.

For a second pass, run the service under a no-network namespace before granting any outbound access. That gives you a dynamic trace of writes and process attempts without exposing the public network. This is also the point where a free server becomes a good test bed precisely because a failed sandbox costs you a restart, not a production incident.

Do not use this workflow as your only audit if you are handling regulated data, managing credentials for other people, or running a service that must be available continuously. In those cases, use signed images, policy checks, and a real review. This approach is for the smaller free-server case where an AI-generated service might otherwise land on a host with too much default privilege.

If you generate the initial service draft with MonkeyCode's free model access, make this systemd drop-in part of the generated review artifact rather than asking the model for a permissions summary. The useful answer is the kernel-facing rule set you can read and test, not the sentence that says the service is safe.

Top comments (0)