Files
appa-net/pfsense.home.arpa/scripts/backup-pfsense-config.sh
Kenji Morishige 2f0a95674c Fix: Resolve relative path issue in backup script git operations
- Convert backup file path to absolute path before git add/commit
- Fixes 'pathspec did not match any files' error when using ../
- Use absolute_backup_path consistently in commit_to_git()
- Tested successfully: backups now commit to git without errors
2026-04-22 14:44:19 -05:00

391 lines
12 KiB
Bash
Executable File

#!/bin/bash
################################################################################
# pfSense Configuration Backup Utility
#
# This script automatically backs up your pfSense router configuration and
# stores it in the repository with a timestamped filename.
#
# Prerequisites:
# - SSH access to pfSense router (with public key authentication)
# - SSH host entry configured (default: 'pfsense' in ~/.ssh/config)
# - Git repository initialized in the current working directory
# - Sufficient disk space for configuration backup (typically < 1MB)
#
# Usage:
# ./backup-pfsense-config.sh # Uses default host 'pfsense'
# ./backup-pfsense-config.sh pfsense.home.arpa # Specify custom host
# ./backup-pfsense-config.sh -h # Show help
#
################################################################################
set -euo pipefail
# Configuration (set defaults)
PFSENSE_HOST="pfsense" # SSH host (from ~/.ssh/config or IP)
PFSENSE_USER="${PFSENSE_USER:-root}" # SSH user
PFSENSE_CONFIG_PATH="/conf/config.xml"
BACKUP_DIR="$(dirname "$0")/../backups"
TIMESTAMP="$(date +%Y-%m-%d_%H%M%S)"
BACKUP_FILE="${BACKUP_DIR}/pfsense-config-${TIMESTAMP}.xml"
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
AUTO_COMMIT="${AUTO_COMMIT:-true}"
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
################################################################################
# Helper Functions
################################################################################
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[✓]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
show_help() {
cat << EOF
Usage: $(basename "$0") [OPTIONS] [HOST]
Backup pfSense router configuration to repository.
ARGUMENTS:
HOST SSH host or IP address (default: 'pfsense')
Should be an entry in ~/.ssh/config or IP address
Example: pfsense, 192.168.1.1, pfsense.home.arpa
OPTIONS:
-h, --help Show this help message
-n, --no-commit Download config without auto-committing to git
--dry-run Show what would be done without making changes
ENVIRONMENT VARIABLES:
PFSENSE_USER SSH user (default: 'root')
AUTO_COMMIT Whether to auto-commit (default: 'true')
EXAMPLES:
# Backup using default host and auto-commit
$ ./backup-pfsense-config.sh
# Backup specific host
$ ./backup-pfsense-config.sh 172.27.0.1
# Backup without auto-commit
$ ./backup-pfsense-config.sh -n
# Dry run (test connectivity)
$ ./backup-pfsense-config.sh --dry-run
CONFIGURATION:
SSH host entry: ${PFSENSE_HOST}
SSH user: ${PFSENSE_USER}
Backup directory: ${BACKUP_DIR}
Config file: ${PFSENSE_CONFIG_PATH}
NOTES:
- Requires SSH public key authentication to pfSense
- First run may prompt to accept SSH key fingerprint
- Backup files are stored with timestamps (YYYY-MM-DD_HHMMSS)
- Backups are committed to git with descriptive messages
- Each backup is human-readable XML for easy diffs
EOF
}
check_prerequisites() {
log_info "Checking prerequisites..."
# Check SSH connectivity
if ! command -v ssh &> /dev/null; then
log_error "SSH is not installed or not in PATH"
return 1
fi
log_success "SSH is available"
# Check git
if ! command -v git &> /dev/null; then
log_error "Git is not installed or not in PATH"
return 1
fi
log_success "Git is available"
# Check backup directory exists
if [[ ! -d "${BACKUP_DIR}" ]]; then
log_warning "Backup directory does not exist: ${BACKUP_DIR}"
log_info "Creating backup directory..."
mkdir -p "${BACKUP_DIR}" || {
log_error "Failed to create backup directory"
return 1
}
log_success "Backup directory created"
fi
# Verify we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
log_error "Not in a git repository (checked from $(pwd))"
return 1
fi
log_success "Git repository found"
return 0
}
test_ssh_connection() {
log_info "Testing SSH connection to ${PFSENSE_HOST}..."
if ssh -o ConnectTimeout=10 "${PFSENSE_USER}@${PFSENSE_HOST}" "echo 'SSH connection successful'" > /dev/null 2>&1; then
log_success "SSH connection to ${PFSENSE_HOST} successful"
return 0
else
log_error "Failed to connect to ${PFSENSE_HOST} via SSH"
log_info "Troubleshooting tips:"
log_info " 1. Verify SSH host is configured: ssh-keygen -F ${PFSENSE_HOST}"
log_info " 2. Test SSH manually: ssh ${PFSENSE_USER}@${PFSENSE_HOST}"
log_info " 3. Check firewall allows SSH (port 22) from your machine"
log_info " 4. Verify public key is installed on pfSense: /root/.ssh/authorized_keys"
return 1
fi
}
fetch_config() {
log_info "Fetching pfSense configuration from ${PFSENSE_HOST}..."
# Use scp to copy the remote file
if ! scp "${PFSENSE_USER}@${PFSENSE_HOST}:${PFSENSE_CONFIG_PATH}" "${BACKUP_FILE}" 2>/dev/null; then
log_error "Failed to download configuration from ${PFSENSE_HOST}"
return 1
fi
if [[ ! -f "${BACKUP_FILE}" ]]; then
log_error "Backup file was not created: ${BACKUP_FILE}"
return 1
fi
log_success "Configuration downloaded to ${BACKUP_FILE}"
# Show file size
local file_size
file_size=$(du -h "${BACKUP_FILE}" | cut -f1)
log_info "Backup size: ${file_size}"
return 0
}
validate_config() {
log_info "Validating backup file..."
# Check if file is valid XML
if ! grep -q '<?xml' "${BACKUP_FILE}"; then
log_error "Backup file does not appear to be valid XML"
log_warning "Removing invalid backup: ${BACKUP_FILE}"
rm -f "${BACKUP_FILE}"
return 1
fi
# Check for pfSense-specific markers
if ! grep -q '<pfsense' "${BACKUP_FILE}"; then
log_warning "Backup file may not be a valid pfSense configuration"
log_info "File contents (first 20 lines):"
head -20 "${BACKUP_FILE}"
return 1
fi
log_success "Backup file is valid XML with pfSense markers"
return 0
}
commit_to_git() {
log_info "Committing backup to git repository..."
# Convert to absolute path to avoid issues with relative paths containing ../
local absolute_backup_path
absolute_backup_path="$(cd "$(dirname "${BACKUP_FILE}")" && pwd)/$(basename "${BACKUP_FILE}")"
if [[ ! -f "${absolute_backup_path}" ]]; then
log_error "Backup file not found at: ${absolute_backup_path}"
return 1
fi
# Add the backup file to git using absolute path
if git -C "${REPO_ROOT}" add "${absolute_backup_path}"; then
log_success "Added backup to git staging area"
else
log_error "Failed to add backup to git"
return 1
fi
# Create a meaningful commit message
local short_date
short_date=$(date +%Y-%m-%d\ %H:%M:%S)
local commit_msg="pfSense: Backup configuration (${short_date})"
# Get configuration summary from XML
local vlan_count
vlan_count=$(grep -c '<vlan>' "${absolute_backup_path}" 2>/dev/null || echo "0")
local rule_count
rule_count=$(grep -c '<rule>' "${absolute_backup_path}" 2>/dev/null || echo "0")
# Enhanced commit message with details
local detailed_msg="${commit_msg}
Contains:
- VLANs: ${vlan_count}
- Firewall rules: ${rule_count}
- Backup timestamp: ${TIMESTAMP}
File: $(basename "${BACKUP_FILE}")
SHA256: $(shasum -a 256 "${absolute_backup_path}" | awk '{print $1}')
"
# Commit the backup
if git -C "${REPO_ROOT}" commit -m "${detailed_msg}" > /dev/null 2>&1; then
log_success "Configuration committed to git"
log_info "Commit message:"
echo "${detailed_msg}" | sed 's/^/ /'
return 0
else
log_warning "No changes to commit (file may be identical to previous backup)"
return 0
fi
}
show_summary() {
echo ""
echo "════════════════════════════════════════════════════════════════"
log_success "pfSense configuration backup completed successfully!"
echo "════════════════════════════════════════════════════════════════"
echo ""
echo "Backup Details:"
echo " Location: ${BACKUP_FILE}"
echo " Size: $(du -h "${BACKUP_FILE}" | cut -f1)"
echo " Timestamp: ${TIMESTAMP}"
echo " From: ${PFSENSE_USER}@${PFSENSE_HOST}:${PFSENSE_CONFIG_PATH}"
echo ""
echo "Next Steps:"
echo " 1. Review the changes: git show HEAD"
echo " 2. Push to remote: git push origin main"
echo " 3. Schedule automated backups in crontab"
echo ""
echo "To schedule daily backups, add to crontab:"
echo " 0 2 * * * cd $(pwd) && ./scripts/backup-pfsense-config.sh"
echo ""
}
################################################################################
# Main Execution
################################################################################
main() {
# Parse arguments properly (flags first, then positional)
local dry_run=false
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
show_help
exit 0
;;
-n|--no-commit)
AUTO_COMMIT="false"
shift
;;
--dry-run)
dry_run=true
shift
;;
-*)
log_error "Unknown option: $1"
show_help
exit 1
;;
*)
# This is a positional argument (hostname)
PFSENSE_HOST="$1"
shift
;;
esac
done
echo ""
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ pfSense Configuration Backup Utility ║"
echo "╚════════════════════════════════════════════════════════════════╝"
echo ""
# Show configuration
log_info "Configuration:"
echo " Host: ${PFSENSE_HOST}"
echo " User: ${PFSENSE_USER}"
echo " Remote path: ${PFSENSE_CONFIG_PATH}"
echo " Backup dir: ${BACKUP_DIR}"
echo " Auto-commit: ${AUTO_COMMIT}"
echo ""
# Check prerequisites
if ! check_prerequisites; then
log_error "Prerequisites check failed"
exit 1
fi
echo ""
# Test SSH connection
if ! test_ssh_connection; then
exit 1
fi
echo ""
if [[ "${dry_run}" == "true" ]]; then
log_success "Dry-run completed successfully. SSH connection is working."
echo "To perform actual backup, run: $(basename "$0") ${PFSENSE_HOST}"
exit 0
fi
# Fetch configuration
if ! fetch_config; then
exit 1
fi
echo ""
# Validate the backup
if ! validate_config; then
exit 1
fi
echo ""
# Commit to git (if enabled)
if [[ "${AUTO_COMMIT}" == "true" ]]; then
if ! commit_to_git; then
log_warning "Backup was downloaded but git commit may have failed"
exit 1
fi
else
log_info "Skipping git commit (AUTO_COMMIT=false)"
fi
# Show summary
echo ""
show_summary
}
# Run main function
main "$@"