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 - 4 new RPCD methods: install_remote, install_mesh, deploy_ttyd, install_status
- ACL permissions updated for remote installation write actions - ACL permissions updated for remote installation write actions
- Use case: Deploy ttyd web terminal to all SecuBox nodes for browser-based SSH - 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 ### 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)** - **Remote ttyd Deployment for Mesh Nodes (Complete)**
- CLI commands: `rttyctl install`, `rttyctl install-status`, `rttyctl deploy-ttyd` - CLI commands: `rttyctl install`, `rttyctl install-status`, `rttyctl deploy-ttyd`
- Installs packages on remote mesh nodes via RPC proxy - 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 ### 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) 2. **LuCI Remote Install Button** - Add "Deploy ttyd" action to Remote Control dashboard (optional)
### v1.1+ Extended Mesh ### v1.1+ Extended Mesh

View File

@ -243,9 +243,161 @@ handle_export_clone() {
json_dump 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 case "$1" in
list) 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) call)
case "$2" in case "$2" in
@ -255,10 +407,17 @@ case "$1" in
diff) handle_diff ;; diff) handle_diff ;;
backup) handle_backup ;; backup) handle_backup ;;
restore) handle_restore ;; restore) handle_restore ;;
restore_all) handle_restore_all ;;
push) handle_push ;; push) handle_push ;;
pull) handle_pull ;; pull) handle_pull ;;
init) handle_init ;; init) handle_init ;;
export_clone) handle_export_clone ;; 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
;; ;;
esac esac

View File

@ -3,13 +3,31 @@
"description": "Configuration Vault Management", "description": "Configuration Vault Management",
"read": { "read": {
"ubus": { "ubus": {
"luci.config-vault": ["status", "modules", "history", "diff"] "luci.config-vault": [
"status",
"modules",
"history",
"diff",
"export_clone_b64"
]
}, },
"uci": ["config-vault"] "uci": ["config-vault"]
}, },
"write": { "write": {
"ubus": { "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"] "uci": ["config-vault"]
} }

View File

@ -457,6 +457,7 @@ EOF
# Import clone package # Import clone package
cmd_import_clone() { cmd_import_clone() {
local archive="$1" local archive="$1"
local apply_flag="$2"
[ -f "$archive" ] || { echo "File not found: $archive"; return 1; } [ -f "$archive" ] || { echo "File not found: $archive"; return 1; }
@ -477,7 +478,321 @@ cmd_import_clone() {
fi fi
echo "" echo ""
# 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 "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 # Show status
@ -640,6 +955,7 @@ COMMANDS:
init Initialize vault repository init Initialize vault repository
backup [module] Backup configs (all or specific module) backup [module] Backup configs (all or specific module)
restore <module> Restore module configs from vault restore <module> Restore module configs from vault
restore-all Restore ALL modules from vault
push Push changes to Gitea push Push changes to Gitea
pull Pull latest from Gitea pull Pull latest from Gitea
status Show vault status status Show vault status
@ -648,22 +964,30 @@ COMMANDS:
modules List configured modules modules List configured modules
track <config> Track a config change (used by hooks) track <config> Track a config change (used by hooks)
export-clone [file] Create deployment clone package export-clone [file] Create deployment clone package
import-clone <file> Import 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: EXAMPLES:
# Initialize and backup all # Initialize and backup all
configvaultctl init configvaultctl init
configvaultctl backup configvaultctl backup
# Backup specific module
configvaultctl backup users
# Create clone for new device # Create clone for new device
configvaultctl export-clone /tmp/secubox-v1.tar.gz configvaultctl export-clone /tmp/secubox-v1.tar.gz
# Restore users on new device # Import and auto-apply on new device
configvaultctl import-clone /tmp/secubox-v1.tar.gz configvaultctl import-clone /tmp/secubox-v1.tar.gz --apply
configvaultctl restore users
# 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: AUDIT TRAIL:
All changes are versioned with git for certification compliance. All changes are versioned with git for certification compliance.
@ -682,6 +1006,9 @@ case "$1" in
restore) restore)
cmd_restore "$2" cmd_restore "$2"
;; ;;
restore-all)
cmd_restore_all
;;
push) push)
cmd_push cmd_push
;; ;;
@ -707,7 +1034,16 @@ case "$1" in
cmd_export_clone "$2" cmd_export_clone "$2"
;; ;;
import-clone|import) 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 usage