Private
Public Access
1
0
Files
linux_patch_manager/scripts/backup.sh
git-echo 124b5b0e3b feat: add bump-version.sh script for version management
Automates version bumps across all version source files:
- Cargo.toml (PRIMARY - workspace.package.version)
- debian/changelog (prepend new entry)
- debian/control (update Version field)
- scripts/build-package.sh (update VERSION variable)
- frontend/package.json (update version field)
- Stale references check after bump

Usage: ./scripts/bump-version.sh <new_version> <old_version>
2026-05-28 10:52:16 -05:00

188 lines
7.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# Linux Patch Manager — Nightly Backup Script
# =============================================================================
# Run via cron or systemd timer.
# Performs:
# 1. pg_dump of the patch_manager database
# 2. CA material backup (/etc/patch-manager/ca/)
# 3. Config backup (/etc/patch-manager/config.toml, jwt keys, tls certs)
# - Secrets are excluded unless GPG_RECIPIENT is set for encryption
# =============================================================================
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
info() { echo -e "${GREEN}[INFO]${NC} $(date +%Y-%m-%dT%H:%M:%S) $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $(date +%Y-%m-%dT%H:%M:%S) $*"; }
error() { echo -e "${RED}[ERROR]${NC} $(date +%Y-%m-%dT%H:%M:%S) $*" >&2; }
# ---------------------------------------------------------------------------
# Configuration (override via environment or config file)
# ---------------------------------------------------------------------------
BACKUP_DIR="${BACKUP_DIR:-/var/backups/patch-manager}"
DB_NAME="${DB_NAME:-patch_manager}"
DB_USER="${DB_USER:-patch_manager}"
CONFIG_DIR="/etc/patch-manager"
RETENTION_DAYS="${RETENTION_DAYS:-30}"
GPG_RECIPIENT="${GPG_RECIPIENT:-}" # Set to GPG key ID to encrypt secret backups
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# ---------------------------------------------------------------------------
# Pre-flight checks
# ---------------------------------------------------------------------------
[[ $EUID -ne 0 ]] && error "This script must be run as root."
command -v pg_dump >/dev/null 2>&1 || error "pg_dump not found. Install postgresql-client."
command -v gpg >/dev/null 2>&1 || { [[ -n "${GPG_RECIPIENT}" ]] && error "GPG_RECIPIENT set but gpg not found." || true; }
mkdir -p "${BACKUP_DIR}"
chmod 700 "${BACKUP_DIR}"
BACKUP_SUCCESS=true
# ---------------------------------------------------------------------------
# 1. Database backup (pg_dump, custom format for parallel restore)
# ---------------------------------------------------------------------------
info "Starting database backup..."
DB_FILE="${BACKUP_DIR}/patch_manager_db_${TIMESTAMP}.dump"
if sudo -u postgres pg_dump -Fc -f "${DB_FILE}" "${DB_NAME}" 2>/dev/null; then
chmod 600 "${DB_FILE}"
chown root:root "${DB_FILE}"
SIZE=$(du -h "${DB_FILE}" | cut -f1)
info "Database backup complete: ${DB_FILE} (${SIZE})"
else
error "Database backup FAILED."
BACKUP_SUCCESS=false
fi
# ---------------------------------------------------------------------------
# 2. CA material backup
# ---------------------------------------------------------------------------
info "Starting CA material backup..."
CA_DIR="${CONFIG_DIR}/ca"
if [[ -d "${CA_DIR}" ]]; then
CA_FILE="${BACKUP_DIR}/patch_manager_ca_${TIMESTAMP}.tar.gz.gpg"
CA_TAR=$(mktemp /tmp/pm_ca_backup_XXXXXX.tar.gz)
tar -czf "${CA_TAR}" -C "$(dirname "${CA_DIR}")" ca/ 2>/dev/null
if [[ -n "${GPG_RECIPIENT}" ]]; then
# Encrypt with GPG — CA key is the most critical secret
if gpg --batch --encrypt --recipient "${GPG_RECIPIENT}" --output "${CA_FILE}" "${CA_TAR}" 2>/dev/null; then
chmod 600 "${CA_FILE}"
CA_SIZE=$(du -h "${CA_FILE}" | cut -f1)
info "CA backup (encrypted): ${CA_FILE} (${CA_SIZE})"
else
error "CA backup GPG encryption FAILED."
BACKUP_SUCCESS=false
fi
rm -f "${CA_TAR}"
else
# No GPG recipient — store unencrypted with strict permissions
warn "GPG_RECIPIENT not set. CA backup stored UNENCRYPTED with strict permissions."
CA_FILE_UNENCRYPTED="${BACKUP_DIR}/patch_manager_ca_${TIMESTAMP}.tar.gz"
mv "${CA_TAR}" "${CA_FILE_UNENCRYPTED}"
chmod 600 "${CA_FILE_UNENCRYPTED}"
chown root:root "${CA_FILE_UNENCRYPTED}"
CA_SIZE=$(du -h "${CA_FILE_UNENCRYPTED}" | cut -f1)
info "CA backup (unencrypted): ${CA_FILE_UNENCRYPTED} (${CA_SIZE})"
fi
else
warn "CA directory not found at ${CA_DIR}, skipping CA backup."
fi
# ---------------------------------------------------------------------------
# 3. Config backup (excluding secrets unless encrypted destination)
# ---------------------------------------------------------------------------
info "Starting config backup..."
CONFIG_FILE="${BACKUP_DIR}/patch_manager_config_${TIMESTAMP}.tar.gz"
CONFIG_FILE_GPG="${BACKUP_DIR}/patch_manager_config_${TIMESTAMP}.tar.gz.gpg"
CONFIG_TAR=$(mktemp /tmp/pm_config_backup_XXXXXX.tar.gz)
# Build file list — always include non-secret config files
CONFIG_FILES=(
"${CONFIG_DIR}/config.toml"
"${CONFIG_DIR}/jwt/verify.pem"
)
# Include TLS cert (public) if present
if [[ -f "${CONFIG_DIR}/tls/tls.crt" ]]; then
CONFIG_FILES+=("${CONFIG_DIR}/tls/tls.crt")
fi
# Build tar from existing files only
EXISTING_FILES=()
for f in "${CONFIG_FILES[@]}"; do
[[ -f "${f}" ]] && EXISTING_FILES+=("${f}")
done
if [[ ${#EXISTING_FILES[@]} -gt 0 ]]; then
tar -czf "${CONFIG_TAR}" "${EXISTING_FILES[@]}" 2>/dev/null
# If GPG_RECIPIENT is set, include secrets in the backup (encrypted)
if [[ -n "${GPG_RECIPIENT}" ]]; then
# Add secret files to a separate encrypted archive
SECRET_FILES=()
[[ -f "${CONFIG_DIR}/jwt/signing.pem" ]] && SECRET_FILES+=("${CONFIG_DIR}/jwt/signing.pem")
[[ -f "${CONFIG_DIR}/tls/tls.key" ]] && SECRET_FILES+=("${CONFIG_DIR}/tls/tls.key")
[[ -f "${CONFIG_DIR}/config.toml" ]] && SECRET_FILES+=("${CONFIG_DIR}/config.toml") # May contain DB URL
if [[ ${#SECRET_FILES[@]} -gt 0 ]]; then
# Re-create tar with secrets included
ALL_FILES=("${EXISTING_FILES[@]}" "${SECRET_FILES[@]}")
# Deduplicate
ALL_FILES_UNIQUE=( $(echo "${ALL_FILES[@]}" | tr ' ' '\n' | sort -u) )
rm -f "${CONFIG_TAR}"
tar -czf "${CONFIG_TAR}" "${ALL_FILES_UNIQUE[@]}" 2>/dev/null
fi
gpg --batch --encrypt --recipient "${GPG_RECIPIENT}" --output "${CONFIG_FILE_GPG}" "${CONFIG_TAR}" 2>/dev/null
chmod 600 "${CONFIG_FILE_GPG}"
rm -f "${CONFIG_TAR}"
CFG_SIZE=$(du -h "${CONFIG_FILE_GPG}" | cut -f1)
info "Config backup (encrypted, secrets included): ${CONFIG_FILE_GPG} (${CFG_SIZE})"
else
# No encryption — secrets excluded, only public config
mv "${CONFIG_TAR}" "${CONFIG_FILE}"
chmod 600 "${CONFIG_FILE}"
chown root:root "${CONFIG_FILE}"
CFG_SIZE=$(du -h "${CONFIG_FILE}" | cut -f1)
info "Config backup (secrets excluded): ${CONFIG_FILE} (${CFG_SIZE})"
fi
else
warn "No config files found, skipping config backup."
fi
# ---------------------------------------------------------------------------
# 4. Retention cleanup
# ---------------------------------------------------------------------------
info "Cleaning up backups older than ${RETENTION_DAYS} days..."
DELETED_COUNT=0
for pattern in "patch_manager_db_" "patch_manager_ca_" "patch_manager_config_"; do
while IFS= read -r -d '' old_file; do
rm -f "${old_file}"
((DELETED_COUNT++)) || true
done < <(find "${BACKUP_DIR}" -name "${pattern}*" -mtime +"${RETENTION_DAYS}" -print0)
done
info "Removed ${DELETED_COUNT} expired backup(s)."
# ---------------------------------------------------------------------------
# 5. Summary
# ---------------------------------------------------------------------------
if [[ "${BACKUP_SUCCESS}" == true ]]; then
info "=== Backup completed successfully ==="
info "Backup directory: ${BACKUP_DIR}"
info "Total size: $(du -sh "${BACKUP_DIR}" | cut -f1)"
info "RPO target: 24 hours (nightly schedule)"
exit 0
else
error "=== Backup completed WITH ERRORS ==="
exit 1
fi