|
#!/bin/bash |
|
# spawn-hybrid.sh - Secure Container Manager for AI Agents |
|
|
|
set -euo pipefail |
|
|
|
# --- Configuration --- |
|
CONTAINER_BASE="${CONTAINER_BASE:-$HOME/.local/containers}" |
|
CONTAINER_CONFIG="${CONTAINER_CONFIG:-$HOME/.config/systemd-nspawn}" |
|
CONTAINER_LOGS="${CONTAINER_LOGS:-$HOME/.local/log/spawn}" |
|
DEFAULT_CPU_QUOTA="50%" |
|
DEFAULT_MEM_LIMIT="2G" |
|
|
|
# Colors |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' |
|
|
|
# --- Logging Helpers --- |
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } |
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } |
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } |
|
log_sec() { echo -e "${BLUE}[SECURITY]${NC} $1"; } |
|
|
|
# --- Dependency Check --- |
|
# Ensure all required host utilities are available before running. |
|
check_dependencies() { |
|
local missing=() |
|
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do |
|
if ! command -v "$cmd" &> /dev/null; then |
|
missing+=("$cmd") |
|
fi |
|
done |
|
if [ ${#missing[@]} -gt 0 ]; then |
|
log_error "Missing dependencies: ${missing[*]}" |
|
exit 1 |
|
fi |
|
} |
|
|
|
# --- Initialization --- |
|
# Setup necessary directory structures in the host environment. |
|
setup_directories() { |
|
mkdir -p "$CONTAINER_BASE" |
|
mkdir -p "$CONTAINER_CONFIG" |
|
mkdir -p "$CONTAINER_LOGS" |
|
chmod 700 "$CONTAINER_LOGS" |
|
} |
|
|
|
# --- Help / Usage --- |
|
show_help() { |
|
cat << EOF |
|
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options] |
|
|
|
Commands: |
|
create <name> [source] [dest] Create a secure container |
|
--source, -s Host directory to mount (e.g., ~/projects) |
|
--dest, -d Mount point inside container (default: /mnt/work) |
|
--rw Mount as Read-Write (Default is Read-Only) |
|
--cpu CPU Quota (default: 50%) |
|
--memory Memory Limit (default: 2G) |
|
--network Enable networking (Default: disabled/isolated) |
|
--timeout Auto-shutdown in seconds (default: 3600) |
|
--packages Extra packages to install (comma separated) |
|
|
|
start <name> Start container |
|
stop <name> Stop container |
|
shell <name> Open secure shell |
|
delete <name> Destroy container |
|
list Show inventory |
|
info <name> Show details & password |
|
-h, --help Show this help message |
|
EOF |
|
} |
|
|
|
# --- Core Functions --- |
|
|
|
create_container() { |
|
# Fail early if no container name is provided. |
|
if [ -z "${1:-}" ]; then |
|
log_error "Container name required." |
|
echo "" |
|
show_help |
|
exit 1 |
|
fi |
|
|
|
# Catch help flag passed as the first argument |
|
if [[ "$1" == "-h" || "$1" == "--help" ]]; then |
|
show_help |
|
exit 0 |
|
fi |
|
|
|
local name="$1" |
|
shift |
|
|
|
# Defaults |
|
local mount_src="" |
|
local mount_dst="" |
|
local mount_mode="ro" |
|
local cpu_quota="$DEFAULT_CPU_QUOTA" |
|
local mem_limit="$DEFAULT_MEM_LIMIT" |
|
local enable_net="false" |
|
local timeout="3600" |
|
local extra_pkgs="" |
|
|
|
# Argument Parsing (Hybrid: Flags or Positional) |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-s|--source) mount_src="$2"; shift 2 ;; |
|
-d|--dest) mount_dst="$2"; shift 2 ;; |
|
--rw) mount_mode="rw"; shift ;; |
|
--cpu) cpu_quota="$2"; shift 2 ;; |
|
--memory) mem_limit="$2"; shift 2 ;; |
|
--network) enable_net="true"; shift ;; |
|
--timeout) timeout="$2"; shift 2 ;; |
|
--packages) extra_pkgs="$2"; shift 2 ;; |
|
-h|--help) show_help; exit 0 ;; |
|
-*) log_error "Unknown option: $1"; exit 1 ;; |
|
*) |
|
# Handle positional arguments: [source] [dest] |
|
if [ -z "$mount_src" ]; then |
|
mount_src="$1" |
|
elif [ -z "$mount_dst" ]; then |
|
mount_dst="$1" |
|
else |
|
log_error "Too many arguments. Unexpected: $1" |
|
exit 1 |
|
fi |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
# Default container mount destination if none is specified |
|
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi |
|
|
|
local container_path="$CONTAINER_BASE/$name" |
|
|
|
if [ -d "$container_path" ]; then |
|
log_error "Container '$name' already exists." |
|
exit 1 |
|
fi |
|
|
|
# 1. Security Validation |
|
# Ensure mounted host directories are not sensitive system paths. |
|
if [ -n "$mount_src" ]; then |
|
mount_src=$(realpath "$mount_src") |
|
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root") |
|
for blocked in "${blocked_paths[@]}"; do |
|
if [[ "$mount_src" == "$blocked"* ]]; then |
|
log_error "Security Block: Mounting '$mount_src' is forbidden." |
|
exit 1 |
|
fi |
|
done |
|
if [ ! -d "$mount_src" ]; then |
|
log_error "Source directory does not exist: $mount_src" |
|
exit 1 |
|
fi |
|
fi |
|
|
|
log_info "Initializing container '$name'..." |
|
mkdir -p "$container_path" |
|
|
|
# 2. Bootstrap |
|
# Install Arch Linux base system and required packages. |
|
log_info "Bootstrapping Arch Linux..." |
|
local pkg_list="base python python-pip git ${extra_pkgs//,/ }" |
|
|
|
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then |
|
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log" |
|
sudo rm -rf "$container_path" |
|
exit 1 |
|
fi |
|
|
|
# 3. Generate Credentials |
|
# Create a secure, random password for the container users. |
|
local agent_pass |
|
agent_pass=$(openssl rand -base64 24) |
|
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd" |
|
chmod 600 "$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
# 4. Configure User & Network |
|
# Set the hostname inside the container. |
|
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null |
|
|
|
# Configure networking if enabled via flags. |
|
if [ "$enable_net" == "true" ]; then |
|
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF |
|
[Match] |
|
Name=host0 |
|
[Network] |
|
DHCP=yes |
|
EOF |
|
else |
|
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network" |
|
fi |
|
|
|
# Enable systemd-resolved and systemd-networkd for DNS and DHCP. |
|
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service" |
|
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service" |
|
|
|
# Setup the 'agent' user and set the root password for su/sudo access. |
|
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent |
|
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
|
|
# Add agent to the wheel group if sudo was installed. |
|
if [[ "$pkg_list" == *"sudo"* ]]; then |
|
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent |
|
fi |
|
|
|
# 5. Create Secure Config (.nspawn) |
|
# Generate the systemd-nspawn profile defining sandboxing rules. |
|
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn" |
|
local mem_bytes |
|
mem_bytes=$(numfmt --from=iec "$mem_limit") |
|
|
|
# Configure directory binds based on provided flags. |
|
local bind_mount="" |
|
if [ -n "$mount_src" ]; then |
|
if [ "$mount_mode" == "rw" ]; then |
|
bind_mount="Bind=$mount_src:$mount_dst" |
|
else |
|
bind_mount="BindReadOnly=$mount_src:$mount_dst" |
|
fi |
|
fi |
|
|
|
cat > "$nspawn_file" <<EOF |
|
[Exec] |
|
Boot=yes |
|
# Safely map container root to unprivileged host user |
|
PrivateUsers=pick |
|
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD |
|
ProcessTwo=yes |
|
# Removed NoNewPrivileges=yes to allow su/sudo functionality |
|
|
|
[Network] |
|
VirtualEthernet=yes |
|
Private=yes |
|
|
|
[CPU] |
|
CPUQuota=$cpu_quota |
|
|
|
[Memory] |
|
MemoryMax=$mem_bytes |
|
|
|
[Files] |
|
PrivateUsersOwnership=auto |
|
ProtectSystem=strict |
|
ProtectHome=read-only |
|
PrivateDevices=yes |
|
ProtectKernelTunables=yes |
|
ProtectControlGroups=yes |
|
LockPersonality=yes |
|
$bind_mount |
|
EOF |
|
|
|
# 6. Auto-Shutdown Timer |
|
setup_shutdown_timer "$container_path" "$timeout" |
|
|
|
# 7. Metadata |
|
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF |
|
NAME=$name |
|
CREATED=$(date -Iseconds) |
|
MOUNT_SRC=$mount_src |
|
MOUNT_MODE=$mount_mode |
|
NET_ENABLED=$enable_net |
|
EOF |
|
|
|
log_sec "Container '$name' created." |
|
log_info "Password saved to secure vault." |
|
} |
|
|
|
# --- Auto-Shutdown Timer --- |
|
# Create a systemd service inside the container to power it off after a timeout. |
|
setup_shutdown_timer() { |
|
local path="$1" |
|
local timeout="$2" |
|
|
|
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null |
|
#!/bin/bash |
|
sleep $timeout |
|
echo "Time limit reached. Shutting down." |
|
poweroff |
|
EOF |
|
sudo chmod +x "$path/usr/local/bin/auto-shutdown" |
|
|
|
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null |
|
[Unit] |
|
Description=Security Timeout |
|
After=multi-user.target |
|
|
|
[Service] |
|
ExecStart=/usr/local/bin/auto-shutdown |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
|
|
sudo ln -sf /etc/systemd/system/auto-shutdown.service \ |
|
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service" |
|
} |
|
|
|
start_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container path not found: $container_path" |
|
exit 1 |
|
fi |
|
|
|
# Check if already running via machinectl |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_warn "Container '$name' is already running." |
|
return |
|
fi |
|
|
|
log_info "Starting $name..." |
|
|
|
# 1. Config Bridge |
|
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name. |
|
# This is critical for applying settings when we start manually. |
|
sudo mkdir -p /run/systemd/nspawn |
|
if [ -f "$config_file" ]; then |
|
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
|
|
# 2. Start Service |
|
# We use systemd-run to launch nspawn as a transient service. |
|
# We explicitly point -D to our custom path, bypassing the need for /var/lib/machines. |
|
|
|
# Cleanup any failed previous units |
|
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true |
|
|
|
sudo systemd-run \ |
|
--unit="spawn-$name" \ |
|
--description="Spawn-Hybrid Container $name" \ |
|
systemd-nspawn --machine="$name" -D "$container_path" --boot |
|
|
|
# 3. Verify |
|
# Poll machinectl to confirm the container has registered successfully. |
|
local attempts=0 |
|
while [ $attempts -lt 5 ]; do |
|
sleep 1 |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_info "Container booted successfully." |
|
return |
|
fi |
|
attempts=$((attempts+1)) |
|
done |
|
|
|
log_warn "Container startup queued. Check status with: systemctl status spawn-$name" |
|
} |
|
|
|
stop_container() { |
|
local name=$1 |
|
log_info "Stopping $name..." |
|
sudo machinectl poweroff "$name" |
|
|
|
# Cleanup config link from the run directory |
|
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then |
|
sudo rm "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
} |
|
|
|
shell_container() { |
|
local name=$1 |
|
# Check if running; if not, start it and wait briefly for it to boot. |
|
if ! machinectl list 2>/dev/null | grep -q "^$name "; then |
|
start_container "$name" |
|
sleep 2 |
|
fi |
|
|
|
log_sec "Entering sandbox as user 'agent'..." |
|
|
|
# Enter the container via machinectl shell. |
|
# Use /usr/bin/env to set variables, as machinectl shell |
|
# does not accept --setenv flags directly. |
|
sudo machinectl shell "agent@$name" \ |
|
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash |
|
} |
|
|
|
delete_container() { |
|
local name=$1 |
|
local path="$CONTAINER_BASE/$name" |
|
|
|
if [ ! -d "$path" ]; then |
|
log_error "Not found." |
|
exit 1 |
|
fi |
|
|
|
echo -e "${RED}DANGER: Deleting $name${NC}" |
|
echo -e "This will wipe: $path" |
|
read -p "Type 'DELETE' to confirm: " confirm |
|
|
|
if [ "$confirm" == "DELETE" ]; then |
|
# Force terminate if running |
|
if machinectl list | grep -q "^$name "; then |
|
sudo machinectl terminate "$name" 2>/dev/null || true |
|
fi |
|
# Remove filesystem and all associated metadata/configs |
|
sudo rm -rf "$path" |
|
rm -f "$CONTAINER_CONFIG/$name.nspawn" |
|
rm -f "$CONTAINER_CONFIG/.$name.pwd" |
|
rm -f "$CONTAINER_CONFIG/.$name.meta" |
|
log_info "Deleted." |
|
else |
|
log_info "Cancelled." |
|
fi |
|
} |
|
|
|
list_containers() { |
|
echo -e "${BLUE}CONTAINERS:${NC}" |
|
# Iterate over generated metadata files to display inventory |
|
for meta in "$CONTAINER_CONFIG"/.*.meta; do |
|
[ -e "$meta" ] || continue |
|
source "$meta" |
|
local status="STOPPED" |
|
if machinectl list 2>/dev/null | grep -q "^$NAME "; then |
|
status="${GREEN}RUNNING${NC}" |
|
fi |
|
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE" |
|
done |
|
} |
|
|
|
info_container() { |
|
local name=$1 |
|
local meta="$CONTAINER_CONFIG/.$name.meta" |
|
local pwd="$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
if [ ! -f "$meta" ]; then |
|
log_error "No metadata found for $name" |
|
exit 1 |
|
fi |
|
|
|
source "$meta" |
|
echo "--------------------------------" |
|
echo "Container: $NAME" |
|
echo "Created: $CREATED" |
|
echo "Network: $NET_ENABLED" |
|
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)" |
|
echo "--------------------------------" |
|
if [ -f "$pwd" ]; then |
|
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}" |
|
fi |
|
echo "--------------------------------" |
|
} |
|
|
|
# --- Main Dispatch --- |
|
|
|
check_dependencies |
|
setup_directories |
|
|
|
# Extract main command, defaulting to 'help' |
|
CMD="${1:-help}" |
|
shift || true |
|
|
|
# Route the execution based on the main command |
|
case "$CMD" in |
|
-h|--help) show_help ;; |
|
create) create_container "$@" ;; |
|
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;; |
|
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;; |
|
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;; |
|
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;; |
|
list) list_containers ;; |
|
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;; |
|
*) show_help ;; |
|
esac |
|
|
|
|
|
check_dependencies() { |
|
local missing=() |
|
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do |
|
if ! command -v "$cmd" &> /dev/null; then |
|
missing+=("$cmd") |
|
fi |
|
done |
|
if [ ${#missing[@]} -gt 0 ]; then |
|
log_error "Missing dependencies: ${missing[*]}" |
|
exit 1 |
|
fi |
|
} |
|
|
|
# --- Initialization --- |
|
# Setup necessary directory structures in the host environment. |
|
setup_directories() { |
|
mkdir -p "$CONTAINER_BASE" |
|
mkdir -p "$CONTAINER_CONFIG" |
|
mkdir -p "$CONTAINER_LOGS" |
|
chmod 700 "$CONTAINER_LOGS" |
|
} |
|
|
|
# --- Help / Usage --- |
|
show_help() { |
|
cat << EOF |
|
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options] |
|
|
|
Commands: |
|
create <name> [source] [dest] Create a secure container |
|
--source, -s Host directory to mount (e.g., ~/projects) |
|
--dest, -d Mount point inside container (default: /mnt/work) |
|
--rw Mount as Read-Write (Default is Read-Only) |
|
--cpu CPU Quota (default: 50%) |
|
--memory Memory Limit (default: 2G) |
|
--network Enable networking (Default: disabled/isolated) |
|
--timeout Auto-shutdown in seconds (default: 3600) |
|
--packages Extra packages to install (comma separated) |
|
|
|
start <name> Start container |
|
stop <name> Stop container |
|
shell <name> Open secure shell |
|
delete <name> Destroy container |
|
list Show inventory |
|
info <name> Show details & password |
|
-h, --help Show this help message |
|
EOF |
|
} |
|
|
|
# --- Core Functions --- |
|
|
|
create_container() { |
|
# Fail early if no container name is provided. |
|
if [ -z "${1:-}" ]; then |
|
log_error "Container name required." |
|
echo "" |
|
show_help |
|
exit 1 |
|
fi |
|
|
|
# Catch help flag passed as the first argument |
|
if [[ "$1" == "-h" || "$1" == "--help" ]]; then |
|
show_help |
|
exit 0 |
|
fi |
|
|
|
local name="$1" |
|
shift |
|
|
|
# Defaults |
|
local mount_src="" |
|
local mount_dst="" |
|
local mount_mode="ro" |
|
local cpu_quota="$DEFAULT_CPU_QUOTA" |
|
local mem_limit="$DEFAULT_MEM_LIMIT" |
|
local enable_net="false" |
|
local timeout="3600" |
|
local extra_pkgs="" |
|
|
|
# Argument Parsing (Hybrid: Flags or Positional) |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-s|--source) mount_src="$2"; shift 2 ;; |
|
-d|--dest) mount_dst="$2"; shift 2 ;; |
|
--rw) mount_mode="rw"; shift ;; |
|
--cpu) cpu_quota="$2"; shift 2 ;; |
|
--memory) mem_limit="$2"; shift 2 ;; |
|
--network) enable_net="true"; shift ;; |
|
--timeout) timeout="$2"; shift 2 ;; |
|
--packages) extra_pkgs="$2"; shift 2 ;; |
|
-h|--help) show_help; exit 0 ;; |
|
-*) log_error "Unknown option: $1"; exit 1 ;; |
|
*) |
|
# Handle positional arguments: [source] [dest] |
|
if [ -z "$mount_src" ]; then |
|
mount_src="$1" |
|
elif [ -z "$mount_dst" ]; then |
|
mount_dst="$1" |
|
else |
|
log_error "Too many arguments. Unexpected: $1" |
|
exit 1 |
|
fi |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
# Default container mount destination if none is specified |
|
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi |
|
|
|
local container_path="$CONTAINER_BASE/$name" |
|
|
|
if [ -d "$container_path" ]; then |
|
log_error "Container '$name' already exists." |
|
exit 1 |
|
fi |
|
|
|
# 1. Security Validation |
|
# Ensure mounted host directories are not sensitive system paths. |
|
if [ -n "$mount_src" ]; then |
|
mount_src=$(realpath "$mount_src") |
|
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root") |
|
for blocked in "${blocked_paths[@]}"; do |
|
if [[ "$mount_src" == "$blocked"* ]]; then |
|
log_error "Security Block: Mounting '$mount_src' is forbidden." |
|
exit 1 |
|
fi |
|
done |
|
if [ ! -d "$mount_src" ]; then |
|
log_error "Source directory does not exist: $mount_src" |
|
exit 1 |
|
fi |
|
fi |
|
|
|
log_info "Initializing container '$name'..." |
|
mkdir -p "$container_path" |
|
|
|
# 2. Bootstrap |
|
# Install Arch Linux base system and required packages. |
|
log_info "Bootstrapping Arch Linux..." |
|
local pkg_list="base python python-pip git ${extra_pkgs//,/ }" |
|
|
|
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then |
|
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log" |
|
sudo rm -rf "$container_path" |
|
exit 1 |
|
fi |
|
|
|
# 3. Generate Credentials |
|
# Create a secure, random password for the container users. |
|
local agent_pass |
|
agent_pass=$(openssl rand -base64 24) |
|
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd" |
|
chmod 600 "$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
# 4. Configure User & Network |
|
# Set the hostname inside the container. |
|
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null |
|
|
|
# Configure networking if enabled via flags. |
|
if [ "$enable_net" == "true" ]; then |
|
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF |
|
[Match] |
|
Name=host0 |
|
[Network] |
|
DHCP=yes |
|
EOF |
|
else |
|
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network" |
|
fi |
|
|
|
# Enable systemd-resolved and systemd-networkd for DNS and DHCP. |
|
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service" |
|
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service" |
|
|
|
# Setup the 'agent' user and set the root password for su/sudo access. |
|
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent |
|
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
|
|
# Add agent to the wheel group if sudo was installed. |
|
if [[ "$pkg_list" == *"sudo"* ]]; then |
|
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent |
|
fi |
|
|
|
# 5. Create Secure Config (.nspawn) |
|
# Generate the systemd-nspawn profile defining sandboxing rules. |
|
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn" |
|
local mem_bytes |
|
mem_bytes=$(numfmt --from=iec "$mem_limit") |
|
|
|
cat > "$nspawn_file" <<EOF |
|
[Exec] |
|
Boot=yes |
|
# Safely map container root to unprivileged host user |
|
PrivateUsers=pick |
|
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD |
|
ProcessTwo=yes |
|
# Removed NoNewPrivileges=yes to allow su/sudo functionality |
|
|
|
[Network] |
|
VirtualEthernet=yes |
|
Private=yes |
|
|
|
[Files] |
|
PrivateUsersOwnership=auto |
|
ProtectSystem=strict |
|
ProtectHome=read-only |
|
PrivateDevices=yes |
|
ProtectKernelTunables=yes |
|
ProtectControlGroups=yes |
|
LockPersonality=yes |
|
|
|
[CPU] |
|
CPUQuota=$cpu_quota |
|
|
|
[Memory] |
|
MemoryMax=$mem_bytes |
|
EOF |
|
|
|
# Configure directory binds based on provided flags. |
|
if [ -n "$mount_src" ]; then |
|
if [ "$mount_mode" == "rw" ]; then |
|
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file" |
|
else |
|
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file" |
|
fi |
|
fi |
|
|
|
echo "Bind=/home/agent" >> "$nspawn_file" |
|
|
|
# 6. Auto-Shutdown Timer |
|
setup_shutdown_timer "$container_path" "$timeout" |
|
|
|
# 7. Metadata |
|
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF |
|
NAME=$name |
|
CREATED=$(date -Iseconds) |
|
MOUNT_SRC=$mount_src |
|
MOUNT_MODE=$mount_mode |
|
NET_ENABLED=$enable_net |
|
EOF |
|
|
|
log_sec "Container '$name' created." |
|
log_info "Password saved to secure vault." |
|
} |
|
|
|
# --- Auto-Shutdown Timer --- |
|
# Create a systemd service inside the container to power it off after a timeout. |
|
setup_shutdown_timer() { |
|
local path="$1" |
|
local timeout="$2" |
|
|
|
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null |
|
#!/bin/bash |
|
sleep $timeout |
|
echo "Time limit reached. Shutting down." |
|
poweroff |
|
EOF |
|
sudo chmod +x "$path/usr/local/bin/auto-shutdown" |
|
|
|
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null |
|
[Unit] |
|
Description=Security Timeout |
|
After=multi-user.target |
|
|
|
[Service] |
|
ExecStart=/usr/local/bin/auto-shutdown |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
|
|
sudo ln -sf /etc/systemd/system/auto-shutdown.service \ |
|
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service" |
|
} |
|
|
|
start_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container path not found: $container_path" |
|
exit 1 |
|
fi |
|
|
|
# Check if already running via machinectl |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_warn "Container '$name' is already running." |
|
return |
|
fi |
|
|
|
log_info "Starting $name..." |
|
|
|
# 1. Config Bridge |
|
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name. |
|
# This is critical for applying settings when we start manually. |
|
sudo mkdir -p /run/systemd/nspawn |
|
if [ -f "$config_file" ]; then |
|
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
|
|
# 2. Start Service |
|
# We use systemd-run to launch nspawn as a transient service. |
|
# We explicitly point -D to our custom path, bypassing the need for /var/lib/machines. |
|
|
|
# Cleanup any failed previous units |
|
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true |
|
|
|
sudo systemd-run \ |
|
--unit="spawn-$name" \ |
|
--description="Spawn-Hybrid Container $name" \ |
|
systemd-nspawn --machine="$name" -D "$container_path" --boot |
|
|
|
# 3. Verify |
|
# Poll machinectl to confirm the container has registered successfully. |
|
local attempts=0 |
|
while [ $attempts -lt 5 ]; do |
|
sleep 1 |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_info "Container booted successfully." |
|
return |
|
fi |
|
attempts=$((attempts+1)) |
|
done |
|
|
|
log_warn "Container startup queued. Check status with: systemctl status spawn-$name" |
|
} |
|
|
|
stop_container() { |
|
local name=$1 |
|
log_info "Stopping $name..." |
|
sudo machinectl poweroff "$name" |
|
|
|
# Cleanup config link from the run directory |
|
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then |
|
sudo rm "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
} |
|
|
|
shell_container() { |
|
local name=$1 |
|
# Check if running; if not, start it and wait briefly for it to boot. |
|
if ! machinectl list 2>/dev/null | grep -q "^$name "; then |
|
start_container "$name" |
|
sleep 2 |
|
fi |
|
|
|
log_sec "Entering sandbox as user 'agent'..." |
|
|
|
# Enter the container via machinectl shell. |
|
# Use /usr/bin/env to set variables, as machinectl shell |
|
# does not accept --setenv flags directly. |
|
sudo machinectl shell "agent@$name" \ |
|
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash |
|
} |
|
|
|
delete_container() { |
|
local name=$1 |
|
local path="$CONTAINER_BASE/$name" |
|
|
|
if [ ! -d "$path" ]; then |
|
log_error "Not found." |
|
exit 1 |
|
fi |
|
|
|
echo -e "${RED}DANGER: Deleting $name${NC}" |
|
echo -e "This will wipe: $path" |
|
read -p "Type 'DELETE' to confirm: " confirm |
|
|
|
if [ "$confirm" == "DELETE" ]; then |
|
# Force terminate if running |
|
if machinectl list | grep -q "^$name "; then |
|
sudo machinectl terminate "$name" 2>/dev/null || true |
|
fi |
|
# Remove filesystem and all associated metadata/configs |
|
sudo rm -rf "$path" |
|
rm -f "$CONTAINER_CONFIG/$name.nspawn" |
|
rm -f "$CONTAINER_CONFIG/.$name.pwd" |
|
rm -f "$CONTAINER_CONFIG/.$name.meta" |
|
log_info "Deleted." |
|
else |
|
log_info "Cancelled." |
|
fi |
|
} |
|
|
|
list_containers() { |
|
echo -e "${BLUE}CONTAINERS:${NC}" |
|
# Iterate over generated metadata files to display inventory |
|
for meta in "$CONTAINER_CONFIG"/.*.meta; do |
|
[ -e "$meta" ] || continue |
|
source "$meta" |
|
local status="STOPPED" |
|
if machinectl list 2>/dev/null | grep -q "^$NAME "; then |
|
status="${GREEN}RUNNING${NC}" |
|
fi |
|
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE" |
|
done |
|
} |
|
|
|
info_container() { |
|
local name=$1 |
|
local meta="$CONTAINER_CONFIG/.$name.meta" |
|
local pwd="$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
if [ ! -f "$meta" ]; then |
|
log_error "No metadata found for $name" |
|
exit 1 |
|
fi |
|
|
|
source "$meta" |
|
echo "--------------------------------" |
|
echo "Container: $NAME" |
|
echo "Created: $CREATED" |
|
echo "Network: $NET_ENABLED" |
|
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)" |
|
echo "--------------------------------" |
|
if [ -f "$pwd" ]; then |
|
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}" |
|
fi |
|
echo "--------------------------------" |
|
} |
|
|
|
# --- Main Dispatch --- |
|
|
|
check_dependencies |
|
setup_directories |
|
|
|
# Extract main command, defaulting to 'help' |
|
CMD="${1:-help}" |
|
shift || true |
|
|
|
# Route the execution based on the main command |
|
case "$CMD" in |
|
-h|--help) show_help ;; |
|
create) create_container "$@" ;; |
|
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;; |
|
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;; |
|
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;; |
|
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;; |
|
list) list_containers ;; |
|
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;; |
|
*) show_help ;; |
|
esac |
|
check_dependencies() { |
|
local missing=() |
|
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do |
|
if ! command -v "$cmd" &> /dev/null; then |
|
missing+=("$cmd") |
|
fi |
|
done |
|
if [ ${#missing[@]} -gt 0 ]; then |
|
log_error "Missing dependencies: ${missing[*]}" |
|
exit 1 |
|
fi |
|
} |
|
|
|
# --- Initialization --- |
|
# Setup necessary directory structures in the host environment. |
|
setup_directories() { |
|
mkdir -p "$CONTAINER_BASE" |
|
mkdir -p "$CONTAINER_CONFIG" |
|
mkdir -p "$CONTAINER_LOGS" |
|
chmod 700 "$CONTAINER_LOGS" |
|
} |
|
|
|
# --- Help / Usage --- |
|
show_help() { |
|
cat << EOF |
|
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options] |
|
|
|
Commands: |
|
create <name> [source] [dest] Create a secure container |
|
--source, -s Host directory to mount (e.g., ~/projects) |
|
--dest, -d Mount point inside container (default: /mnt/work) |
|
--rw Mount as Read-Write (Default is Read-Only) |
|
--cpu CPU Quota (default: 50%) |
|
--memory Memory Limit (default: 2G) |
|
--network Enable networking (Default: disabled/isolated) |
|
--timeout Auto-shutdown in seconds (default: 3600) |
|
--packages Extra packages to install (comma separated) |
|
|
|
start <name> Start container |
|
stop <name> Stop container |
|
shell <name> Open secure shell |
|
delete <name> Destroy container |
|
list Show inventory |
|
info <name> Show details & password |
|
-h, --help Show this help message |
|
EOF |
|
} |
|
|
|
# --- Core Functions --- |
|
|
|
create_container() { |
|
# Fail early if no container name is provided. |
|
if [ -z "${1:-}" ]; then |
|
log_error "Container name required." |
|
echo "" |
|
show_help |
|
exit 1 |
|
fi |
|
|
|
# Catch help flag passed as the first argument |
|
if [[ "$1" == "-h" || "$1" == "--help" ]]; then |
|
show_help |
|
exit 0 |
|
fi |
|
|
|
local name="$1" |
|
shift |
|
|
|
# Defaults |
|
local mount_src="" |
|
local mount_dst="" |
|
local mount_mode="ro" |
|
local cpu_quota="$DEFAULT_CPU_QUOTA" |
|
local mem_limit="$DEFAULT_MEM_LIMIT" |
|
local enable_net="false" |
|
local timeout="3600" |
|
local extra_pkgs="" |
|
|
|
# Argument Parsing (Hybrid: Flags or Positional) |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-s|--source) mount_src="$2"; shift 2 ;; |
|
-d|--dest) mount_dst="$2"; shift 2 ;; |
|
--rw) mount_mode="rw"; shift ;; |
|
--cpu) cpu_quota="$2"; shift 2 ;; |
|
--memory) mem_limit="$2"; shift 2 ;; |
|
--network) enable_net="true"; shift ;; |
|
--timeout) timeout="$2"; shift 2 ;; |
|
--packages) extra_pkgs="$2"; shift 2 ;; |
|
-h|--help) show_help; exit 0 ;; |
|
-*) log_error "Unknown option: $1"; exit 1 ;; |
|
*) |
|
# Handle positional arguments: [source] [dest] |
|
if [ -z "$mount_src" ]; then |
|
mount_src="$1" |
|
elif [ -z "$mount_dst" ]; then |
|
mount_dst="$1" |
|
else |
|
log_error "Too many arguments. Unexpected: $1" |
|
exit 1 |
|
fi |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
# Default container mount destination if none is specified |
|
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi |
|
|
|
local container_path="$CONTAINER_BASE/$name" |
|
|
|
if [ -d "$container_path" ]; then |
|
log_error "Container '$name' already exists." |
|
exit 1 |
|
fi |
|
|
|
# 1. Security Validation |
|
# Ensure mounted host directories are not sensitive system paths. |
|
if [ -n "$mount_src" ]; then |
|
mount_src=$(realpath "$mount_src") |
|
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root") |
|
for blocked in "${blocked_paths[@]}"; do |
|
if [[ "$mount_src" == "$blocked"* ]]; then |
|
log_error "Security Block: Mounting '$mount_src' is forbidden." |
|
exit 1 |
|
fi |
|
done |
|
if [ ! -d "$mount_src" ]; then |
|
log_error "Source directory does not exist: $mount_src" |
|
exit 1 |
|
fi |
|
fi |
|
|
|
log_info "Initializing container '$name'..." |
|
mkdir -p "$container_path" |
|
|
|
# 2. Bootstrap |
|
# Install Arch Linux base system and required packages. |
|
log_info "Bootstrapping Arch Linux..." |
|
local pkg_list="base python python-pip git ${extra_pkgs//,/ }" |
|
|
|
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then |
|
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log" |
|
sudo rm -rf "$container_path" |
|
exit 1 |
|
fi |
|
|
|
# 3. Generate Credentials |
|
# Create a secure, random password for the container users. |
|
local agent_pass |
|
agent_pass=$(openssl rand -base64 24) |
|
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd" |
|
chmod 600 "$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
# 4. Configure User & Network |
|
# Set the hostname inside the container. |
|
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null |
|
|
|
# Configure networking if enabled via flags. |
|
if [ "$enable_net" == "true" ]; then |
|
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF |
|
[Match] |
|
Name=host0 |
|
[Network] |
|
DHCP=yes |
|
EOF |
|
else |
|
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network" |
|
fi |
|
|
|
# Enable systemd-resolved and systemd-networkd for DNS and DHCP. |
|
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service" |
|
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service" |
|
|
|
# Setup the 'agent' user and set the root password for su/sudo access. |
|
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent |
|
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
|
|
# Add agent to the wheel group if sudo was installed. |
|
if [[ "$pkg_list" == *"sudo"* ]]; then |
|
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent |
|
fi |
|
|
|
# 5. Create Secure Config (.nspawn) |
|
# Generate the systemd-nspawn profile defining sandboxing rules. |
|
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn" |
|
local mem_bytes |
|
mem_bytes=$(numfmt --from=iec "$mem_limit") |
|
|
|
cat > "$nspawn_file" <<EOF |
|
[Exec] |
|
Boot=yes |
|
# Safely map container root to unprivileged host user |
|
PrivateUsers=pick |
|
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD |
|
ProcessTwo=yes |
|
# Removed NoNewPrivileges=yes to allow su/sudo functionality |
|
|
|
[Network] |
|
VirtualEthernet=yes |
|
Private=yes |
|
|
|
[Files] |
|
PrivateUsersOwnership=auto |
|
ProtectSystem=strict |
|
ProtectHome=read-only |
|
PrivateDevices=yes |
|
ProtectKernelTunables=yes |
|
ProtectControlGroups=yes |
|
LockPersonality=yes |
|
|
|
[CPU] |
|
CPUQuota=$cpu_quota |
|
|
|
[Memory] |
|
MemoryMax=$mem_bytes |
|
EOF |
|
|
|
# Configure directory binds based on provided flags. |
|
if [ -n "$mount_src" ]; then |
|
if [ "$mount_mode" == "rw" ]; then |
|
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file" |
|
else |
|
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file" |
|
fi |
|
fi |
|
|
|
echo "Bind=/home/agent" >> "$nspawn_file" |
|
|
|
# 6. Auto-Shutdown Timer |
|
setup_shutdown_timer "$container_path" "$timeout" |
|
|
|
# 7. Metadata |
|
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF |
|
NAME=$name |
|
CREATED=$(date -Iseconds) |
|
MOUNT_SRC=$mount_src |
|
MOUNT_MODE=$mount_mode |
|
NET_ENABLED=$enable_net |
|
EOF |
|
|
|
log_sec "Container '$name' created." |
|
log_info "Password saved to secure vault." |
|
} |
|
|
|
# --- Auto-Shutdown Timer --- |
|
# Create a systemd service inside the container to power it off after a timeout. |
|
setup_shutdown_timer() { |
|
local path="$1" |
|
local timeout="$2" |
|
|
|
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null |
|
#!/bin/bash |
|
sleep $timeout |
|
echo "Time limit reached. Shutting down." |
|
poweroff |
|
EOF |
|
sudo chmod +x "$path/usr/local/bin/auto-shutdown" |
|
|
|
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null |
|
[Unit] |
|
Description=Security Timeout |
|
After=multi-user.target |
|
|
|
[Service] |
|
ExecStart=/usr/local/bin/auto-shutdown |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
|
|
sudo ln -sf /etc/systemd/system/auto-shutdown.service \ |
|
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service" |
|
} |
|
|
|
start_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container path not found: $container_path" |
|
exit 1 |
|
fi |
|
|
|
# Check if already running via machinectl |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_warn "Container '$name' is already running." |
|
return |
|
fi |
|
|
|
log_info "Starting $name..." |
|
|
|
# 1. Config Bridge |
|
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name. |
|
# This is critical for applying settings when we start manually. |
|
sudo mkdir -p /run/systemd/nspawn |
|
if [ -f "$config_file" ]; then |
|
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
|
|
# 2. Start Service |
|
# We use systemd-run to launch nspawn as a transient service. |
|
# We explicitly point -D to our custom path, bypassing the need for /var/lib/machines. |
|
|
|
# Cleanup any failed previous units |
|
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true |
|
|
|
sudo systemd-run \ |
|
--unit="spawn-$name" \ |
|
--description="Spawn-Hybrid Container $name" \ |
|
systemd-nspawn --machine="$name" -D "$container_path" --boot |
|
|
|
# 3. Verify |
|
# Poll machinectl to confirm the container has registered successfully. |
|
local attempts=0 |
|
while [ $attempts -lt 5 ]; do |
|
sleep 1 |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_info "Container booted successfully." |
|
return |
|
fi |
|
attempts=$((attempts+1)) |
|
done |
|
|
|
log_warn "Container startup queued. Check status with: systemctl status spawn-$name" |
|
} |
|
|
|
stop_container() { |
|
local name=$1 |
|
log_info "Stopping $name..." |
|
sudo machinectl poweroff "$name" |
|
|
|
# Cleanup config link from the run directory |
|
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then |
|
sudo rm "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
} |
|
|
|
shell_container() { |
|
local name=$1 |
|
# Check if running; if not, start it and wait briefly for it to boot. |
|
if ! machinectl list 2>/dev/null | grep -q "^$name "; then |
|
start_container "$name" |
|
sleep 2 |
|
fi |
|
|
|
log_sec "Entering sandbox as user 'agent'..." |
|
|
|
# Enter the container via machinectl shell. |
|
# Use /usr/bin/env to set variables, as machinectl shell |
|
# does not accept --setenv flags directly. |
|
sudo machinectl shell "agent@$name" \ |
|
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash |
|
} |
|
|
|
delete_container() { |
|
local name=$1 |
|
local path="$CONTAINER_BASE/$name" |
|
|
|
if [ ! -d "$path" ]; then |
|
log_error "Not found." |
|
exit 1 |
|
fi |
|
|
|
echo -e "${RED}DANGER: Deleting $name${NC}" |
|
echo -e "This will wipe: $path" |
|
read -p "Type 'DELETE' to confirm: " confirm |
|
|
|
if [ "$confirm" == "DELETE" ]; then |
|
# Force terminate if running |
|
if machinectl list | grep -q "^$name "; then |
|
sudo machinectl terminate "$name" 2>/dev/null || true |
|
fi |
|
# Remove filesystem and all associated metadata/configs |
|
sudo rm -rf "$path" |
|
rm -f "$CONTAINER_CONFIG/$name.nspawn" |
|
rm -f "$CONTAINER_CONFIG/.$name.pwd" |
|
rm -f "$CONTAINER_CONFIG/.$name.meta" |
|
log_info "Deleted." |
|
else |
|
log_info "Cancelled." |
|
fi |
|
} |
|
|
|
list_containers() { |
|
echo -e "${BLUE}CONTAINERS:${NC}" |
|
# Iterate over generated metadata files to display inventory |
|
for meta in "$CONTAINER_CONFIG"/.*.meta; do |
|
[ -e "$meta" ] || continue |
|
source "$meta" |
|
local status="STOPPED" |
|
if machinectl list 2>/dev/null | grep -q "^$NAME "; then |
|
status="${GREEN}RUNNING${NC}" |
|
fi |
|
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE" |
|
done |
|
} |
|
|
|
info_container() { |
|
local name=$1 |
|
local meta="$CONTAINER_CONFIG/.$name.meta" |
|
local pwd="$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
if [ ! -f "$meta" ]; then |
|
log_error "No metadata found for $name" |
|
exit 1 |
|
fi |
|
|
|
source "$meta" |
|
echo "--------------------------------" |
|
echo "Container: $NAME" |
|
echo "Created: $CREATED" |
|
echo "Network: $NET_ENABLED" |
|
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)" |
|
echo "--------------------------------" |
|
if [ -f "$pwd" ]; then |
|
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}" |
|
fi |
|
echo "--------------------------------" |
|
} |
|
|
|
# --- Main Dispatch --- |
|
|
|
check_dependencies |
|
setup_directories |
|
|
|
# Extract main command, defaulting to 'help' |
|
CMD="${1:-help}" |
|
shift || true |
|
|
|
# Route the execution based on the main command |
|
case "$CMD" in |
|
-h|--help) show_help ;; |
|
create) create_container "$@" ;; |
|
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;; |
|
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;; |
|
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;; |
|
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;; |
|
list) list_containers ;; |
|
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;; |
|
*) show_help ;; |
|
esac |
|
cat << EOF |
|
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options] |
|
|
|
Commands: |
|
create <name> [source] [dest] Create a secure container |
|
--source, -s Host directory to mount (e.g., ~/projects) |
|
--dest, -d Mount point inside container (default: /mnt/work) |
|
--rw Mount as Read-Write (Default is Read-Only) |
|
--cpu CPU Quota (default: 50%) |
|
--memory Memory Limit (default: 2G) |
|
--network Enable networking (Default: disabled/isolated) |
|
--timeout Auto-shutdown in seconds (default: 3600) |
|
--packages Extra packages to install (comma separated) |
|
|
|
start <name> Start container |
|
stop <name> Stop container |
|
shell <name> Open secure shell |
|
delete <name> Destroy container |
|
list Show inventory |
|
info <name> Show details & password |
|
EOF |
|
} |
|
|
|
# --- Core Functions --- |
|
|
|
create_container() { |
|
local name="$1" |
|
shift |
|
|
|
# Defaults |
|
local mount_src="" |
|
local mount_dst="" |
|
local mount_mode="ro" |
|
local cpu_quota="$DEFAULT_CPU_QUOTA" |
|
local mem_limit="$DEFAULT_MEM_LIMIT" |
|
local enable_net="false" |
|
local timeout="3600" |
|
local extra_pkgs="" |
|
|
|
# Argument Parsing (Hybrid: Flags or Positional) |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-s|--source) mount_src="$2"; shift 2 ;; |
|
-d|--dest) mount_dst="$2"; shift 2 ;; |
|
--rw) mount_mode="rw"; shift ;; |
|
--cpu) cpu_quota="$2"; shift 2 ;; |
|
--memory) mem_limit="$2"; shift 2 ;; |
|
--network) enable_net="true"; shift ;; |
|
--timeout) timeout="$2"; shift 2 ;; |
|
--packages) extra_pkgs="$2"; shift 2 ;; |
|
-*) log_error "Unknown option: $1"; exit 1 ;; |
|
*) |
|
# Handle positional arguments: [source] [dest] |
|
if [ -z "$mount_src" ]; then |
|
mount_src="$1" |
|
elif [ -z "$mount_dst" ]; then |
|
mount_dst="$1" |
|
else |
|
log_error "Too many arguments. Unexpected: $1" |
|
exit 1 |
|
fi |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi |
|
|
|
local container_path="$CONTAINER_BASE/$name" |
|
|
|
if [ -d "$container_path" ]; then |
|
log_error "Container '$name' already exists." |
|
exit 1 |
|
fi |
|
|
|
# 1. Security Validation |
|
if [ -n "$mount_src" ]; then |
|
mount_src=$(realpath "$mount_src") |
|
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root") |
|
for blocked in "${blocked_paths[@]}"; do |
|
if [[ "$mount_src" == "$blocked"* ]]; then |
|
log_error "Security Block: Mounting '$mount_src' is forbidden." |
|
exit 1 |
|
fi |
|
done |
|
if [ ! -d "$mount_src" ]; then |
|
log_error "Source directory does not exist: $mount_src" |
|
exit 1 |
|
fi |
|
fi |
|
|
|
log_info "Initializing container '$name'..." |
|
mkdir -p "$container_path" |
|
|
|
# 2. Bootstrap |
|
log_info "Bootstrapping Arch Linux..." |
|
local pkg_list="base python python-pip git ${extra_pkgs//,/ }" |
|
|
|
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then |
|
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log" |
|
sudo rm -rf "$container_path" |
|
exit 1 |
|
fi |
|
|
|
# 3. Generate Credentials |
|
local agent_pass |
|
agent_pass=$(openssl rand -base64 24) |
|
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd" |
|
chmod 600 "$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
# 4. Configure User & Network |
|
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null |
|
|
|
if [ "$enable_net" == "true" ]; then |
|
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF |
|
[Match] |
|
Name=host0 |
|
[Network] |
|
DHCP=yes |
|
EOF |
|
else |
|
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network" |
|
fi |
|
|
|
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service" |
|
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service" |
|
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent |
|
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
|
|
if [[ "$pkg_list" == *"sudo"* ]]; then |
|
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent |
|
fi |
|
|
|
# 5. Create Secure Config (.nspawn) |
|
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn" |
|
local mem_bytes |
|
mem_bytes=$(numfmt --from=iec "$mem_limit") |
|
|
|
cat > "$nspawn_file" <<EOF |
|
[Exec] |
|
Boot=yes |
|
PrivateUsers=pick |
|
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD |
|
ProcessTwo=yes |
|
|
|
[Network] |
|
VirtualEthernet=yes |
|
Private=yes |
|
|
|
[Files] |
|
PrivateUsersOwnership=auto |
|
ProtectSystem=strict |
|
ProtectHome=read-only |
|
PrivateDevices=yes |
|
ProtectKernelTunables=yes |
|
ProtectControlGroups=yes |
|
LockPersonality=yes |
|
|
|
[CPU] |
|
CPUQuota=$cpu_quota |
|
|
|
[Memory] |
|
MemoryMax=$mem_bytes |
|
EOF |
|
|
|
if [ -n "$mount_src" ]; then |
|
if [ "$mount_mode" == "rw" ]; then |
|
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file" |
|
else |
|
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file" |
|
fi |
|
fi |
|
|
|
echo "Bind=/home/agent" >> "$nspawn_file" |
|
|
|
# 6. Auto-Shutdown Timer |
|
setup_shutdown_timer "$container_path" "$timeout" |
|
|
|
# 7. Metadata |
|
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF |
|
NAME=$name |
|
CREATED=$(date -Iseconds) |
|
MOUNT_SRC=$mount_src |
|
MOUNT_MODE=$mount_mode |
|
NET_ENABLED=$enable_net |
|
EOF |
|
|
|
log_sec "Container '$name' created." |
|
log_info "Password saved to secure vault." |
|
} |
|
|
|
setup_shutdown_timer() { |
|
local path="$1" |
|
local timeout="$2" |
|
|
|
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null |
|
#!/bin/bash |
|
sleep $timeout |
|
echo "Time limit reached. Shutting down." |
|
poweroff |
|
EOF |
|
sudo chmod +x "$path/usr/local/bin/auto-shutdown" |
|
|
|
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null |
|
[Unit] |
|
Description=Security Timeout |
|
After=multi-user.target |
|
|
|
[Service] |
|
ExecStart=/usr/local/bin/auto-shutdown |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
|
|
sudo ln -sf /etc/systemd/system/auto-shutdown.service \ |
|
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service" |
|
} |
|
|
|
start_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container path not found: $container_path" |
|
exit 1 |
|
fi |
|
|
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_warn "Container '$name' is already running." |
|
return |
|
fi |
|
|
|
log_info "Starting $name..." |
|
|
|
# 1. Config Bridge |
|
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name |
|
sudo mkdir -p /run/systemd/nspawn |
|
if [ -f "$config_file" ]; then |
|
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
|
|
# 2. Start Service |
|
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true |
|
|
|
# We use systemd-run to launch nspawn as a transient service. |
|
sudo systemd-run \ |
|
--unit="spawn-$name" \ |
|
--description="Spawn-Hybrid Container $name" \ |
|
systemd-nspawn --machine="$name" -D "$container_path" --boot |
|
|
|
# 3. Verify |
|
local attempts=0 |
|
while [ $attempts -lt 5 ]; do |
|
sleep 1 |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_info "Container booted successfully." |
|
return |
|
fi |
|
attempts=$((attempts+1)) |
|
done |
|
|
|
log_warn "Container startup queued. Check status with: systemctl status spawn-$name" |
|
} |
|
|
|
stop_container() { |
|
local name=$1 |
|
log_info "Stopping $name..." |
|
sudo machinectl poweroff "$name" |
|
|
|
# Cleanup config link |
|
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then |
|
sudo rm "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
} |
|
|
|
shell_container() { |
|
local name=$1 |
|
if ! machinectl list | grep -q "^$name "; then |
|
start_container "$name" |
|
sleep 2 |
|
fi |
|
log_sec "Entering sandbox as user 'agent'..." |
|
|
|
# FIX: Use /usr/bin/env to set variables. |
|
# machinectl shell interprets arguments as command + args. |
|
sudo machinectl shell "agent@$name" \ |
|
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash |
|
} |
|
|
|
delete_container() { |
|
local name=$1 |
|
local path="$CONTAINER_BASE/$name" |
|
|
|
if [ ! -d "$path" ]; then |
|
log_error "Not found." |
|
exit 1 |
|
fi |
|
|
|
echo -e "${RED}DANGER: Deleting $name${NC}" |
|
echo -e "This will wipe: $path" |
|
read -p "Type 'DELETE' to confirm: " confirm |
|
|
|
if [ "$confirm" == "DELETE" ]; then |
|
if machinectl list | grep -q "^$name "; then |
|
sudo machinectl terminate "$name" 2>/dev/null || true |
|
fi |
|
sudo rm -rf "$path" |
|
rm -f "$CONTAINER_CONFIG/$name.nspawn" |
|
rm -f "$CONTAINER_CONFIG/.$name.pwd" |
|
rm -f "$CONTAINER_CONFIG/.$name.meta" |
|
log_info "Deleted." |
|
else |
|
log_info "Cancelled." |
|
fi |
|
} |
|
|
|
list_containers() { |
|
echo -e "${BLUE}CONTAINERS:${NC}" |
|
for meta in "$CONTAINER_CONFIG"/.*.meta; do |
|
[ -e "$meta" ] || continue |
|
source "$meta" |
|
local status="STOPPED" |
|
if machinectl list 2>/dev/null | grep -q "^$NAME "; then |
|
status="${GREEN}RUNNING${NC}" |
|
fi |
|
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE" |
|
done |
|
} |
|
|
|
info_container() { |
|
local name=$1 |
|
local meta="$CONTAINER_CONFIG/.$name.meta" |
|
local pwd="$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
if [ ! -f "$meta" ]; then |
|
log_error "No metadata found for $name" |
|
exit 1 |
|
fi |
|
|
|
source "$meta" |
|
echo "--------------------------------" |
|
echo "Container: $NAME" |
|
echo "Created: $CREATED" |
|
echo "Network: $NET_ENABLED" |
|
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)" |
|
echo "--------------------------------" |
|
if [ -f "$pwd" ]; then |
|
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}" |
|
fi |
|
echo "--------------------------------" |
|
} |
|
|
|
# --- Main Dispatch --- |
|
|
|
check_dependencies |
|
setup_directories |
|
|
|
CMD="${1:-help}" |
|
shift || true |
|
|
|
case "$CMD" in |
|
create) create_container "$@" ;; |
|
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;; |
|
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;; |
|
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;; |
|
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;; |
|
list) list_containers ;; |
|
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;; |
|
*) show_help ;; |
|
esac |
|
``` |
|
|
|
if ! command -v $cmd &> /dev/null; then |
|
missing+=($cmd) |
|
fi |
|
done |
|
|
|
if [ ${#missing[@]} -gt 0 ]; then |
|
log_error "Missing dependencies: ${missing[*]}" |
|
log_info "Install with: sudo pacman -S arch-install-scripts systemd-container" |
|
exit 1 |
|
fi |
|
} |
|
|
|
setup_directories() { |
|
mkdir -p "$CONTAINER_BASE" |
|
mkdir -p "$CONTAINER_CONFIG" |
|
log_info "Container base: $CONTAINER_BASE" |
|
} |
|
|
|
create_container() { |
|
local name=$1 |
|
local mount_src="${2:-}" |
|
local mount_dst="${3:-/mnt/shared}" |
|
local permission_arg="${4:-}" |
|
local container_path="$CONTAINER_BASE/$name" |
|
|
|
if [ -d "$container_path" ]; then |
|
log_error "Container '$name' already exists" |
|
exit 1 |
|
fi |
|
|
|
# Determine Mount Mode |
|
local mount_mode="ro" |
|
if [ "$permission_arg" == "--rw" ]; then |
|
mount_mode="rw" |
|
fi |
|
|
|
# Validate mount source if provided |
|
if [ -n "$mount_src" ]; then |
|
# Expand tilde to home directory |
|
mount_src="${mount_src/#\~/$HOME}" |
|
|
|
if [ ! -d "$mount_src" ]; then |
|
log_error "Mount source directory does not exist: $mount_src" |
|
exit 1 |
|
fi |
|
|
|
if [ "$mount_mode" == "rw" ]; then |
|
log_warn "Will mount (READ-WRITE): $mount_src -> $mount_dst" |
|
else |
|
log_info "Will mount (READ-ONLY): $mount_src -> $mount_dst" |
|
fi |
|
fi |
|
|
|
log_info "Creating container '$name' at $container_path" |
|
|
|
# Create container directory |
|
mkdir -p "$container_path" |
|
|
|
# Create mount point inside container |
|
if [ -n "$mount_src" ]; then |
|
mkdir -p "$container_path$mount_dst" |
|
fi |
|
|
|
# Bootstrap minimal Arch Linux |
|
log_info "Bootstrapping Arch Linux (requires sudo for pacstrap)..." |
|
local max_attempts=3 |
|
local attempt=1 |
|
|
|
while [ $attempt -le $max_attempts ]; do |
|
log_info "Attempt $attempt of $max_attempts..." |
|
if sudo pacstrap -c "$container_path" base base-devel python python-pip git helix go zig --noconfirm; then |
|
log_info "Bootstrap completed successfully!" |
|
break |
|
else |
|
if [ $attempt -eq $max_attempts ]; then |
|
log_error "Failed to bootstrap. Try: sudo pacman-mirrors --fasttrack" |
|
sudo rm -rf "$container_path" |
|
exit 1 |
|
fi |
|
sleep 3 |
|
attempt=$((attempt + 1)) |
|
fi |
|
done |
|
|
|
# Create configuration with specific mount mode |
|
create_nspawn_config "$name" "$mount_src" "$mount_dst" "$mount_mode" |
|
|
|
# Set hostname |
|
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null |
|
|
|
# Setup networking |
|
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF |
|
[Match] |
|
Name=host0 |
|
|
|
[Network] |
|
DHCP=yes |
|
EOF |
|
|
|
# Enable networkd |
|
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service" |
|
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service" |
|
|
|
# Create user |
|
log_info "Setting up 'agent' user..." |
|
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -G wheel -s /bin/bash agent |
|
echo "agent:agent" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
echo "%wheel ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "$container_path/etc/sudoers.d/wheel" > /dev/null |
|
|
|
# Install Python packages |
|
log_info "Installing Python packages..." |
|
sudo systemd-nspawn -D "$container_path" --pipe \ |
|
bash -c "python -m pip install --break-system-packages mistral-vibe" || \ |
|
log_warn "Failed to install packages." |
|
|
|
log_info "Container '$name' created!" |
|
} |
|
|
|
create_nspawn_config() { |
|
local name=$1 |
|
local mount_src="${2:-}" |
|
local mount_dst="${3:-}" |
|
local mode="${4:-ro}" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
cat > "$config_file" <<EOF |
|
[Exec] |
|
Boot=yes |
|
PrivateUsers=pick |
|
|
|
[Network] |
|
VirtualEthernet=yes |
|
Private=yes |
|
|
|
[Files] |
|
PrivateUsersOwnership=auto |
|
EOF |
|
|
|
# Add bind mount based on mode |
|
if [ -n "$mount_src" ]; then |
|
if [ "$mode" == "rw" ]; then |
|
echo "Bind=$mount_src:$mount_dst" >> "$config_file" |
|
log_warn "Configured READ-WRITE bind: $mount_src -> $mount_dst" |
|
else |
|
echo "BindReadOnly=$mount_src:$mount_dst" >> "$config_file" |
|
log_info "Configured READ-ONLY bind: $mount_src -> $mount_dst" |
|
fi |
|
fi |
|
|
|
log_info "Created nspawn config: $config_file" |
|
} |
|
|
|
start_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container '$name' does not exist" |
|
exit 1 |
|
fi |
|
|
|
log_info "Starting container '$name'..." |
|
|
|
local bind_args="" |
|
if [ -f "$config_file" ]; then |
|
# Extract Bind= lines (Read-Write) -> convert to --bind= |
|
local bind_rw=$(grep "^Bind=" "$config_file" | sed 's/^Bind=/--bind=/' | tr '\n' ' ') |
|
|
|
# Extract BindReadOnly= lines (Read-Only) -> convert to --bind-ro= |
|
local bind_ro=$(grep "^BindReadOnly=" "$config_file" | sed 's/^BindReadOnly=/--bind-ro=/' | tr '\n' ' ') |
|
|
|
bind_args="$bind_rw $bind_ro" |
|
fi |
|
|
|
# Start container |
|
sudo systemd-nspawn -D "$container_path" \ |
|
--machine="$name" \ |
|
--network-veth \ |
|
--private-users=pick \ |
|
--private-users-ownership=auto \ |
|
$bind_args \ |
|
--boot & |
|
|
|
sleep 2 |
|
log_info "Container '$name' started" |
|
} |
|
|
|
shell_container() { |
|
local name=$1 |
|
|
|
if ! machinectl list | grep -q "$name"; then |
|
log_warn "Container '$name' is not running. Starting..." |
|
start_container "$name" |
|
sleep 3 |
|
fi |
|
|
|
log_info "Opening shell in container '$name' as user 'agent'..." |
|
sudo machinectl shell agent@$name /bin/bash |
|
} |
|
|
|
stop_container() { |
|
local name=$1 |
|
|
|
log_info "Stopping container '$name'..." |
|
sudo machinectl poweroff "$name" 2>/dev/null || true |
|
sleep 2 |
|
log_info "Container '$name' stopped" |
|
} |
|
|
|
delete_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container '$name' does not exist" |
|
exit 1 |
|
fi |
|
|
|
# Check if container is running |
|
if machinectl list 2>/dev/null | grep -q "$name"; then |
|
log_warn "Container '$name' is running. Stopping it first..." |
|
stop_container "$name" |
|
fi |
|
|
|
log_warn "About to delete container '$name'" |
|
log_warn "Path: $container_path" |
|
read -p "Are you sure? (yes/no): " confirm |
|
|
|
if [ "$confirm" != "yes" ]; then |
|
log_info "Deletion cancelled" |
|
exit 0 |
|
fi |
|
|
|
log_info "Deleting container '$name' (requires sudo for root-owned files)..." |
|
|
|
# Remove container directory (use sudo for root-owned files) |
|
sudo rm -rf "$container_path" |
|
|
|
# Remove config file (user-owned, no sudo needed) |
|
if [ -f "$config_file" ]; then |
|
rm -f "$config_file" |
|
fi |
|
|
|
log_info "Container '$name' deleted successfully" |
|
} |
|
|
|
list_containers() { |
|
log_info "Available containers:" |
|
if [ -d "$CONTAINER_BASE" ]; then |
|
ls -1 "$CONTAINER_BASE" 2>/dev/null || echo " (none)" |
|
else |
|
echo " (none)" |
|
fi |
|
|
|
echo "" |
|
log_info "Running containers:" |
|
machinectl list --no-pager 2>/dev/null || echo " (none)" |
|
} |
|
|
|
case "${1:-}" in |
|
create) |
|
```bash /home/akh/bin/spawn.sh |
|
#!/bin/bash |
|
# spawn-sh - Production-Grade Secure Container Manager for AI Agents |
|
# |
|
set -euo pipefail |
|
|
|
# --- Configuration --- |
|
CONTAINER_BASE="${CONTAINER_BASE:-$HOME/.local/containers}" |
|
CONTAINER_CONFIG="${CONTAINER_CONFIG:-$HOME/.config/systemd-nspawn}" |
|
CONTAINER_LOGS="${CONTAINER_LOGS:-$HOME/.local/log/spawn}" |
|
DEFAULT_CPU_QUOTA="50%" |
|
DEFAULT_MEM_LIMIT="2G" |
|
|
|
# Colors |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' |
|
|
|
# --- Logging Helpers --- |
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } |
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } |
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } |
|
log_sec() { echo -e "${BLUE}[SECURITY]${NC} $1"; } |
|
|
|
# --- Dependency Check --- |
|
check_dependencies() { |
|
local missing=() |
|
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do |
|
if ! command -v "$cmd" &> /dev/null; then |
|
missing+=("$cmd") |
|
fi |
|
done |
|
if [ ${#missing[@]} -gt 0 ]; then |
|
log_error "Missing dependencies: ${missing[*]}" |
|
exit 1 |
|
fi |
|
} |
|
|
|
setup_directories() { |
|
mkdir -p "$CONTAINER_BASE" |
|
mkdir -p "$CONTAINER_CONFIG" |
|
mkdir -p "$CONTAINER_LOGS" |
|
chmod 700 "$CONTAINER_LOGS" |
|
} |
|
|
|
# --- Parsing Logic --- |
|
|
|
show_help() { |
|
cat << EOF |
|
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options] |
|
|
|
Commands: |
|
create <name> [source] [dest] Create a secure container |
|
--source, -s Host directory to mount (e.g., ~/projects) |
|
--dest, -d Mount point inside container (default: /mnt/work) |
|
--rw Mount as Read-Write (Default is Read-Only) |
|
--cpu CPU Quota (default: 50%) |
|
--memory Memory Limit (default: 2G) |
|
--network Enable networking (Default: disabled/isolated) |
|
--timeout Auto-shutdown in seconds (default: 3600) |
|
--packages Extra packages to install (comma separated) |
|
|
|
start <name> Start container |
|
stop <name> Stop container |
|
shell <name> Open secure shell |
|
delete <name> Destroy container |
|
list Show inventory |
|
info <name> Show details & password |
|
EOF |
|
} |
|
|
|
# --- Core Functions --- |
|
|
|
create_container() { |
|
local name="$1" |
|
shift |
|
|
|
# Defaults |
|
local mount_src="" |
|
local mount_dst="" |
|
local mount_mode="ro" |
|
local cpu_quota="$DEFAULT_CPU_QUOTA" |
|
local mem_limit="$DEFAULT_MEM_LIMIT" |
|
local enable_net="false" |
|
local timeout="3600" |
|
local extra_pkgs="" |
|
|
|
# Argument Parsing (Hybrid: Flags or Positional) |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-s|--source) mount_src="$2"; shift 2 ;; |
|
-d|--dest) mount_dst="$2"; shift 2 ;; |
|
--rw) mount_mode="rw"; shift ;; |
|
--cpu) cpu_quota="$2"; shift 2 ;; |
|
--memory) mem_limit="$2"; shift 2 ;; |
|
--network) enable_net="true"; shift ;; |
|
--timeout) timeout="$2"; shift 2 ;; |
|
--packages) extra_pkgs="$2"; shift 2 ;; |
|
-*) log_error "Unknown option: $1"; exit 1 ;; |
|
*) |
|
# Handle positional arguments: [source] [dest] |
|
if [ -z "$mount_src" ]; then |
|
mount_src="$1" |
|
elif [ -z "$mount_dst" ]; then |
|
mount_dst="$1" |
|
else |
|
log_error "Too many arguments. Unexpected: $1" |
|
exit 1 |
|
fi |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi |
|
|
|
local container_path="$CONTAINER_BASE/$name" |
|
|
|
if [ -d "$container_path" ]; then |
|
log_error "Container '$name' already exists." |
|
exit 1 |
|
fi |
|
|
|
# 1. Security Validation |
|
if [ -n "$mount_src" ]; then |
|
mount_src=$(realpath "$mount_src") |
|
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root") |
|
for blocked in "${blocked_paths[@]}"; do |
|
if [[ "$mount_src" == "$blocked"* ]]; then |
|
log_error "Security Block: Mounting '$mount_src' is forbidden." |
|
exit 1 |
|
fi |
|
done |
|
if [ ! -d "$mount_src" ]; then |
|
log_error "Source directory does not exist: $mount_src" |
|
exit 1 |
|
fi |
|
fi |
|
|
|
log_info "Initializing container '$name'..." |
|
mkdir -p "$container_path" |
|
|
|
# 2. Bootstrap |
|
log_info "Bootstrapping Arch Linux..." |
|
local pkg_list="base python python-pip git ${extra_pkgs//,/ }" |
|
|
|
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then |
|
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log" |
|
sudo rm -rf "$container_path" |
|
exit 1 |
|
fi |
|
|
|
# 3. Generate Credentials |
|
local agent_pass |
|
agent_pass=$(openssl rand -base64 24) |
|
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd" |
|
chmod 600 "$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
# 4. Configure User & Network |
|
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null |
|
|
|
if [ "$enable_net" == "true" ]; then |
|
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF |
|
[Match] |
|
Name=host0 |
|
[Network] |
|
DHCP=yes |
|
EOF |
|
else |
|
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network" |
|
fi |
|
|
|
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service" |
|
|
|
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent |
|
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
|
|
if [[ "$pkg_list" == *"sudo"* ]]; then |
|
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent |
|
fi |
|
|
|
# 5. Create Secure Config (.nspawn) |
|
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn" |
|
local mem_bytes |
|
mem_bytes=$(numfmt --from=iec "$mem_limit") |
|
|
|
cat > "$nspawn_file" <<EOF |
|
[Exec] |
|
Boot=yes |
|
PrivateUsers=pick |
|
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD |
|
ProcessTwo=yes |
|
NoNewPrivileges=yes |
|
|
|
[Network] |
|
VirtualEthernet=yes |
|
Private=yes |
|
|
|
[Files] |
|
PrivateUsersOwnership=auto |
|
ProtectSystem=strict |
|
ProtectHome=read-only |
|
PrivateDevices=yes |
|
ProtectKernelTunables=yes |
|
ProtectControlGroups=yes |
|
LockPersonality=yes |
|
|
|
[CPU] |
|
CPUQuota=$cpu_quota |
|
|
|
[Memory] |
|
MemoryMax=$mem_bytes |
|
EOF |
|
|
|
if [ -n "$mount_src" ]; then |
|
if [ "$mount_mode" == "rw" ]; then |
|
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file" |
|
else |
|
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file" |
|
fi |
|
fi |
|
|
|
echo "Bind=/home/agent" >> "$nspawn_file" |
|
|
|
# 6. Auto-Shutdown Timer |
|
setup_shutdown_timer "$container_path" "$timeout" |
|
|
|
# 7. Metadata |
|
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF |
|
NAME=$name |
|
CREATED=$(date -Iseconds) |
|
MOUNT_SRC=$mount_src |
|
MOUNT_MODE=$mount_mode |
|
NET_ENABLED=$enable_net |
|
EOF |
|
|
|
log_sec "Container '$name' created." |
|
log_info "Password saved to secure vault." |
|
} |
|
|
|
setup_shutdown_timer() { |
|
local path="$1" |
|
local timeout="$2" |
|
|
|
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null |
|
#!/bin/bash |
|
sleep $timeout |
|
echo "Time limit reached. Shutting down." |
|
poweroff |
|
EOF |
|
sudo chmod +x "$path/usr/local/bin/auto-shutdown" |
|
|
|
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null |
|
[Unit] |
|
Description=Security Timeout |
|
After=multi-user.target |
|
|
|
[Service] |
|
ExecStart=/usr/local/bin/auto-shutdown |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
|
|
sudo ln -sf /etc/systemd/system/auto-shutdown.service \ |
|
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service" |
|
} |
|
|
|
start_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container path not found: $container_path" |
|
exit 1 |
|
fi |
|
|
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_warn "Container '$name' is already running." |
|
return |
|
fi |
|
|
|
log_info "Starting $name..." |
|
|
|
# 1. Config Bridge |
|
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name |
|
sudo mkdir -p /run/systemd/nspawn |
|
if [ -f "$config_file" ]; then |
|
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
|
|
# 2. Start Service |
|
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true |
|
|
|
# We use systemd-run to launch nspawn as a transient service. |
|
sudo systemd-run \ |
|
--unit="spawn-$name" \ |
|
--description="Spawn-Hybrid Container $name" \ |
|
systemd-nspawn --machine="$name" -D "$container_path" --boot |
|
|
|
# 3. Verify |
|
local attempts=0 |
|
while [ $attempts -lt 5 ]; do |
|
sleep 1 |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_info "Container booted successfully." |
|
return |
|
fi |
|
attempts=$((attempts+1)) |
|
done |
|
|
|
log_warn "Container startup queued. Check status with: systemctl status spawn-$name" |
|
} |
|
|
|
stop_container() { |
|
local name=$1 |
|
log_info "Stopping $name..." |
|
sudo machinectl poweroff "$name" |
|
|
|
# Cleanup config link |
|
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then |
|
sudo rm "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
} |
|
|
|
shell_container() { |
|
local name=$1 |
|
if ! machinectl list | grep -q "^$name "; then |
|
start_container "$name" |
|
sleep 2 |
|
fi |
|
log_sec "Entering sandbox as user 'agent'..." |
|
|
|
# FIX: Use /usr/bin/env to set variables. |
|
```bash /home/akh/bin/spawn.sh |
|
#!/bin/bash |
|
# spawn-sh - Production-Grade Secure Container Manager for AI Agents |
|
# |
|
set -euo pipefail |
|
|
|
# --- Configuration --- |
|
CONTAINER_BASE="${CONTAINER_BASE:-$HOME/.local/containers}" |
|
CONTAINER_CONFIG="${CONTAINER_CONFIG:-$HOME/.config/systemd-nspawn}" |
|
CONTAINER_LOGS="${CONTAINER_LOGS:-$HOME/.local/log/spawn}" |
|
DEFAULT_CPU_QUOTA="50%" |
|
DEFAULT_MEM_LIMIT="2G" |
|
|
|
# Colors |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' |
|
|
|
# --- Logging Helpers --- |
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } |
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } |
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } |
|
log_sec() { echo -e "${BLUE}[SECURITY]${NC} $1"; } |
|
|
|
# --- Dependency Check --- |
|
check_dependencies() { |
|
local missing=() |
|
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do |
|
if ! command -v "$cmd" &> /dev/null; then |
|
missing+=("$cmd") |
|
fi |
|
done |
|
if [ ${#missing[@]} -gt 0 ]; then |
|
log_error "Missing dependencies: ${missing[*]}" |
|
exit 1 |
|
fi |
|
} |
|
|
|
setup_directories() { |
|
mkdir -p "$CONTAINER_BASE" |
|
mkdir -p "$CONTAINER_CONFIG" |
|
mkdir -p "$CONTAINER_LOGS" |
|
chmod 700 "$CONTAINER_LOGS" |
|
} |
|
|
|
# --- Parsing Logic --- |
|
|
|
show_help() { |
|
cat << EOF |
|
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options] |
|
|
|
Commands: |
|
create <name> [source] [dest] Create a secure container |
|
--source, -s Host directory to mount (e.g., ~/projects) |
|
--dest, -d Mount point inside container (default: /mnt/work) |
|
--rw Mount as Read-Write (Default is Read-Only) |
|
--cpu CPU Quota (default: 50%) |
|
--memory Memory Limit (default: 2G) |
|
--network Enable networking (Default: disabled/isolated) |
|
--timeout Auto-shutdown in seconds (default: 3600) |
|
--packages Extra packages to install (comma separated) |
|
|
|
start <name> Start container |
|
stop <name> Stop container |
|
shell <name> Open secure shell |
|
delete <name> Destroy container |
|
list Show inventory |
|
info <name> Show details & password |
|
EOF |
|
} |
|
|
|
# --- Core Functions --- |
|
|
|
create_container() { |
|
local name="$1" |
|
shift |
|
|
|
# Defaults |
|
local mount_src="" |
|
local mount_dst="" |
|
local mount_mode="ro" |
|
local cpu_quota="$DEFAULT_CPU_QUOTA" |
|
local mem_limit="$DEFAULT_MEM_LIMIT" |
|
local enable_net="false" |
|
local timeout="3600" |
|
local extra_pkgs="" |
|
|
|
# Argument Parsing (Hybrid: Flags or Positional) |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-s|--source) mount_src="$2"; shift 2 ;; |
|
-d|--dest) mount_dst="$2"; shift 2 ;; |
|
--rw) mount_mode="rw"; shift ;; |
|
--cpu) cpu_quota="$2"; shift 2 ;; |
|
--memory) mem_limit="$2"; shift 2 ;; |
|
--network) enable_net="true"; shift ;; |
|
--timeout) timeout="$2"; shift 2 ;; |
|
--packages) extra_pkgs="$2"; shift 2 ;; |
|
-*) log_error "Unknown option: $1"; exit 1 ;; |
|
*) |
|
# Handle positional arguments: [source] [dest] |
|
if [ -z "$mount_src" ]; then |
|
mount_src="$1" |
|
elif [ -z "$mount_dst" ]; then |
|
mount_dst="$1" |
|
else |
|
log_error "Too many arguments. Unexpected: $1" |
|
exit 1 |
|
fi |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi |
|
|
|
local container_path="$CONTAINER_BASE/$name" |
|
|
|
if [ -d "$container_path" ]; then |
|
log_error "Container '$name' already exists." |
|
exit 1 |
|
fi |
|
|
|
# 1. Security Validation |
|
if [ -n "$mount_src" ]; then |
|
mount_src=$(realpath "$mount_src") |
|
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root") |
|
for blocked in "${blocked_paths[@]}"; do |
|
if [[ "$mount_src" == "$blocked"* ]]; then |
|
log_error "Security Block: Mounting '$mount_src' is forbidden." |
|
exit 1 |
|
fi |
|
done |
|
if [ ! -d "$mount_src" ]; then |
|
log_error "Source directory does not exist: $mount_src" |
|
exit 1 |
|
fi |
|
fi |
|
|
|
log_info "Initializing container '$name'..." |
|
mkdir -p "$container_path" |
|
|
|
# 2. Bootstrap |
|
log_info "Bootstrapping Arch Linux..." |
|
local pkg_list="base python python-pip git ${extra_pkgs//,/ }" |
|
|
|
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then |
|
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log" |
|
sudo rm -rf "$container_path" |
|
exit 1 |
|
fi |
|
|
|
# 3. Generate Credentials |
|
local agent_pass |
|
agent_pass=$(openssl rand -base64 24) |
|
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd" |
|
chmod 600 "$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
# 4. Configure User & Network |
|
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null |
|
|
|
if [ "$enable_net" == "true" ]; then |
|
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF |
|
[Match] |
|
Name=host0 |
|
[Network] |
|
DHCP=yes |
|
EOF |
|
else |
|
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network" |
|
fi |
|
|
|
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service" |
|
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service" |
|
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent |
|
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
|
|
if [[ "$pkg_list" == *"sudo"* ]]; then |
|
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent |
|
fi |
|
|
|
# 5. Create Secure Config (.nspawn) |
|
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn" |
|
local mem_bytes |
|
mem_bytes=$(numfmt --from=iec "$mem_limit") |
|
|
|
cat > "$nspawn_file" <<EOF |
|
[Exec] |
|
Boot=yes |
|
PrivateUsers=pick |
|
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD |
|
ProcessTwo=yes |
|
|
|
[Network] |
|
VirtualEthernet=yes |
|
Private=yes |
|
|
|
[Files] |
|
PrivateUsersOwnership=auto |
|
ProtectSystem=strict |
|
ProtectHome=read-only |
|
PrivateDevices=yes |
|
ProtectKernelTunables=yes |
|
ProtectControlGroups=yes |
|
LockPersonality=yes |
|
|
|
[CPU] |
|
CPUQuota=$cpu_quota |
|
|
|
[Memory] |
|
MemoryMax=$mem_bytes |
|
EOF |
|
|
|
if [ -n "$mount_src" ]; then |
|
if [ "$mount_mode" == "rw" ]; then |
|
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file" |
|
else |
|
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file" |
|
fi |
|
fi |
|
|
|
echo "Bind=/home/agent" >> "$nspawn_file" |
|
|
|
# 6. Auto-Shutdown Timer |
|
setup_shutdown_timer "$container_path" "$timeout" |
|
|
|
# 7. Metadata |
|
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF |
|
NAME=$name |
|
CREATED=$(date -Iseconds) |
|
MOUNT_SRC=$mount_src |
|
MOUNT_MODE=$mount_mode |
|
NET_ENABLED=$enable_net |
|
EOF |
|
|
|
log_sec "Container '$name' created." |
|
log_info "Password saved to secure vault." |
|
} |
|
|
|
setup_shutdown_timer() { |
|
local path="$1" |
|
local timeout="$2" |
|
|
|
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null |
|
#!/bin/bash |
|
sleep $timeout |
|
echo "Time limit reached. Shutting down." |
|
poweroff |
|
EOF |
|
sudo chmod +x "$path/usr/local/bin/auto-shutdown" |
|
|
|
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null |
|
[Unit] |
|
Description=Security Timeout |
|
After=multi-user.target |
|
|
|
[Service] |
|
ExecStart=/usr/local/bin/auto-shutdown |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
|
|
sudo ln -sf /etc/systemd/system/auto-shutdown.service \ |
|
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service" |
|
} |
|
|
|
start_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container path not found: $container_path" |
|
exit 1 |
|
fi |
|
|
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_warn "Container '$name' is already running." |
|
return |
|
fi |
|
|
|
log_info "Starting $name..." |
|
|
|
# 1. Config Bridge |
|
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name |
|
sudo mkdir -p /run/systemd/nspawn |
|
if [ -f "$config_file" ]; then |
|
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
|
|
# 2. Start Service |
|
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true |
|
|
|
# We use systemd-run to launch nspawn as a transient service. |
|
sudo systemd-run \ |
|
--unit="spawn-$name" \ |
|
--description="Spawn-Hybrid Container $name" \ |
|
systemd-nspawn --machine="$name" -D "$container_path" --boot |
|
|
|
# 3. Verify |
|
local attempts=0 |
|
while [ $attempts -lt 5 ]; do |
|
sleep 1 |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_info "Container booted successfully." |
|
return |
|
fi |
|
attempts=$((attempts+1)) |
|
done |
|
|
|
log_warn "Container startup queued. Check status with: systemctl status spawn-$name" |
|
} |
|
|
|
stop_container() { |
|
local name=$1 |
|
log_info "Stopping $name..." |
|
sudo machinectl poweroff "$name" |
|
|
|
# Cleanup config link |
|
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then |
|
sudo rm "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
} |
|
|
|
shell_container() { |
|
local name=$1 |
|
if ! machinectl list | grep -q "^$name "; then |
|
start_container "$name" |
|
sleep 2 |
|
fi |
|
log_sec "Entering sandbox as user 'agent'..." |
|
|
|
# FIX: Use /usr/bin/env to set variables. |
|
# machinectl shell interprets arguments as command + args. |
|
sudo machinectl shell "agent@$name" \ |
|
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash |
|
} |
|
|
|
delete_container() { |
|
local name=$1 |
|
local path="$CONTAINER_BASE/$name" |
|
|
|
if [ ! -d "$path" ]; then |
|
log_error "Not found." |
|
exit 1 |
|
fi |
|
|
|
echo -e "${RED}DANGER: Deleting $name${NC}" |
|
echo -e "This will wipe: $path" |
|
read -p "Type 'DELETE' to confirm: " confirm |
|
|
|
if [ "$confirm" == "DELETE" ]; then |
|
if machinectl list | grep -q "^$name "; then |
|
sudo machinectl terminate "$name" 2>/dev/null || true |
|
fi |
|
sudo rm -rf "$path" |
|
rm -f "$CONTAINER_CONFIG/$name.nspawn" |
|
rm -f "$CONTAINER_CONFIG/.$name.pwd" |
|
rm -f "$CONTAINER_CONFIG/.$name.meta" |
|
log_info "Deleted." |
|
else |
|
log_info "Cancelled." |
|
fi |
|
} |
|
|
|
list_containers() { |
|
echo -e "${BLUE}CONTAINERS:${NC}" |
|
for meta in "$CONTAINER_CONFIG"/.*.meta; do |
|
[ -e "$meta" ] || continue |
|
source "$meta" |
|
local status="STOPPED" |
|
if machinectl list 2>/dev/null | grep -q "^$NAME "; then |
|
status="${GREEN}RUNNING${NC}" |
|
fi |
|
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE" |
|
done |
|
} |
|
|
|
info_container() { |
|
local name=$1 |
|
local meta="$CONTAINER_CONFIG/.$name.meta" |
|
local pwd="$CONTAINER_CONFIG/.$name.pwd" |
|
```bash /home/akh/bin/spawn.sh |
|
#!/bin/bash |
|
# spawn-sh - Production-Grade Secure Container Manager for AI Agents |
|
# |
|
set -euo pipefail |
|
|
|
# --- Configuration --- |
|
CONTAINER_BASE="${CONTAINER_BASE:-$HOME/.local/containers}" |
|
CONTAINER_CONFIG="${CONTAINER_CONFIG:-$HOME/.config/systemd-nspawn}" |
|
CONTAINER_LOGS="${CONTAINER_LOGS:-$HOME/.local/log/spawn}" |
|
DEFAULT_CPU_QUOTA="50%" |
|
DEFAULT_MEM_LIMIT="2G" |
|
|
|
# Colors |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' |
|
|
|
# --- Logging Helpers --- |
|
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } |
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } |
|
log_error() { echo -e "${RED}[ERROR]${NC} $1"; } |
|
log_sec() { echo -e "${BLUE}[SECURITY]${NC} $1"; } |
|
|
|
# --- Dependency Check --- |
|
check_dependencies() { |
|
local missing=() |
|
for cmd in pacstrap systemd-nspawn machinectl openssl systemd-run; do |
|
if ! command -v "$cmd" &> /dev/null; then |
|
missing+=("$cmd") |
|
fi |
|
done |
|
if [ ${#missing[@]} -gt 0 ]; then |
|
log_error "Missing dependencies: ${missing[*]}" |
|
exit 1 |
|
fi |
|
} |
|
|
|
setup_directories() { |
|
mkdir -p "$CONTAINER_BASE" |
|
mkdir -p "$CONTAINER_CONFIG" |
|
mkdir -p "$CONTAINER_LOGS" |
|
chmod 700 "$CONTAINER_LOGS" |
|
} |
|
|
|
# --- Parsing Logic --- |
|
|
|
show_help() { |
|
cat << EOF |
|
Usage: $(basename "$0") {create|start|stop|delete|list|info|shell} [options] |
|
|
|
Commands: |
|
create <name> [source] [dest] Create a secure container |
|
--source, -s Host directory to mount (e.g., ~/projects) |
|
--dest, -d Mount point inside container (default: /mnt/work) |
|
--rw Mount as Read-Write (Default is Read-Only) |
|
--cpu CPU Quota (default: 50%) |
|
--memory Memory Limit (default: 2G) |
|
--network Enable networking (Default: disabled/isolated) |
|
--timeout Auto-shutdown in seconds (default: 3600) |
|
--packages Extra packages to install (comma separated) |
|
|
|
start <name> Start container |
|
stop <name> Stop container |
|
shell <name> Open secure shell |
|
delete <name> Destroy container |
|
list Show inventory |
|
info <name> Show details & password |
|
EOF |
|
} |
|
|
|
# --- Core Functions --- |
|
|
|
create_container() { |
|
local name="$1" |
|
shift |
|
|
|
# Defaults |
|
local mount_src="" |
|
local mount_dst="" |
|
local mount_mode="ro" |
|
local cpu_quota="$DEFAULT_CPU_QUOTA" |
|
local mem_limit="$DEFAULT_MEM_LIMIT" |
|
local enable_net="false" |
|
local timeout="3600" |
|
local extra_pkgs="" |
|
|
|
# Argument Parsing (Hybrid: Flags or Positional) |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
-s|--source) mount_src="$2"; shift 2 ;; |
|
-d|--dest) mount_dst="$2"; shift 2 ;; |
|
--rw) mount_mode="rw"; shift ;; |
|
--cpu) cpu_quota="$2"; shift 2 ;; |
|
--memory) mem_limit="$2"; shift 2 ;; |
|
--network) enable_net="true"; shift ;; |
|
--timeout) timeout="$2"; shift 2 ;; |
|
--packages) extra_pkgs="$2"; shift 2 ;; |
|
-*) log_error "Unknown option: $1"; exit 1 ;; |
|
*) |
|
# Handle positional arguments: [source] [dest] |
|
if [ -z "$mount_src" ]; then |
|
mount_src="$1" |
|
elif [ -z "$mount_dst" ]; then |
|
mount_dst="$1" |
|
else |
|
log_error "Too many arguments. Unexpected: $1" |
|
exit 1 |
|
fi |
|
shift |
|
;; |
|
esac |
|
done |
|
|
|
if [ -z "$mount_dst" ]; then mount_dst="/mnt/work"; fi |
|
|
|
local container_path="$CONTAINER_BASE/$name" |
|
|
|
if [ -d "$container_path" ]; then |
|
log_error "Container '$name' already exists." |
|
exit 1 |
|
fi |
|
|
|
# 1. Security Validation |
|
if [ -n "$mount_src" ]; then |
|
mount_src=$(realpath "$mount_src") |
|
local blocked_paths=("$HOME/.ssh" "$HOME/.aws" "$HOME/.config" "/etc" "/boot" "/dev" "/root") |
|
for blocked in "${blocked_paths[@]}"; do |
|
if [[ "$mount_src" == "$blocked"* ]]; then |
|
log_error "Security Block: Mounting '$mount_src' is forbidden." |
|
exit 1 |
|
fi |
|
done |
|
if [ ! -d "$mount_src" ]; then |
|
log_error "Source directory does not exist: $mount_src" |
|
exit 1 |
|
fi |
|
fi |
|
|
|
log_info "Initializing container '$name'..." |
|
mkdir -p "$container_path" |
|
|
|
# 2. Bootstrap |
|
log_info "Bootstrapping Arch Linux..." |
|
local pkg_list="base python python-pip git ${extra_pkgs//,/ }" |
|
|
|
if ! sudo pacstrap -c "$container_path" $pkg_list --noconfirm > "$CONTAINER_LOGS/$name-bootstrap.log" 2>&1; then |
|
log_error "Bootstrap failed. See logs at $CONTAINER_LOGS/$name-bootstrap.log" |
|
sudo rm -rf "$container_path" |
|
exit 1 |
|
fi |
|
|
|
# 3. Generate Credentials |
|
local agent_pass |
|
agent_pass=$(openssl rand -base64 24) |
|
echo "$agent_pass" > "$CONTAINER_CONFIG/.$name.pwd" |
|
chmod 600 "$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
# 4. Configure User & Network |
|
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null |
|
|
|
if [ "$enable_net" == "true" ]; then |
|
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF |
|
[Match] |
|
Name=host0 |
|
[Network] |
|
DHCP=yes |
|
EOF |
|
else |
|
sudo rm -f "$container_path/etc/systemd/network/80-container-host0.network" |
|
fi |
|
|
|
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service" |
|
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \ |
|
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service" |
|
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -s /bin/bash agent |
|
echo "agent:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
echo "root:$agent_pass" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd |
|
|
|
if [[ "$pkg_list" == *"sudo"* ]]; then |
|
sudo systemd-nspawn -D "$container_path" --pipe usermod -aG wheel agent |
|
fi |
|
|
|
# 5. Create Secure Config (.nspawn) |
|
local nspawn_file="$CONTAINER_CONFIG/$name.nspawn" |
|
local mem_bytes |
|
mem_bytes=$(numfmt --from=iec "$mem_limit") |
|
|
|
cat > "$nspawn_file" <<EOF |
|
[Exec] |
|
Boot=yes |
|
PrivateUsers=pick |
|
DropCapability=CAP_SYS_MODULE CAP_SYS_BOOT CAP_SYS_TIME CAP_AUDIT_CONTROL CAP_AUDIT_WRITE CAP_MAC_ADMIN CAP_MAC_OVERRIDE CAP_SYSLOG CAP_MKNOD |
|
ProcessTwo=yes |
|
|
|
[Network] |
|
VirtualEthernet=yes |
|
Private=yes |
|
|
|
[Files] |
|
PrivateUsersOwnership=auto |
|
ProtectSystem=strict |
|
ProtectHome=read-only |
|
PrivateDevices=yes |
|
ProtectKernelTunables=yes |
|
ProtectControlGroups=yes |
|
LockPersonality=yes |
|
|
|
[CPU] |
|
CPUQuota=$cpu_quota |
|
|
|
[Memory] |
|
MemoryMax=$mem_bytes |
|
EOF |
|
|
|
if [ -n "$mount_src" ]; then |
|
if [ "$mount_mode" == "rw" ]; then |
|
echo "Bind=$mount_src:$mount_dst" >> "$nspawn_file" |
|
else |
|
echo "BindReadOnly=$mount_src:$mount_dst" >> "$nspawn_file" |
|
fi |
|
fi |
|
|
|
echo "Bind=/home/agent" >> "$nspawn_file" |
|
|
|
# 6. Auto-Shutdown Timer |
|
setup_shutdown_timer "$container_path" "$timeout" |
|
|
|
# 7. Metadata |
|
cat > "$CONTAINER_CONFIG/.$name.meta" <<EOF |
|
NAME=$name |
|
CREATED=$(date -Iseconds) |
|
MOUNT_SRC=$mount_src |
|
MOUNT_MODE=$mount_mode |
|
NET_ENABLED=$enable_net |
|
EOF |
|
|
|
log_sec "Container '$name' created." |
|
log_info "Password saved to secure vault." |
|
} |
|
|
|
setup_shutdown_timer() { |
|
local path="$1" |
|
local timeout="$2" |
|
|
|
cat <<EOF | sudo tee "$path/usr/local/bin/auto-shutdown" > /dev/null |
|
#!/bin/bash |
|
sleep $timeout |
|
echo "Time limit reached. Shutting down." |
|
poweroff |
|
EOF |
|
sudo chmod +x "$path/usr/local/bin/auto-shutdown" |
|
|
|
cat <<EOF | sudo tee "$path/etc/systemd/system/auto-shutdown.service" > /dev/null |
|
[Unit] |
|
Description=Security Timeout |
|
After=multi-user.target |
|
|
|
[Service] |
|
ExecStart=/usr/local/bin/auto-shutdown |
|
|
|
[Install] |
|
WantedBy=multi-user.target |
|
EOF |
|
|
|
sudo ln -sf /etc/systemd/system/auto-shutdown.service \ |
|
"$path/etc/systemd/system/multi-user.target.wants/auto-shutdown.service" |
|
} |
|
|
|
start_container() { |
|
local name=$1 |
|
local container_path="$CONTAINER_BASE/$name" |
|
local config_file="$CONTAINER_CONFIG/$name.nspawn" |
|
|
|
if [ ! -d "$container_path" ]; then |
|
log_error "Container path not found: $container_path" |
|
exit 1 |
|
fi |
|
|
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_warn "Container '$name' is already running." |
|
return |
|
fi |
|
|
|
log_info "Starting $name..." |
|
|
|
# 1. Config Bridge |
|
# Link the user's config to /run/systemd/nspawn so nspawn finds it by machine name |
|
sudo mkdir -p /run/systemd/nspawn |
|
if [ -f "$config_file" ]; then |
|
sudo ln -sf "$config_file" "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
|
|
# 2. Start Service |
|
sudo systemctl reset-failed "spawn-$name" 2>/dev/null || true |
|
|
|
# We use systemd-run to launch nspawn as a transient service. |
|
sudo systemd-run \ |
|
--unit="spawn-$name" \ |
|
--description="Spawn-Hybrid Container $name" \ |
|
systemd-nspawn --machine="$name" -D "$container_path" --boot |
|
|
|
# 3. Verify |
|
local attempts=0 |
|
while [ $attempts -lt 5 ]; do |
|
sleep 1 |
|
if machinectl list 2>/dev/null | grep -q "^$name "; then |
|
log_info "Container booted successfully." |
|
return |
|
fi |
|
attempts=$((attempts+1)) |
|
done |
|
|
|
log_warn "Container startup queued. Check status with: systemctl status spawn-$name" |
|
} |
|
|
|
stop_container() { |
|
local name=$1 |
|
log_info "Stopping $name..." |
|
sudo machinectl poweroff "$name" |
|
|
|
# Cleanup config link |
|
if [ -f "/run/systemd/nspawn/$name.nspawn" ]; then |
|
sudo rm "/run/systemd/nspawn/$name.nspawn" |
|
fi |
|
} |
|
|
|
shell_container() { |
|
local name=$1 |
|
if ! machinectl list | grep -q "^$name "; then |
|
start_container "$name" |
|
sleep 2 |
|
fi |
|
log_sec "Entering sandbox as user 'agent'..." |
|
|
|
# FIX: Use /usr/bin/env to set variables. |
|
# machinectl shell interprets arguments as command + args. |
|
sudo machinectl shell "agent@$name" \ |
|
/usr/bin/env TERM=xterm-256color HOME=/home/agent /bin/bash |
|
} |
|
|
|
delete_container() { |
|
local name=$1 |
|
local path="$CONTAINER_BASE/$name" |
|
|
|
if [ ! -d "$path" ]; then |
|
log_error "Not found." |
|
exit 1 |
|
fi |
|
|
|
echo -e "${RED}DANGER: Deleting $name${NC}" |
|
echo -e "This will wipe: $path" |
|
read -p "Type 'DELETE' to confirm: " confirm |
|
|
|
if [ "$confirm" == "DELETE" ]; then |
|
if machinectl list | grep -q "^$name "; then |
|
sudo machinectl terminate "$name" 2>/dev/null || true |
|
fi |
|
sudo rm -rf "$path" |
|
rm -f "$CONTAINER_CONFIG/$name.nspawn" |
|
rm -f "$CONTAINER_CONFIG/.$name.pwd" |
|
rm -f "$CONTAINER_CONFIG/.$name.meta" |
|
log_info "Deleted." |
|
else |
|
log_info "Cancelled." |
|
fi |
|
} |
|
|
|
list_containers() { |
|
echo -e "${BLUE}CONTAINERS:${NC}" |
|
for meta in "$CONTAINER_CONFIG"/.*.meta; do |
|
[ -e "$meta" ] || continue |
|
source "$meta" |
|
local status="STOPPED" |
|
if machinectl list 2>/dev/null | grep -q "^$NAME "; then |
|
status="${GREEN}RUNNING${NC}" |
|
fi |
|
echo -e " $NAME\t[$status]\tNet: $NET_ENABLED\tMode: $MOUNT_MODE" |
|
done |
|
} |
|
|
|
info_container() { |
|
local name=$1 |
|
local meta="$CONTAINER_CONFIG/.$name.meta" |
|
local pwd="$CONTAINER_CONFIG/.$name.pwd" |
|
|
|
if [ ! -f "$meta" ]; then |
|
log_error "No metadata found for $name" |
|
exit 1 |
|
fi |
|
|
|
source "$meta" |
|
echo "--------------------------------" |
|
echo "Container: $NAME" |
|
echo "Created: $CREATED" |
|
echo "Network: $NET_ENABLED" |
|
echo "Mount: $MOUNT_SRC ($MOUNT_MODE)" |
|
echo "--------------------------------" |
|
if [ -f "$pwd" ]; then |
|
echo -e "Password: ${YELLOW}$(cat "$pwd")${NC}" |
|
fi |
|
echo "--------------------------------" |
|
} |
|
|
|
# --- Main Dispatch --- |
|
|
|
check_dependencies |
|
setup_directories |
|
|
|
CMD="${1:-help}" |
|
shift || true |
|
|
|
case "$CMD" in |
|
create) create_container "$@" ;; |
|
start) [ -z "${1:-}" ] && log_error "Name required" && exit 1; start_container "$1" ;; |
|
stop) [ -z "${1:-}" ] && log_error "Name required" && exit 1; stop_container "$1" ;; |
|
delete) [ -z "${1:-}" ] && log_error "Name required" && exit 1; delete_container "$1" ;; |
|
shell) [ -z "${1:-}" ] && log_error "Name required" && exit 1; shell_container "$1" ;; |
|
list) list_containers ;; |
|
info) [ -z "${1:-}" ] && log_error "Name required" && exit 1; info_container "$1" ;; |
|
*) show_help ;; |
|
esac |
|
|
|
echo " create <name> - Create a new container" |
|
echo " start <name> - Start a container" |
|
echo " shell <name> - Open shell in container" |
|
echo " stop <name> - Stop a container" |
|
echo " list - List all containers" |
|
exit 1 |
|
;; |
|
esac |