Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This script automates encrypted, deduplicated backups of local directories using
- Checks the latest Restic release, downloads checksums and PGP signature.
- Verifies the signature and checksum before installing (x86_64 and aarch64 supported).
- **Optional script self-update (interactive)**: Checks GitHub release, downloads, verifies checksum, and updates the script.
- **3-2-1 backup support**: Supports 3-2-1 backup architecture via the `--fallback` flag.

-----

Expand Down Expand Up @@ -102,6 +103,7 @@ For those familiar with setting up backup scripts, here is a fast track to get y
- `sudo ./restic-backup.sh --ls [snapshot_id] [path ...]` — List files/dirs within a snapshot (paged with `less`).
- `sudo ./restic-backup.sh --find <pattern...>` - Search for files/dirs across all snapshots (e.g., --find \"*.log\" -l).
- `sudo ./restic-backup.sh --recovery-kit` - Generate a self-contained recovery script (with embedded password).
- `sudo ./restic-backup.sh --fallback` - Temporarily route command to the fallback repository.

Tip: `--verbose` is interactive; cron should use the default quiet mode. The script auto-reexecs with sudo if not run as root.

Expand Down Expand Up @@ -177,6 +179,93 @@ sudo ./restic-backup.sh --sync-restore latest /srv/app/data && systemctl restart

-----

## 3-2-1 Backup Strategy & Fallback Mode

This script supports 3-2-1 backup architecture via the `--fallback` flag.

If your primary cloud repository (e.g., a Hetzner Storage Box or AWS S3) becomes unreachable, you can instantly route any read/restore command to a secondary mirrored repository (e.g., a local NAS) without altering your core configuration.

### Prerequisite & Core Assumption

**This script does not automatically replicate or synchronize your data between different storage backends.** To utilize Fallback Mode, **it is assumed that you have independently configured an automated synchronization or mirroring task** (using tools like `rsync`, `rclone`, or your storage platform's native replication tools) that pulls or pushes a **1:1 exact copy** of your primary Restic repository directory to your local NAS or alternative remote server.

Because Restic repositories are completely self-contained and portable, this mirrored directory is immediately readable as a valid repository by the script.

### How it Works

1. **The Mirror:** Your external sync task (e.g., a TrueNAS Rsync Pull task) runs on its own schedule to mirror your primary repository (like a Hetzner Storage Box) to a secondary location (like a home server).
2. **The Config:** Define your secondary mirrored repository URL in `restic-backup.conf`:

```bash
RESTIC_FALLBACK_REPOSITORY="sftp:local-nas:/mnt/backups/server_01"
# Optional: Set RESTIC_FALLBACK_PASSWORD_FILE if the secondary uses a different key
```

3. **The Execution:** Append `--fallback` to any operational command. The script will dynamically swap the environment variables and route the traffic to your mirror.

### Fallback Use Cases

The fallback flag safely works with all non-destructive commands. It is highly recommended to use it for restores if your primary connection is degraded or severed:

```sh
# Check the integrity of your local mirrored copy
sudo ./restic-backup.sh --fallback --check

# Browse the files inside your local mirror
sudo ./restic-backup.sh --fallback --ls latest

# Execute an interactive restore directly from the local mirror
sudo ./restic-backup.sh --fallback --restore
```

> **Warning:** Do not run standard backup jobs or `--forget` operations using the `--fallback` flag if your secondary repository is maintained by a one-way sync (like Rsync). Writing new snapshots directly to a synchronized mirror can cause merge conflicts or data loss during the next sync cycle. Use fallback strictly for recovery and verification.

### Setting Up the Secure Fallback Connection

To securely connect a cloud VPS to a home server or NAS (like TrueNAS), **we highly recommend using an Overlay Network / Mesh VPN** (such as Tailscale or NetBird). This assigns a private, static IP to your NAS and allows the VPS to bypass NAT and firewalls without exposing your home SSH port to the public internet.

To configure this secure connection for Restic's fallback mode, follow these steps on your VPS:

**1. Generate and Copy SSH Keys**
Restic requires passwordless authentication to run automatically. Generate an SSH key on your VPS (if you don't already have one) and copy the public key to your secondary backup server.

```sh
# Generate a key as root (Press Enter for no passphrase)
sudo ssh-keygen -t ed25519 -f /root/.ssh/id_fallback_backup

# Copy the PUBLIC key to your NAS/TrueNAS admin user
# (Replace with your NAS user and Mesh VPN IP)
sudo ssh-copy-id -i /root/.ssh/id_fallback_backup.pub truenas_admin@100.x.x.x
```

**2. Configure the SSH Alias**
Create or edit the SSH config file for the root user to define exactly how the VPS should connect to the NAS. This alias (`local-nas`) is what you will use in your `RESTIC_FALLBACK_REPOSITORY` variable.

```sh
sudo nano /root/.ssh/config
```

Add the following block, adjusting the IP and user for your specific Mesh VPN setup:

```text
Host local-nas
HostName 100.x.x.x # Your Tailscale/NetBird IP
User truenas_admin # The user on your NAS
IdentityFile /root/.ssh/id_fallback_backup
StrictHostKeyChecking accept-new
```

**3. Test the Connection**
Verify that your VPS can successfully tunnel to the NAS without prompting for a password.

```sh
sudo ssh local-nas pwd
```

If this command successfully prints the working directory on your NAS, your secure pipeline is established. You can now set `RESTIC_FALLBACK_REPOSITORY="sftp:local-nas:/path/to/backup"` in your configuration file.

-----

## Diagnostics & Exit Codes

The script uses specific exit codes for different failures to help with debugging automated runs.
Expand Down
9 changes: 9 additions & 0 deletions restic-backup.conf
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,12 @@ EXCLUDE_FILE="/etc/restic-excludes.txt"

# Additional exclude patterns (space-separated)
EXCLUDE_PATTERNS="*.tmp *.log *.cache .DS_Store Thumbs.db"

# --- Fallback Repository (3-2-1 Backup Strategy) ---
# Use the --fallback flag to route commands to a secondary mirrored repository.
# Ideal for restoring from a local TrueNAS/NAS pull if the primary cloud repo is unreachable.
RESTIC_FALLBACK_REPOSITORY="sftp:truenas-backup:/mnt/HDDs/Hatzner_VPS_Backups/home/i3vps"

# Optional: If your secondary mirror uses a different restic password, specify it here.
# Leave blank to automatically use the primary RESTIC_PASSWORD_FILE.
RESTIC_FALLBACK_PASSWORD_FILE=""
30 changes: 26 additions & 4 deletions restic-backup.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#!/usr/bin/env bash

# =================================================================
# Restic Backup Script v0.45 - 2026.05.09
# Restic Backup Script v0.46 - 2026.05.24
# =================================================================

export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH
set -euo pipefail
umask 077

# --- Script Constants ---
SCRIPT_VERSION="0.45"
SCRIPT_VERSION="0.46"
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
PROG_NAME=$(basename "$0"); readonly PROG_NAME
CONFIG_FILE="${SCRIPT_DIR}/restic-backup.conf"
Expand Down Expand Up @@ -46,6 +46,7 @@ display_help() {
echo
echo -e "${C_BOLD}${C_YELLOW}OPTIONS:${C_RESET}"
printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--verbose" "Show detailed live output."
printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--fallback" "Temporarily route command to the fallback repository."
printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--fix-permissions" "Interactive only: auto-fix 600/400 on conf/secret."
printf " ${C_GREEN}%-20s${C_RESET} %s\n" "--help, -h" "Display this help message."
echo
Expand Down Expand Up @@ -811,8 +812,24 @@ send_notification() {
}

setup_environment() {
export RESTIC_REPOSITORY
export RESTIC_PASSWORD_FILE
# Handle Fallback Routing
if [[ "${USE_FALLBACK:-false}" == "true" ]]; then
if [ -z "${RESTIC_FALLBACK_REPOSITORY:-}" ]; then
echo -e "${C_RED}ERROR: --fallback flag used, but RESTIC_FALLBACK_REPOSITORY is not set in config.${C_RESET}" >&2
exit 1
fi
export RESTIC_REPOSITORY="${RESTIC_FALLBACK_REPOSITORY}"
if [ -n "${RESTIC_FALLBACK_PASSWORD_FILE:-}" ]; then
export RESTIC_PASSWORD_FILE="${RESTIC_FALLBACK_PASSWORD_FILE}"
else
export RESTIC_PASSWORD_FILE # Fallback to primary password
fi
log_message "FALLBACK MODE ENGAGED: Routing to $RESTIC_REPOSITORY"
echo -e "${C_YELLOW}⚠️ Running in FALLBACK mode. Using alternate repository.${C_RESET}"
else
export RESTIC_REPOSITORY
export RESTIC_PASSWORD_FILE
fi

if [ -n "${GOMAXPROCS_LIMIT:-}" ]; then
export GOMAXPROCS="${GOMAXPROCS_LIMIT}"
Expand Down Expand Up @@ -1808,13 +1825,18 @@ EOF
# 1. Parse flags.
VERBOSE_MODE=false
SKIP_OWNERSHIP_FIX=false
USE_FALLBACK=false
AUTO_FIX_PERMS=${AUTO_FIX_PERMS:-false}
while [[ $# -gt 0 ]]; do
case "$1" in
--verbose)
VERBOSE_MODE=true
shift
;;
--fallback)
USE_FALLBACK=true
shift
;;
--exact-ownership)
SKIP_OWNERSHIP_FIX=true
shift
Expand Down
2 changes: 1 addition & 1 deletion restic-backup.sh.sha256
Original file line number Diff line number Diff line change
@@ -1 +1 @@
52a1734972189b3e0509055659678722d58b72eb9baa5a22dbc1b1d207e7c262 restic-backup.sh
b8b5fb61ee330a2352eff74825366050c7ecfc512a0c6a20c306a594883dbfe6 restic-backup.sh