Skip to content

Instantly share code, notes, and snippets.

@emk
Last active January 11, 2026 22:59
Show Gist options
  • Select an option

  • Save emk/a7834664da037622cf36abc90e58e840 to your computer and use it in GitHub Desktop.

Select an option

Save emk/a7834664da037622cf36abc90e58e840 to your computer and use it in GitHub Desktop.
Run Gas Town with a confined blast radius. Maybe.

systemd-nspawn Container Setup Guide

Want to run Gas Town on a Linux machine with a pretense of isolation? You could probably run Docker if you're sensible, but I decided to be foolish and build a complete VM-like setup on top of systemd-nspawn with:

  • A mapped, shared gt directory.
  • A network firewall to isolate it from your local net, assuming you use private networking IDs. It should be able to look stuff up on the internet, but probably can't talk to internal stuff.
  • A bunch of container namespacing support which seemed like a good idea.

The instructions below are by Claude Code Sonnet 4.5, which also did 80% of the troubleshooting. There may be a missing step or two somewhere. Use at your own risk. This is basically a jury-rigged amateur zoo for observing 10-30 agents. The threat models is "well-meaning but overenthusiastic."

Overview

This guide documents the setup of a systemd-nspawn container named "gastown" with:

  • Ubuntu 24.04 (noble)
  • User namespacing for security isolation
  • Shared directory with UID mapping
  • Isolated network with internet access
  • Optional firewall rules for network isolation
  • Port forwarding (8080:8080)

Prerequisites

sudo apt-get install debootstrap systemd-container

Setup Steps

1. Bootstrap Ubuntu Noble

# Bootstrap the container
sudo debootstrap --include=systemd,dbus,sudo,vim,wget,curl noble /var/lib/machines/gastown http://archive.ubuntu.com/ubuntu/

2. Basic Configuration

# Set root password
sudo systemd-nspawn -D /var/lib/machines/gastown passwd

# Set machine ID
sudo systemd-nspawn -D /var/lib/machines/gastown systemd-machine-id-setup

3. Create User Account

# Shell into the container
sudo machinectl shell gastown

# Inside the container:
useradd -m -s /bin/bash gastown
passwd gastown
usermod -aG sudo gastown
exit

4. Configure Container

Create /etc/systemd/nspawn/gastown.nspawn:

[Exec]
Boot=yes
Hostname=gastown

[Files]
Bind=/home/USERNAME/gt:/home/gastown/gt:idmap

[Network]
Private=yes
VirtualEthernet=yes
Port=tcp:8080:8080

Key point: The :idmap suffix on the bind mount automatically maps host UID/GID to container UID/GID, making files owned by the Ubuntu initial user (UID 1000) on the host appear as owned by gastown (UID 1000) in the container.

5. Configure Networking

The container needs network configuration to access the internet and perform DNS resolution.

5.1. Enable systemd-networkd on Host

sudo systemctl enable systemd-networkd
sudo systemctl start systemd-networkd

Note: This will only manage container interfaces (ve-*), not your main network interfaces.

5.2. Configure Host-Side Virtual Ethernet

Create /etc/systemd/network/80-container-ve.network:

[Match]
Name=ve-*
Driver=veth

[Network]
Address=10.0.85.1/24
IPMasquerade=ipv4
DHCPServer=yes
IPForward=yes

[DHCPServer]
PoolOffset=100
PoolSize=50
EmitDNS=yes
DNS=8.8.8.8 1.1.1.1

This configuration:

  • Matches all virtual ethernet interfaces for containers (ve-*)
  • Assigns 10.0.85.1/24 to the host side
  • Enables NAT/masquerading for internet access
  • Runs a DHCP server providing IPs 10.0.85.100-149
  • Provides public DNS servers (Google 8.8.8.8, Cloudflare 1.1.1.1)

5.3. Configure Container-Side Network

Create /var/lib/machines/gastown/etc/systemd/network/80-container-host0.network:

[Match]
Virtualization=container
Name=host0

[Network]
DHCP=yes

This configures the container to obtain its IP address, gateway, and DNS via DHCP.

5.4. Enable systemd-networkd Inside Container

Critical step: Enable systemd-networkd and systemd-resolved inside the container:

# Enable and start systemd-networkd in container
machinectl shell gastown /bin/systemctl enable --now systemd-networkd

# Enable and start systemd-resolved in container for DNS
machinectl shell gastown /bin/systemctl enable --now systemd-resolved

5.5. Apply Network Configuration

Reload systemd-networkd on the host and restart the container:

# Restart systemd-networkd to pick up new configuration
sudo systemctl restart systemd-networkd

# Wait for network to stabilize
sleep 3

6. Configure Firewall

Important: If you have Docker installed, this step is required because Docker sets the FORWARD chain policy to DROP. The firewall rules allow internet access while blocking local network access.

Create /etc/systemd/system/gastown-firewall.service:

[Unit]
Description=Firewall rules for gastown container
After=network.target systemd-nspawn@gastown.service
Wants=systemd-nspawn@gastown.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/bin/bash -c 'iptables -I FORWARD 1 -o ve-gastown -m state --state RELATED,ESTABLISHED -j ACCEPT; \
                         iptables -I FORWARD 2 -i ve-gastown -d 192.168.0.0/16 -j REJECT --reject-with icmp-port-unreachable; \
                         iptables -I FORWARD 3 -i ve-gastown -d 172.16.0.0/12 -j REJECT --reject-with icmp-port-unreachable; \
                         iptables -I FORWARD 4 -i ve-gastown -d 10.0.0.0/8 ! -s 10.0.85.0/24 -j REJECT --reject-with icmp-port-unreachable; \
                         iptables -I FORWARD 5 -i ve-gastown -d 169.254.0.0/16 -j REJECT --reject-with icmp-port-unreachable; \
                         iptables -I FORWARD 6 -i ve-gastown -j ACCEPT'
ExecStop=/bin/bash -c 'iptables -D FORWARD -o ve-gastown -m state --state RELATED,ESTABLISHED -j ACCEPT; \
                        iptables -D FORWARD -i ve-gastown -d 192.168.0.0/16 -j REJECT --reject-with icmp-port-unreachable; \
                        iptables -D FORWARD -i ve-gastown -d 172.16.0.0/12 -j REJECT --reject-with icmp-port-unreachable; \
                        iptables -D FORWARD -i ve-gastown -d 10.0.0.0/8 ! -s 10.0.85.0/24 -j REJECT --reject-with icmp-port-unreachable; \
                        iptables -D FORWARD -i ve-gastown -d 169.254.0.0/16 -j REJECT --reject-with icmp-port-unreachable; \
                        iptables -D FORWARD -i ve-gastown -j ACCEPT'

[Install]
WantedBy=multi-user.target

Enable the firewall service:

sudo systemctl daemon-reload
sudo systemctl enable gastown-firewall.service
sudo systemctl start gastown-firewall.service

What this does:

  • ✅ Rule 1: Allows return traffic from internet to container (ESTABLISHED,RELATED)
  • ❌ Rules 2-5: Block access to private networks (192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 except container subnet, 169.254.0.0/16)
  • ✅ Rule 6: Allow all outbound traffic from container (internet access)

Result:

  • ✅ Container can access the internet
  • ❌ Container cannot access local network (192.168.0.0/16)
  • ❌ Container cannot access other private networks

Note: Rules are inserted at the top of the FORWARD chain (positions 1-6) to ensure they are evaluated before Docker's rules. This is critical when Docker is installed.

7. Enable and Start

# Enable auto-start
sudo machinectl enable gastown

# Start the container
sudo machinectl start gastown

# Verify it's running
machinectl list

8. Verify Networking

After setup, verify internet connectivity and DNS resolution:

# Check host-side interface has IP
ip addr show ve-gastown

# Check container-side interface has IP from DHCP
machinectl shell gastown /bin/ip addr show host0

# Test internet connectivity (IP-based)
machinectl shell gastown /bin/ping -c 2 8.8.8.8

# Test DNS resolution
machinectl shell gastown /bin/ping -c 2 google.com

# Test HTTPS connectivity
machinectl shell gastown /bin/curl -I https://google.com

# Verify local network is blocked (should fail)
machinectl shell gastown /bin/ping -c 2 192.168.86.1

Expected results:

  • ✅ Pings to 8.8.8.8 should succeed
  • ✅ Pings to google.com should succeed (DNS working)
  • ✅ HTTPS requests should work
  • ❌ Pings to 192.168.86.1 (or your router IP) should fail

Daily Usage Commands

Container Management

# Start container
sudo machinectl start gastown

# Stop container
sudo machinectl stop gastown

# Restart container
sudo machinectl restart gastown

# Check status
machinectl list
sudo machinectl status gastown

Accessing the Container

# Shell as root
sudo machinectl shell gastown

# Shell as gastown user
sudo machinectl shell --uid=gastown gastown

# Run a single command as gastown
sudo machinectl shell --uid=gastown gastown /bin/bash -c "command here"

Monitoring

# View recent logs
sudo journalctl -u systemd-nspawn@gastown.service -n 50

# Follow logs in real-time
sudo journalctl -u systemd-nspawn@gastown.service -f

How It Works

User Namespacing

The default systemd-nspawn service uses the -U flag, which enables user namespacing:

  • Container UIDs are shifted into a high range (e.g., 1776025600+)
  • UID 0 (root) in container maps to 1776025600 on host
  • UID 1000 (gastown) in container maps to 1776025601 on host
  • This provides security isolation—container root can't affect host

UID Mapping with idmap

The idmap bind mount option:

  • Maps host UID 1000 (initial user) to container UID 1000 (gastown)
  • Makes shared files readable/writable by both users
  • Works transparently with user namespacing

Network Configuration

Container Network Namespace:

  • Private=yes: Container gets its own network namespace
  • VirtualEthernet=yes: Creates a virtual ethernet pair (veth)
    • Host side: ve-gastown
    • Container side: host0
  • Port=tcp:8080:8080: Forwards port 8080 from container to host

systemd-networkd Setup:

  • Host: systemd-networkd manages ve-gastown interface
    • Host-side interface (ve-gastown) gets 10.0.85.1/24
    • DHCP server runs on host, providing IPs to containers (10.0.85.100-149)
    • IPMasquerade=ipv4 enables NAT for internet access
    • Public DNS servers (8.8.8.8, 1.1.1.1) provided via DHCP
  • Container: systemd-networkd must be enabled inside the container
    • Container-side interface (host0) gets IP via DHCP
    • systemd-resolved handles DNS queries

Firewall Isolation:

  • Required when Docker is installed (Docker sets FORWARD policy to DROP)
  • Allows internet access from container
  • Blocks container from accessing local network (192.168.0.0/16)
  • Prevents lateral movement to other private networks
  • Rules inserted at top of FORWARD chain (positions 1-6) to take precedence over Docker rules

Troubleshooting

Container won't start

# Check detailed logs
sudo journalctl -u systemd-nspawn@gastown.service -n 100

# Verify configuration syntax
sudo systemd-analyze verify systemd-nspawn@gastown.service

Bind mount shows wrong permissions

  • Ensure you're using :idmap suffix on the bind mount
  • Verify both host and container users have UID 1000
  • Restart container after config changes

Network not working

Common causes:

  1. systemd-networkd not enabled in container - Most common issue!

    # Check status in container
    machinectl shell gastown /bin/systemctl status systemd-networkd
    
    # If disabled, enable it
    machinectl shell gastown /bin/systemctl enable --now systemd-networkd
    machinectl shell gastown /bin/systemctl enable --now systemd-resolved
  2. Firewall rules missing or incorrect - Required when Docker is installed

    # Check FORWARD policy (if DROP, firewall rules are required)
    sudo iptables -L FORWARD -n -v | head -2
    
    # Check if firewall service is running
    sudo systemctl status gastown-firewall.service
    
    # View current FORWARD rules
    sudo iptables -L FORWARD -n -v --line-numbers
  3. Network interface diagnostic commands:

    # Check if virtual ethernet interface exists and is UP
    ip link show ve-gastown
    
    # Check host-side interface has IP address
    ip addr show ve-gastown
    
    # Check container-side interface
    machinectl shell gastown /bin/ip addr show host0
    
    # Test internet connectivity (IP-based)
    machinectl shell gastown /bin/ping -c 4 8.8.8.8
    
    # Test DNS resolution
    machinectl shell gastown /bin/ping -c 4 google.com
    
    # Check DNS configuration in container
    machinectl shell gastown /bin/resolvectl status
    
    # Verify NAT/masquerading is enabled
    sudo iptables -t nat -L -n -v | grep MASQUERADE
    
    # Check systemd-networkd status on host
    sudo systemctl status systemd-networkd
    
    # View networkd logs
    sudo journalctl -u systemd-networkd -n 50

Cleanup (if needed)

# Stop and disable container
sudo machinectl stop gastown
sudo machinectl disable gastown

# Stop and disable firewall (if configured)
sudo systemctl stop gastown-firewall.service
sudo systemctl disable gastown-firewall.service

# Remove container root filesystem
sudo rm -rf /var/lib/machines/gastown

# Remove container configuration
sudo rm /etc/systemd/nspawn/gastown.nspawn

# Remove network configuration (optional - affects all ve-* containers)
sudo rm /etc/systemd/network/80-container-ve.network

# Remove firewall service
sudo rm /etc/systemd/system/gastown-firewall.service

# Reload systemd
sudo systemctl daemon-reload

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment