Skip to main content

System Architecture

The Proxmox VE Helper Scripts use a modular architecture with multiple function libraries that work together to create and configure LXC containers and VMs.

Component Overview

┌─────────────────────────────────────────────────────────────┐
│                    Installation Script                       │
│  (pihole-install.sh, docker-install.sh, etc.)              │
└────────────────────┬────────────────────────────────────────┘

                     v
┌─────────────────────────────────────────────────────────────┐
│                   build.func Library                         │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  variables()                                         │   │
│  │  - Initialize NSAPP, var_install, etc.             │   │
│  └──────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  install_script()                                    │   │
│  │  - Display mode menu                                │   │
│  │  - Route to appropriate workflow                    │   │
│  └──────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  base_settings()                                     │   │
│  │  - Apply built-in defaults                          │   │
│  │  - Read environment variables (var_*)               │   │
│  └──────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  load_vars_file()                                    │   │
│  │  - Safe file parsing (NO source/eval)              │   │
│  │  - Whitelist validation                             │   │
│  │  - Value sanitization                               │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

                     v
┌─────────────────────────────────────────────────────────────┐
│           Configuration Files (on Disk)                      │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  /usr/local/community-scripts/default.vars          │   │
│  │  (User global defaults)                             │   │
│  └──────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  /usr/local/community-scripts/defaults/*.vars       │   │
│  │  (App-specific defaults)                            │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Configuration File Format

User Defaults: default.vars

Location: /usr/local/community-scripts/default.vars Format Specification:
# File Format: Simple key=value pairs
# Purpose: Store global user defaults
# Security: Sanitized values, whitelist validation

# Comments and blank lines are ignored
# Line format: var_name=value
# No spaces around the equals sign
# String values do not need quoting (but may be quoted)

[CONTENT]
var_cpu=4
var_ram=2048
var_disk=20
var_hostname=mydefault
var_brg=vmbr0
var_gateway=192.168.1.1
FILE       := (BLANK_LINE | COMMENT_LINE | VAR_LINE)*
BLANK_LINE := \n
COMMENT_LINE := '#' [^\n]* \n
VAR_LINE   := VAR_NAME '=' VAR_VALUE \n
VAR_NAME   := 'var_' [a-z_]+
VAR_VALUE  := [^\n]*  # Any printable characters except newline
Constraints:
ConstraintValue
Max file size64 KB
Max line length1024 bytes
Max variables100
Allowed var namesvar_[a-z_]+
Value validationWhitelist + Sanitization

App Defaults: <app>.vars

Location: /usr/local/community-scripts/defaults/<appname>.vars Naming Convention: <nsapp>.vars
  • nsapp = lowercase app name with spaces removed
  • Examples:
    • piholepihole.vars
    • opnsenseopnsense.vars
    • docker composedockercompose.vars

Variable Precedence

Loading Order

When a container is being created, variables are resolved in this order:
Step 1: Read ENVIRONMENT VARIABLES
   ├─ Check if var_cpu is already set in shell environment
   ├─ Check if var_ram is already set
   └─ ...all var_* variables

Step 2: Load APP-SPECIFIC DEFAULTS
   ├─ Check if /usr/local/community-scripts/defaults/pihole.vars exists
   ├─ Load all var_* from that file
   └─ These override built-ins but NOT environment variables

Step 3: Load USER GLOBAL DEFAULTS
   ├─ Check if /usr/local/community-scripts/default.vars exists
   ├─ Load all var_* from that file
   └─ These override built-ins but NOT app-specific

Step 4: Use BUILT-IN DEFAULTS
   └─ Hardcoded in script (lowest priority)

Precedence Examples

# Shell environment has highest priority
$ export var_cpu=16
$ bash pihole-install.sh

# Result: Container gets 16 cores
# (ignores app defaults, user defaults, built-ins)

Security Model

Threat Model

ThreatMitigation
Arbitrary Code ExecutionNo source or eval; manual parsing only
Variable InjectionWhitelist of allowed variable names
Command Substitution_sanitize_value() blocks $(), backticks, etc.
Path TraversalFiles locked to /usr/local/community-scripts/
Permission EscalationFiles created with restricted permissions
Information DisclosureSensitive variables not logged

Security Controls

# Only specific variables allowed
if ! _is_whitelisted_key "$key"; then
  skip_this_variable
fi

# Values sanitized
if ! val="$(_sanitize_value "$value")"; then
  reject_entire_line
fi
# ❌ DANGEROUS (OLD)
source /path/to/config.conf
# Could execute: rm -rf / or any code

# ✅ SAFE (NEW)
load_vars_file "/path/to/config.conf"
# Only reads var_name=value pairs, no execution
# Dangerous Patterns Blocked:
# - $(...)  : Command substitution
# - `...`   : Command substitution
# - ;       : Command separator
# - &       : Background execution
# - <(...)  : Process substitution

_sanitize_value() {
  case "$1" in
  *'$('* | *'`'* | *';'* | *'&'* | *'<('*)
    echo ""
    return 1  # Reject dangerous value
    ;;
  esac
  echo "$1"
  return 0
}
# Only these variables can be configured
var_cpu, var_ram, var_disk, var_brg, ...
var_hostname, var_pw, var_ssh, ...

# NOT allowed:
var_malicious, var_hack, custom_var, ...

Data Flow Diagrams

Installation Flow: Advanced Settings

┌──────────────┐
│  Start Script│
└──────┬───────┘

       v
┌──────────────────────────────┐
│ Display Installation Mode    │
│ Menu (5 options)             │
└──────┬───────────────────────┘
       │ User selects "Advanced Settings"
       v
┌──────────────────────────────────┐
│ Call: base_settings()            │
│ (Apply built-in defaults)        │
└──────┬───────────────────────────┘

       v
┌──────────────────────────────────┐
│ Call: advanced_settings()        │
│ (Show 19-step wizard)            │
│ - Ask CPU, RAM, Disk, Network... │
└──────┬───────────────────────────┘

       v
┌──────────────────────────────────┐
│ Show Summary                     │
│ Review all chosen values         │
└──────┬───────────────────────────┘
       │ User confirms
       v
┌──────────────────────────────────┐
│ Create Container                 │
│ Using current variable values    │
└──────┬───────────────────────────┘

       v
┌──────────────────────────────────┐
│ Installation Complete            │
└──────┬───────────────────────────┘

       v
┌──────────────────────────────────────┐
│ Offer: Save as App Defaults?         │
│ (Save current settings)              │
└──────┬───────────────────────────────┘

       ├─ YES → Save to defaults/<app>.vars

       └─ NO  → Exit

Variable Resolution Flow

CONTAINER CREATION STARTED

         v
   ┌─────────────────────┐
   │ Check ENVIRONMENT   │
   │ for var_cpu, var_..│
   └──────┬──────────────┘
          │ Found? Use them (Priority 1)
          │ Not found? Continue...
          v
   ┌──────────────────────────┐
   │ Load App Defaults        │
   │ /defaults/<app>.vars     │
   └──────┬───────────────────┘
          │ File exists? Parse & load (Priority 2)
          │ Not found? Continue...
          v
   ┌──────────────────────────┐
   │ Load User Defaults       │
   │ /default.vars            │
   └──────┬───────────────────┘
          │ File exists? Parse & load (Priority 3)
          │ Not found? Continue...
          v
   ┌──────────────────────────┐
   │ Use Built-in Defaults    │
   │ (Hardcoded values)       │
   └──────┬───────────────────┘

          v
   ┌──────────────────────────┐
   │ All Variables Resolved   │
   │ Ready for container      │
   │ creation                 │
   └──────────────────────────┘

Key Functions Reference

load_vars_file(filepath)

Purpose: Safely load variables from .vars files without using source or eval Returns:
  • 0 on success
  • 1 on error (file missing, parse error, etc.)
Implementation Pattern:
build.func:234-257
load_vars_file() {
  local file="$1"

  # File must exist
  [ -f "$file" ] || return 0

  # Parse line by line (not with source/eval)
  local line key val
  while IFS='=' read -r key val || [ -n "$key" ]; do
    # Skip comments and empty lines
    [[ "$key" =~ ^[[:space:]]*# ]] && continue
    [[ -z "$key" ]] && continue

    # Validate key is in whitelist
    _is_whitelisted_key "$key" || continue

    # Sanitize and export value
    val="$(_sanitize_value "$val")"
    [ $? -eq 0 ] && export "$key=$val"
  done < "$file"

  return 0
}

_sanitize_value(value)

Purpose: Remove dangerous characters/patterns from configuration values Returns:
  • 0 (success) + sanitized value on stdout
  • 1 (failure) + nothing if dangerous
Dangerous Patterns:
PatternThreatExample
$(...)Command substitution$(rm -rf /)
` `Command substitution`whoami`
;Command separatorvalue; rm -rf /
&Background executionvalue & malicious
<(Process substitution<(cat /etc/passwd)
The configuration system uses a whitelist approach combined with value sanitization to prevent code injection attacks. Never use source or eval with user-provided configuration files.

Build docs developers (and LLMs) love