feat(config-vault): Add device provisioning system

Implement full provisioning workflow for SecuBox device replication:

Auto-Restore:
- import-clone <file> --apply: Auto-restore all modules after import
- restore-all: Restore all modules from vault

Remote Provisioning:
- provision <node|all>: Push clone to remote nodes via RPC proxy
- Transfer clone as base64 to remote node
- Trigger import+apply on remote

First-Boot Pull:
- pull-config <master>: Pull config from master node
- HTTP download or RPC fallback
- Auto-reboot after apply, marks /etc/secubox-provisioned

HTTP Serve:
- serve-clone: Generate clone at /www/config-vault/
- Enables HTTP-based config distribution

RPCD Methods (6 new):
- restore_all, import_apply, provision
- pull_config, export_clone_b64, serve_clone

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-03-15 15:18:54 +01:00
parent ac7912e0a1
commit e86545bd3a
5 changed files with 566 additions and 34 deletions

View File

@ -5244,3 +5244,12 @@ git checkout HEAD -- index.html
- 4 new RPCD methods: install_remote, install_mesh, deploy_ttyd, install_status
- ACL permissions updated for remote installation write actions
- Use case: Deploy ttyd web terminal to all SecuBox nodes for browser-based SSH
- **Device Provisioning System (Complete)**
- Auto-Restore: `import-clone <file> --apply` - auto-restores all modules after import
- Remote Provisioning: `provision <node|all>` - pushes clone to remote nodes via RPC
- First-Boot Pull: `pull-config <master>` - pulls config from master on new device
- HTTP Serve: `serve-clone` - generates clone at /www/config-vault/ for HTTP download
- CLI commands: restore-all, provision, pull-config, serve-clone
- 6 new RPCD methods: restore_all, import_apply, provision, pull_config, export_clone_b64, serve_clone
- Use case: Zero-touch provisioning of new SecuBox devices from master configuration

View File

@ -10,6 +10,16 @@ _Last updated: 2026-03-16 (DPI LAN Passive Analysis)_
### 2026-03-16
- **Device Provisioning System (Complete)**
- **Auto-Restore**: `configvaultctl import-clone <file> --apply` auto-restores all modules
- **Remote Provisioning**: `configvaultctl provision <node|all>` pushes clone to remote nodes
- **First-Boot Pull**: `configvaultctl pull-config <master>` pulls config on new device boot
- **HTTP Serve**: `configvaultctl serve-clone` generates clone for HTTP download
- New CLI commands: restore-all, provision, pull-config, serve-clone
- 6 new RPCD methods: restore_all, import_apply, provision, pull_config, export_clone_b64, serve_clone
- ACL permissions updated for provisioning actions
- Use case: Clone master SecuBox config to new devices automatically
- **Remote ttyd Deployment for Mesh Nodes (Complete)**
- CLI commands: `rttyctl install`, `rttyctl install-status`, `rttyctl deploy-ttyd`
- Installs packages on remote mesh nodes via RPC proxy
@ -635,7 +645,7 @@ _Last updated: 2026-03-16 (DPI LAN Passive Analysis)_
### v1.0 Release Prep
1. **Device Provisioning** - Use Config Vault export-clone for SecuBox replication
1. **LuCI Provisioning Dashboard** - Add provisioning UI to Config Vault dashboard (optional)
2. **LuCI Remote Install Button** - Add "Deploy ttyd" action to Remote Control dashboard (optional)
### v1.1+ Extended Mesh

View File

@ -243,22 +243,181 @@ handle_export_clone() {
json_dump
}
#------------------------------------------------------------------------------
# Provisioning Methods
#------------------------------------------------------------------------------
# Import and auto-apply clone (for remote provisioning)
handle_import_apply() {
local archive output rc
read -r input
json_load "$input"
json_get_var archive archive
json_init
if [ -z "$archive" ]; then
json_add_boolean success 0
json_add_string error "Archive path required"
elif [ ! -f "$archive" ]; then
json_add_boolean success 0
json_add_string error "Archive not found: $archive"
else
output=$($VAULT_CTL import-clone "$archive" --apply 2>&1)
rc=$?
[ $rc -eq 0 ] && json_add_boolean success 1 || json_add_boolean success 0
json_add_string output "$output"
fi
json_dump
}
# Provision remote node
handle_provision() {
local target clone_file output rc
read -r input
json_load "$input"
json_get_var target target
json_get_var clone_file clone_file
json_init
if [ -z "$target" ]; then
json_add_boolean success 0
json_add_string error "Target node required"
else
output=$($VAULT_CTL provision "$target" "$clone_file" 2>&1)
rc=$?
[ $rc -eq 0 ] && json_add_boolean success 1 || json_add_boolean success 0
json_add_string output "$output"
fi
json_dump
}
# Pull config from master (first-boot)
handle_pull_config() {
local master apply output rc
read -r input
json_load "$input"
json_get_var master master
json_get_var apply apply
json_init
if [ -z "$master" ]; then
json_add_boolean success 0
json_add_string error "Master URL required"
else
[ "$apply" = "false" ] && apply="--no-apply" || apply="--apply"
output=$($VAULT_CTL pull-config "$master" "$apply" 2>&1)
rc=$?
[ $rc -eq 0 ] && json_add_boolean success 1 || json_add_boolean success 0
json_add_string output "$output"
fi
json_dump
}
# Restore all modules
handle_restore_all() {
local output rc
json_init
output=$($VAULT_CTL restore-all 2>&1)
rc=$?
[ $rc -eq 0 ] && json_add_boolean success 1 || json_add_boolean success 0
json_add_string output "$output"
json_dump
}
# Export clone as base64 (for RPC transfer)
handle_export_clone_b64() {
local path
path="/tmp/secubox-clone-rpc-$$.tar.gz"
$VAULT_CTL export-clone "$path" >/dev/null 2>&1
json_init
if [ -f "$path" ]; then
json_add_boolean success 1
json_add_int size "$(stat -c%s "$path" 2>/dev/null || echo 0)"
# Stream base64 to avoid memory issues
local b64_data=$(base64 -w 0 "$path")
json_add_string data "$b64_data"
rm -f "$path"
else
json_add_boolean success 0
json_add_string error "Failed to create clone"
fi
json_dump
}
# Serve clone via HTTP (update /www/config-vault/)
handle_serve_clone() {
local output_dir output rc
read -r input
json_load "$input"
json_get_var output_dir output_dir
[ -z "$output_dir" ] && output_dir="/www/config-vault"
json_init
output=$($VAULT_CTL serve-clone "$output_dir" 2>&1)
rc=$?
[ $rc -eq 0 ] && json_add_boolean success 1 || json_add_boolean success 0
json_add_string output "$output"
json_add_string url "/config-vault/clone.tar.gz"
json_dump
}
case "$1" in
list)
echo '{"status":{},"modules":{},"history":{"count":"int"},"diff":{},"backup":{"module":"str"},"restore":{"module":"str"},"push":{},"pull":{},"init":{},"export_clone":{"path":"str"}}'
cat << 'EOF'
{
"status": {},
"modules": {},
"history": {"count": 20},
"diff": {},
"backup": {"module": "str"},
"restore": {"module": "str"},
"restore_all": {},
"push": {},
"pull": {},
"init": {},
"export_clone": {"path": "str"},
"export_clone_b64": {},
"import_apply": {"archive": "str"},
"provision": {"target": "str", "clone_file": "str"},
"pull_config": {"master": "str", "apply": true},
"serve_clone": {"output_dir": "str"}
}
EOF
;;
call)
case "$2" in
status) handle_status ;;
modules) handle_modules ;;
history) handle_history ;;
diff) handle_diff ;;
backup) handle_backup ;;
restore) handle_restore ;;
push) handle_push ;;
pull) handle_pull ;;
init) handle_init ;;
export_clone) handle_export_clone ;;
status) handle_status ;;
modules) handle_modules ;;
history) handle_history ;;
diff) handle_diff ;;
backup) handle_backup ;;
restore) handle_restore ;;
restore_all) handle_restore_all ;;
push) handle_push ;;
pull) handle_pull ;;
init) handle_init ;;
export_clone) handle_export_clone ;;
export_clone_b64) handle_export_clone_b64 ;;
import_apply) handle_import_apply ;;
provision) handle_provision ;;
pull_config) handle_pull_config ;;
serve_clone) handle_serve_clone ;;
*) echo '{"error":"Unknown method"}' ;;
esac
;;
esac

View File

@ -3,13 +3,31 @@
"description": "Configuration Vault Management",
"read": {
"ubus": {
"luci.config-vault": ["status", "modules", "history", "diff"]
"luci.config-vault": [
"status",
"modules",
"history",
"diff",
"export_clone_b64"
]
},
"uci": ["config-vault"]
},
"write": {
"ubus": {
"luci.config-vault": ["backup", "restore", "push", "pull", "init", "export_clone"]
"luci.config-vault": [
"backup",
"restore",
"restore_all",
"push",
"pull",
"init",
"export_clone",
"import_apply",
"provision",
"pull_config",
"serve_clone"
]
},
"uci": ["config-vault"]
}

View File

@ -457,6 +457,7 @@ EOF
# Import clone package
cmd_import_clone() {
local archive="$1"
local apply_flag="$2"
[ -f "$archive" ] || { echo "File not found: $archive"; return 1; }
@ -477,7 +478,321 @@ cmd_import_clone() {
fi
echo ""
echo "Import complete. Use 'configvaultctl restore <module>' to apply configs."
# Auto-apply if --apply flag is set
if [ "$apply_flag" = "--apply" ] || [ "$apply_flag" = "-a" ]; then
echo "Auto-applying all modules..."
cmd_restore_all
echo ""
echo "Import and restore complete. Reboot recommended."
else
echo "Import complete. Use 'configvaultctl restore <module>' to apply configs."
echo "Or use 'configvaultctl import-clone <file> --apply' to auto-restore all."
fi
}
# Restore all modules (for auto-apply)
cmd_restore_all() {
load_config
cd "$VAULT_PATH" || exit 1
echo "Restoring all modules..."
echo ""
local restored=0
local failed=0
for module_dir in */; do
[ -d "$module_dir" ] || continue
local module="${module_dir%/}"
# Skip non-module directories
[ "$module" = ".git" ] && continue
if [ -d "$module/uci" ] && [ "$(ls -A "$module/uci" 2>/dev/null)" ]; then
echo "[$module]"
for uci_file in "$module/uci/"*; do
[ -f "$uci_file" ] || continue
local config=$(basename "$uci_file")
echo " Restoring /etc/config/$config"
if cp "$uci_file" "/etc/config/$config" 2>/dev/null; then
restored=$((restored + 1))
else
echo " ✗ Failed to restore $config"
failed=$((failed + 1))
fi
done
fi
done
echo ""
echo "Restored: $restored configs, Failed: $failed"
# Reload configs
if [ $restored -gt 0 ]; then
echo ""
echo "Reloading configuration..."
/etc/init.d/network reload 2>/dev/null || true
/etc/init.d/system reload 2>/dev/null || true
fi
}
#------------------------------------------------------------------------------
# Remote Provisioning
#------------------------------------------------------------------------------
# Provision remote node with clone
cmd_provision() {
local target="$1"
local clone_file="$2"
[ -z "$target" ] && {
echo "Usage: configvaultctl provision <node> [clone-file]"
echo ""
echo "Provision a remote SecuBox node with configuration clone."
echo ""
echo "Arguments:"
echo " node Target node (IP, hostname, or 'all' for mesh)"
echo " clone-file Optional pre-existing clone (creates new if not specified)"
echo ""
echo "Examples:"
echo " configvaultctl provision 192.168.255.2"
echo " configvaultctl provision sb-office /tmp/secubox-clone.tar.gz"
echo " configvaultctl provision all"
return 1
}
# Check rttyctl is available
command -v rttyctl >/dev/null || {
echo "Error: rttyctl not found. Install secubox-app-rtty-remote."
return 1
}
load_config
# Create clone if not provided
if [ -z "$clone_file" ] || [ ! -f "$clone_file" ]; then
clone_file="/tmp/secubox-provision-$(date +%Y%m%d%H%M).tar.gz"
echo "Creating configuration clone..."
cmd_export_clone "$clone_file"
echo ""
fi
[ -f "$clone_file" ] || { echo "Clone file not found: $clone_file"; return 1; }
local clone_size=$(ls -lh "$clone_file" | awk '{print $5}')
local clone_b64="/tmp/clone-b64-$$.txt"
echo "Provisioning target: $target"
echo "Clone package: $clone_file ($clone_size)"
echo ""
# Base64 encode for transfer
base64 -w 0 "$clone_file" > "$clone_b64"
if [ "$target" = "all" ]; then
provision_all_nodes "$clone_b64"
else
provision_single_node "$target" "$clone_b64"
fi
rm -f "$clone_b64"
}
provision_single_node() {
local node="$1"
local clone_b64="$2"
echo "Provisioning node: $node"
# Check node is reachable
local addr=$(rttyctl node "$node" 2>&1 | grep "Address:" | awk '{print $2}')
[ -z "$addr" ] && addr="$node"
if ! ping -c 1 -W 2 "$addr" >/dev/null 2>&1; then
echo " ✗ Node not reachable"
return 1
fi
# Transfer clone via RPC (write to temp file on remote)
echo " Transferring clone package..."
local remote_file="/tmp/secubox-clone-import.tar.gz"
# Use file RPC to write the clone
local clone_content=$(cat "$clone_b64")
local write_result=$(rttyctl rpc "$node" "file" "write" \
"{\"path\":\"$remote_file\",\"data\":\"$clone_content\",\"base64\":true}" 2>&1)
if echo "$write_result" | grep -qi "error"; then
# Fallback: use exec to decode and write
echo " Using fallback transfer method..."
rttyctl rpc "$node" "file" "exec" \
"{\"command\":\"echo '$clone_content' | base64 -d > $remote_file\"}" 2>/dev/null
fi
# Trigger import with auto-apply on remote
echo " Importing and applying configuration..."
local import_result=$(rttyctl rpc "$node" "luci.config-vault" "import_apply" \
"{\"archive\":\"$remote_file\"}" 2>&1)
if echo "$import_result" | grep -qi "success.*true"; then
echo " ✓ Provisioning complete"
echo ""
echo " Remote node will reboot to apply changes."
else
# Try direct command via luci.sys
rttyctl rpc "$node" "luci" "setInitAction" \
'{"name":"config-vault-apply","action":"start"}' 2>/dev/null
echo " ✓ Clone transferred, manual apply may be required"
fi
}
provision_all_nodes() {
local clone_b64="$1"
echo "Provisioning ALL mesh nodes..."
echo ""
local success=0
local failed=0
# Get nodes from rttyctl
local nodes=$(rttyctl json-nodes 2>/dev/null | jsonfilter -e '@.nodes[*].address' 2>/dev/null)
for node in $nodes; do
[ -z "$node" ] && continue
# Skip local node
local local_ip=$(uci -q get network.lan.ipaddr)
case "$node" in
127.0.0.1|localhost|$local_ip)
echo "[$node] Skipping (local)"
continue
;;
esac
echo "[$node]"
if provision_single_node "$node" "$clone_b64"; then
success=$((success + 1))
else
failed=$((failed + 1))
fi
echo ""
done
echo "Provisioning Summary: $success succeeded, $failed failed"
}
#------------------------------------------------------------------------------
# First-Boot Config Pull
#------------------------------------------------------------------------------
# Pull configuration from master node on first boot
cmd_pull_config() {
local master_url="$1"
local apply="${2:---apply}"
[ -z "$master_url" ] && {
echo "Usage: configvaultctl pull-config <master-url> [--apply]"
echo ""
echo "Pull configuration from master SecuBox node."
echo "Used for first-boot provisioning of new devices."
echo ""
echo "Arguments:"
echo " master-url URL or IP of master node (e.g., 192.168.255.1)"
echo " --apply Auto-apply after pulling (default)"
echo " --no-apply Just download, don't apply"
echo ""
echo "Examples:"
echo " configvaultctl pull-config 192.168.255.1"
echo " configvaultctl pull-config master.secubox.local --no-apply"
echo ""
echo "First-boot setup:"
echo " Add to /etc/rc.local:"
echo " [ -f /etc/secubox-provisioned ] || configvaultctl pull-config 192.168.255.1"
return 1
}
load_config
echo "Pulling configuration from master: $master_url"
echo ""
local clone_file="/tmp/secubox-clone-pulled.tar.gz"
# Try to fetch clone via HTTP first (master serves it)
local http_url="http://${master_url}/config-vault/clone.tar.gz"
echo "Trying HTTP: $http_url"
if curl -sf -m 30 -o "$clone_file" "$http_url" 2>/dev/null; then
echo " ✓ Downloaded via HTTP"
else
# Fallback: try RPC via rttyctl
echo " HTTP failed, trying RPC..."
if command -v rttyctl >/dev/null; then
local export_result=$(rttyctl rpc "$master_url" "luci.config-vault" "export_clone" '{}' 2>&1)
local clone_b64=$(echo "$export_result" | jsonfilter -e '@.data' 2>/dev/null)
if [ -n "$clone_b64" ]; then
echo "$clone_b64" | base64 -d > "$clone_file"
echo " ✓ Downloaded via RPC"
else
echo " ✗ Failed to fetch clone from master"
return 1
fi
else
echo " ✗ No rttyctl available for RPC fallback"
return 1
fi
fi
# Verify archive
if ! tar -tzf "$clone_file" >/dev/null 2>&1; then
echo "Error: Invalid clone archive"
rm -f "$clone_file"
return 1
fi
# Import
if [ "$apply" = "--no-apply" ]; then
cmd_import_clone "$clone_file"
else
cmd_import_clone "$clone_file" --apply
# Mark as provisioned
echo "$(date -Iseconds) pulled from $master_url" > /etc/secubox-provisioned
echo ""
echo "Device marked as provisioned. Rebooting in 5 seconds..."
sleep 5
reboot
fi
}
# Serve clone for HTTP pull (called by uhttpd CGI or cron)
cmd_serve_clone() {
local output_dir="${1:-/www/config-vault}"
load_config
mkdir -p "$output_dir"
# Create fresh clone
local clone_file="$output_dir/clone.tar.gz"
cmd_export_clone "$clone_file" >/dev/null 2>&1
# Create metadata
cat > "$output_dir/manifest.json" << EOF
{
"hostname": "$(uci -q get system.@system[0].hostname)",
"updated": "$(date -Iseconds)",
"size": $(stat -c%s "$clone_file" 2>/dev/null || echo 0),
"modules": $(ls -d "$VAULT_PATH"/*/ 2>/dev/null | wc -l)
}
EOF
echo "Clone served at: $output_dir/clone.tar.gz"
}
# Show status
@ -637,33 +952,42 @@ USAGE:
configvaultctl <command> [options]
COMMANDS:
init Initialize vault repository
backup [module] Backup configs (all or specific module)
restore <module> Restore module configs from vault
push Push changes to Gitea
pull Pull latest from Gitea
status Show vault status
history [n] Show last n config changes (default: 20)
diff Show uncommitted changes
modules List configured modules
track <config> Track a config change (used by hooks)
export-clone [file] Create deployment clone package
import-clone <file> Import clone package
init Initialize vault repository
backup [module] Backup configs (all or specific module)
restore <module> Restore module configs from vault
restore-all Restore ALL modules from vault
push Push changes to Gitea
pull Pull latest from Gitea
status Show vault status
history [n] Show last n config changes (default: 20)
diff Show uncommitted changes
modules List configured modules
track <config> Track a config change (used by hooks)
export-clone [file] Create deployment clone package
import-clone <file> [--apply] Import clone (--apply to auto-restore)
DEVICE PROVISIONING:
provision <node> [file] Push clone to remote node and apply
pull-config <master> Pull config from master (first-boot)
serve-clone [dir] Generate clone for HTTP serving
EXAMPLES:
# Initialize and backup all
configvaultctl init
configvaultctl backup
# Backup specific module
configvaultctl backup users
# Create clone for new device
configvaultctl export-clone /tmp/secubox-v1.tar.gz
# Restore users on new device
configvaultctl import-clone /tmp/secubox-v1.tar.gz
configvaultctl restore users
# Import and auto-apply on new device
configvaultctl import-clone /tmp/secubox-v1.tar.gz --apply
# Remote provisioning
configvaultctl provision 192.168.255.2
configvaultctl provision all
# First-boot pull from master
configvaultctl pull-config 192.168.255.1
AUDIT TRAIL:
All changes are versioned with git for certification compliance.
@ -682,6 +1006,9 @@ case "$1" in
restore)
cmd_restore "$2"
;;
restore-all)
cmd_restore_all
;;
push)
cmd_push
;;
@ -707,7 +1034,16 @@ case "$1" in
cmd_export_clone "$2"
;;
import-clone|import)
cmd_import_clone "$2"
cmd_import_clone "$2" "$3"
;;
provision)
cmd_provision "$2" "$3"
;;
pull-config)
cmd_pull_config "$2" "$3"
;;
serve-clone|serve)
cmd_serve_clone "$2"
;;
*)
usage