Install Guide
A step-by-step walkthrough for standing up a BackupBloc server and attaching Linux or macOS clients to it. The entire flow is driven by two scripts — install-server.sh and install-agent.sh (or install-agent-mac.sh) — and takes roughly ten minutes end-to-end.
descriptionOverview
BackupBloc has two pieces you install, plus an optional third for off-site resilience:
- Backup server — one host that runs
rest-server(repository storage), the Flask management API, the admin web UI, and nginx as the TLS-terminating reverse proxy. - Backup agent — one install per machine you want to back up. The agent is just
resticplus a systemd timer (Linux) or LaunchDaemon (macOS) that runs snapshots on a schedule and reports status back to the server. - DR server optional — a second server in another location that keeps an append-only, off-site mirror of your backups. See Disaster recovery.
All encryption happens on the agent side, so the server never sees plaintext. The server just stores encrypted, content-addressed blobs.
checklistRequirements
| Role | Minimum | Recommended |
|---|---|---|
| Server OS | Any systemd Linux (Debian/Ubuntu/RHEL/Fedora/Arch/Alpine/openSUSE) | Ubuntu 22.04 LTS or Debian 12 |
| Server hardware | 1 vCPU, 512 MB RAM | 2+ vCPU, 2 GB+ RAM |
| Server storage | Enough for repos | 2–10× total client data (dedup) |
| Server access | root / sudo | root, plus a public domain if you want Let's Encrypt TLS |
| Linux agent | Kernel 3.2+, systemd | Any modern systemd distro |
| macOS agent | macOS 11 Big Sur | macOS 13 Ventura+ with Homebrew |
| Network | Agent → server on TCP 8000 (REST) + 443 (admin UI) | Open 80 outbound to agents too if using Let's Encrypt |
Port 8000 is not for the public internet. The REST server should only be reachable from agent IPs you trust. Firewall it or bind it to a private interface.
hubArchitecture
┌─────────────────────── BACKUP SERVER ──────────────────────────┐
│ │
│ nginx :443 ──► backup-api :5000 (Flask, mgmt) │
│ nginx :443/restic ──► rest-server :8000 (restic repo I/O) │
│ │ │
│ ▼ │
│ /var/lib/restic-repos/ │
│ ├── default/ │
│ └── offsite/ │
└─────────────────────────────────────────────────────────────────┘
▲
│ HTTPS (restic backup + admin heartbeat)
│
┌─── AGENTS ──────────────────────────────────────────────────────┐
│ web-01 (Linux) db-01 (Linux) MacBook (macOS) │
│ systemd timer systemd timer launchd │
└─────────────────────────────────────────────────────────────────┘
dns1 · Backup server — prepare the host
Provision a fresh Linux VM or dedicated machine. Point DNS at it if you plan to use a hostname (required for Let's Encrypt). Open the ports in the table below inbound.
| Port | Purpose | Scope |
|---|---|---|
22 | SSH (installer + admin) | Your admin IP |
80 | HTTP → HTTPS redirect + ACME challenge | Public (only if using Let's Encrypt) |
443 | Admin UI + management API (HTTPS) | Admin IPs, or public |
8000 | restic rest-server (agent traffic) | Agent IPs only |
The installer is completely interactive — there are no environment variables or config files to pre-populate. It will also install restic, rest-server, nginx, Python 3, and any other dependencies itself.
terminalRun the server installer
Download and extract the server package
Pull the latest server package from the CDN and unpack it. The tarball already contains the installer plus the server/ and admin/ trees it needs (and the agent + DR installers).
curl -fsSL https://backupbloc.com/downloads/backupbloc-server.tar.gz -o backupbloc-server.tar.gz
tar -xzf backupbloc-server.tar.gz
cd backupbloc-server
Run the installer as root
sudo ./install-server.sh
Answer the prompts
The installer asks for each setting in turn. Press Enter to accept the default shown in brackets.
| Prompt | Default | Notes |
|---|---|---|
| Server hostname / IP | $(hostname -f) | What agents will connect to. Use the public DNS name if you expose this box. |
| REST server port | 8000 | restic speaks to this. Only agents need it. |
| Admin UI port | 8080 | nginx binds here. Put 443 if you want the UI on the standard HTTPS port. |
| Repository directory | /var/lib/restic-repos | Where backup data is stored. Use a mountpoint with plenty of room. |
| Admin username | admin | Used for the web UI and the rest-server htpasswd. |
| Admin password | auto-generated | If left blank a strong 20-char password is printed — save it before pressing Enter. |
| Enable TLS? | N | Strongly recommended. Pick Y in production. |
Pick a TLS source (if you enabled TLS)
- Let's Encrypt — free, auto-renewing. Requires a public domain and port 80 open. Installer runs
certbotin standalone mode, fixes thelive/andarchive/permissions so the restic-server user can read privkey.pem, and installs a renewal deploy-hook that restarts services automatically. - Self-signed — generated on the spot, valid 10 years. No domain needed. Agents will need
--insecure-tlsor the CA added manually. - Existing certificate — you supply the paths to your
fullchain.pemandprivkey.pem.
Confirm the summary
The installer prints everything it's about to do, then asks for a single Y to proceed. From that point it is fully automatic — roughly 2–3 minutes.
Note the final banner
When it finishes you'll see the admin URL, credentials, and a list of important paths. Copy these somewhere safe now — the password is not shown again.
Access Points:
Admin UI: https://backup.example.com:8080
REST Server: https://backup.example.com:8000
Credentials:
Username: admin
Password: xxxxxxxxxxxxxxxxxxx
task_altVerify the services
Three systemd units should be active (running):
systemctl status restic-rest-server restic-api nginx
If any are red, check their logs:
journalctl -u restic-rest-server -n 50 --no-pager
journalctl -u restic-api -n 50 --no-pager
The installer also creates a default repository at /var/lib/restic-repos/default/ with a random 32-char encryption password saved to /etc/restic-manager/default-repo.password. Agents created later default to this repository name.
loginFirst login
Open the admin UI
Browse to the URL from the installer's final banner. Log in with the admin username and password.
Activate your license
On first login the panel will prompt for a license key. Paste the BB-XXXXXXXX-XXXXXXXX key issued to you. The panel phones home to license.backupbloc.com once, caches the result, and will keep working through short control-plane outages via the 12 h grace window.
(Optional) rotate the admin password
Use Settings → Change password in the admin UI. The new password is written to /etc/restic-server/users (bcrypt) and reused by the REST server on the next restart.
verified_user2 · Backup agent — authorise first
Every agent needs to be authorised before it can register. BackupBloc supports two styles:
- Server license (IP-bound) — for fixed servers with a static public IP. You pre-authorise the IP in the admin UI under Clients → Add Server. The agent installer reads the public IP at runtime, the server validates it, and credentials are returned automatically — no password entry.
- Personal key — for laptops and roaming machines. You generate a
BB-XXXXXXXX-XXXXXXXXkey in the admin UI under Clients → Personal Licenses and paste it into the installer.
You must create the license before running the agent installer, otherwise the bootstrap call will fail and the installer will stop.
memoryLinux agent install
Run the installer (one line)
SSH into the client as root and run the hosted installer. It downloads restic and everything else it needs:
sudo bash <(curl -fsSL https://backupbloc.com/downloads/agent/install-agent.sh)
Air-gapped / no outbound internet? Copy install-agent.sh from your server tree to the client and run it directly: scp install-agent.sh root@web-01:/tmp/ && ssh root@web-01 'bash /tmp/install-agent.sh'.
Answer the prompts
| Prompt | Meaning |
|---|---|
| Server hostname / IP | What you entered at the SERVER_HOST prompt when you installed the server. |
| REST port | 8000 by default. The installer auto-detects http vs https by probing. |
| Repository name | default unless you've created others in the admin UI. |
| Admin UI URL | Full base URL of the admin UI — used for self-registration + heartbeats. e.g. https://backup.example.com |
| License type | 1 for a personal key (enter BB-XXXXXXXX-XXXXXXXX), 2 for a pre-authorised server IP. |
| Repo encryption password | Blank = auto-generate. Saved to /etc/restic-agent/repo.password — save a copy off-box too. |
| What to back up | Common paths / custom list / full-system. |
| Schedule | 6-hourly, daily, weekly, or custom cron expression. |
| Retention | How many daily / weekly / monthly snapshots to keep. |
| SSH port | Used by "Backup Now" in the admin UI to fire ad-hoc runs. |
Installer finishes — first backup kicks off
The installer registers the client with the server, initialises the repository if needed, writes /etc/restic-agent/config.env, and drops a systemd service + timer. The first run fires immediately so you can see a snapshot appear in the admin UI within a minute or two.
systemctl status restic-backup.timer
systemctl list-timers | grep restic
Jump to the macOS agent section below — the flow is similar but uses Homebrew + launchd instead of apt + systemd.
laptop_macmacOS agent install
Grant Full Disk Access first. System Settings → Privacy & Security → Full Disk Access → add /usr/local/bin/restic (Intel) or /opt/homebrew/bin/restic (Apple Silicon) and /bin/bash. Without FDA the agent will skip ~/Library and many app data directories silently.
Run the installer with sudo
In Terminal on the Mac:
sudo bash <(curl -fsSL https://backupbloc.com/downloads/agent/install-agent-mac.sh)
Offline alternative: scp install-agent-mac.sh admin@mac:/tmp/ then sudo bash /tmp/install-agent-mac.sh.
Answer the prompts
Same fields as the Linux installer: server hostname, REST port, repo name, license type (personal key usually, for laptops), repo password, paths, schedule, retention.
Homebrew + restic are installed for you
If Homebrew is missing the installer pulls it down and runs brew install restic. A LaunchDaemon is written to /Library/LaunchDaemons/com.backupbloc.agent.plist and loaded — so jobs fire on schedule even when no user is logged in.
Confirm the LaunchDaemon is loaded
sudo launchctl list | grep backupbloc
log show --predicate 'process == "bash"' --last 5m --info
fact_checkVerify the first backup
- Open the admin UI → Clients. The new host should appear with a green dot within ~60 seconds of install.
- Click it → Snapshots. The first snapshot should show up after the scheduled run (or immediately, since the installer triggers an initial run).
- Pick a snapshot → Browse files to confirm the content looks right. You can restore individual files or whole directories from here.
You're done. Repeat the agent install on every machine you want backed up. Server-side, the admin UI gives you live job logs, retention management, ad-hoc "Backup Now", and per-client snapshot browsing + restore.
restoreRestoring files
Restore from any snapshot to one of three destinations — the original client, the backup server itself, or a brand-new / different server (for rebuilding a machine that has failed). The same chooser appears whether you restore from the primary backup or from a DR copy.
Find the snapshot
Open Snapshots, pick the client and the snapshot you want. Click a snapshot to browse its files, or click Restore to open the restore dialog.
(Optional) narrow to one path
In Path Filter, enter a single file or folder (e.g. /home/user/site) to restore just that. Leave it blank to restore the whole snapshot.
Choose where to restore
| Destination | What happens |
|---|---|
| Original client | Files are pushed back onto the machine the snapshot came from, over SSH. |
| This server | Files land in a folder on the backup server (under an allowed restore path) for you to inspect or copy. |
| New / other server | Restore onto a different or freshly-built machine — by registered agent, or by SSH details. See the steps below. |
Set the Restore to path, click Start restore, and watch the live progress bar.
Restoring to a new / replacement server
This is the disaster-recovery path: a server failed, you've built a replacement, and you want its data back on it. Choose New / other server, then one of:
- Registered agent — the replacement already has the BackupBloc agent installed. Pick it from the list; the manager restores onto it.
- By SSH details — the replacement has no agent yet. Enter its IP/hostname and SSH port. First authorise the manager's key on it (the dialog shows the exact command):
mkdir -p /root/.ssh && echo 'ssh-ed25519 AAAA…' >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys
Set the destination path on the new server — use / for a full system recovery, or a staging folder (e.g. /var/tmp/recovery) to copy specific data across first. The manager SSHes in, installs restic if missing, then restores either directly (the new server pulls the repo over the network — fast) or via relay (the manager restores and streams the files over SSH) if the new server can't reach the backup server's REST port. The path is chosen automatically and shown live.
The destination path is on the target server and isn't restricted by the backup server's allowed-prefix list — restoring to / overwrites files in place. Use a staging folder if you only want to copy specific data across.
For the same flow starting from your off-site DR copy (when the primary is also gone), use DR → Manage replication → Restore — it offers the identical destination chooser. See Rebuild a failed server.
delete_foreverUninstall the agent
When you decommission a machine, run the uninstaller on the host and remove the client in the panel. Doing only one of those leaves the other side polling / accumulating noise.
Linux
One-line install/uninstall — downloads from the CDN and runs:
sudo bash <(curl -fsSL https://backupbloc.com/downloads/agent/uninstall-agent.sh)
If the host is air-gapped, copy uninstall-agent.sh across and run sudo bash uninstall-agent.sh.
The script stops + disables the systemd units (restic-backup.timer, restic-mysql-backup.timer, restic-restore-poller.timer), removes the cron entry if present, deletes the binaries under /usr/local/bin, and clears /etc/restic-agent + /var/log/restic-agent.
macOS
sudo bash <(curl -fsSL https://backupbloc.com/downloads/agent/uninstall-agent-mac.sh)
Tears down the LaunchDaemon (com.backupbloc.agent), removes the binaries, and clears the agent config + logs the same way.
Manual one-liner (if you can't fetch the script)
sudo systemctl disable --now restic-backup.timer restic-mysql-backup.timer restic-restore-poller.timer 2>/dev/null
sudo rm -f /etc/systemd/system/restic-{backup,mysql-backup,restore-poller}.{service,timer}
sudo rm -f /usr/local/bin/restic-{backup,mysql-backup,progress-reporter,restore-reporter,restore-poller,validate-license}
sudo rm -rf /etc/restic-agent /var/log/restic-agent /etc/cron.d/restic-restore-poller
sudo systemctl daemon-reload
Then in the panel
- Open Clients, find the row, click Delete.
- Choose Remove client, keep backups if you want to retain the snapshots (restorable forever) — or Remove + delete all backups to also delete the repository on the server.
- Either choice revokes the agent's license entry, so even if the uninstaller didn't run, the agent stops backing up on its next license re-check (within 24 h).
What about restic itself? The uninstaller leaves the restic binary in place — it's a general-purpose tool you may want for other things. Remove manually with sudo rm /usr/local/bin/restic (or via your package manager) if it's no longer needed.
Decommissioning shortcut. If you can't SSH to the host (e.g. it's already gone), removing the client in the panel is enough on its own — the agent's daily license re-check fails within 24 h, the local license.status flips to invalid, and every subsequent backup refuses to run. You'll receive a "License re-validation failed" notification each day until the agent is fully removed.
cloud_sync3 · Disaster recovery (DR)
Disaster recovery keeps a second, off-site copy of your backups on a separate server — so even if the main backup server is lost, corrupted, or its repository is wiped, your data survives. A DR server holds an append-only mirror of selected repositories: new backup data is copied to it on a schedule, but anything deleted or pruned on the main server is never deleted on the DR. Accidental deletions and ransomware can't propagate.
The whole feature is managed from the existing admin UI under Disaster Recovery → DR Servers — there is no second panel to log in to.
Disaster Recovery is a paid add-on. If your plan doesn't include it, the DR Servers page shows an upgrade prompt instead of the management UI. Once DR is enabled on your licence, the page unlocks and everything below works. Whether DR is included is controlled by your licence — no reinstall or config change is needed when it's added.
Because BackupBloc stores every agent's repository centrally on the main server, replication runs main server → DR server. Your source machines (web-01, db-01, …) are never involved in DR and never need access to it.
schemaHow it works
┌──────────── MAIN BACKUP SERVER ────────────┐ ┌─────────── DR SERVER ───────────┐
│ /var/lib/restic-repos/ (canonical) │ │ /var/lib/restic-repos/ (mirror) │
│ │ │ SSH │ ▲ │
│ └── rclone copy (append-only) ───────┼────────┼────────┘ │
│ every 5 min … 6 h, per repo │ (SFTP) │ append-only rest-server :8000 │
└─────────────────────────────────────────────┘ └──────────────────────────────────┘
restore / browse ◄────────────────────────────── (restic over SSH)
- Append-only — replication uses
rclone copy(neversync), so files removed on the main server are kept on the DR. - Snapshots copied last — data and indexes are copied before snapshot files, so the DR copy is always a valid, restorable repository even mid-sync.
- All over SSH — the main server reaches the DR with its existing trigger key. No extra credentials travel the network in cleartext.
- Same encryption — the DR holds the identical encrypted repo, so the main server's existing repo password unlocks it for restores. Nothing new to manage.
checklistDR requirements
| Item | Requirement |
|---|---|
| DR host | A second server, ideally in a different location/provider. Any systemd Linux (Debian/Ubuntu/RHEL/Fedora). |
| Storage | At least as much free space as the repositories you'll replicate — plan for more, since the append-only copy keeps history the main server may prune. |
| Access | root / sudo on the DR host. The main server must be able to reach the DR over SSH (port 22 by default). |
| Network | Outbound from the main server → DR on the SSH port. The DR does not need to reach back into the main server. |
rocket_launchOption A — Set up a new DR node (recommended)
This provisions a blank server into a full DR node automatically: it installs restic, rclone, and an append-only rest-server, authorises the main server, and registers itself so it appears in your panel.
Open the DR page and start the wizard
In the admin UI, go to Disaster Recovery → DR Servers and click Set up new DR node. Give it a name (e.g. DR-Sydney) and click Generate install command.
Copy the one-time install command
The panel produces a ready-to-paste command containing a single-use enrollment token (valid for one hour). It looks like this:
bash <(curl -fsSL https://backupbloc.com/downloads/agent/install-dr.sh) \
--manager https://backup.example.com --token BB-DR-XXXXXXXXXXXX
Run it on the blank DR server as root
SSH into the new server and paste the command. It installs everything, opens the firewall for the rest-server port, authorises the main server's SSH key, and calls home to register. It finishes in a couple of minutes.
sudo bash <(curl -fsSL https://backupbloc.com/downloads/agent/install-dr.sh) \
--manager https://backup.example.com --token BB-DR-XXXXXXXXXXXX
Confirm it appears in the panel
Back in DR Servers, the new node shows up with a green Online status and ticks for rclone, restic, and rest-server, along with its free disk space. You're ready to assign replication.
The installer is hosted at backupbloc.com/downloads/agent/install-dr.sh. You can inspect it before running — it's a plain Bash script.
lanOption B — Add an existing server by SSH
If you already have a server you want to use (and prefer to install the tools yourself, or just point at an existing box), add it by SSH details instead. The main server reaches out to it.
Add the server
On the DR Servers page click Add DR server, enter a name, the DR host's IP/hostname, and SSH port, then save.
Authorise the main server's key (if prompted)
If the main server can't SSH in yet, the panel shows an Awaiting SSH key status and a one-line command to run on the DR host. It adds the main server's public key to /root/.ssh/authorized_keys:
mkdir -p /root/.ssh && echo 'ssh-ed25519 AAAA…' >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys
Then click Test connection. The status flips to Online.
A manually-added server must have restic and rclone installed for replication and restore to work. The card's tick marks show what's present. The installer in Option A handles this for you.
syncAssign replication
Choose which client repositories replicate to the DR server, and how often.
Open "Manage replication"
On the DR server's card click Manage replication.
Add a repo and pick an interval
Under Add replication, choose a client (its repository is selected automatically) and an interval, then click Add. An initial seed sync starts straight away.
| Interval options |
|---|
| 5 min · 15 min · 30 min · 1 hour · 2 hours · 3 hours · 6 hours |
Monitor and control each repo
Each replication row shows its last-sync status and lag (e.g. synced 8 min ago). Per row you can:
- Sync now — run an immediate replication.
- Pause / Resume — turn the schedule off without losing the assignment.
- Change interval — pick a new frequency from the dropdown.
- View log — see the per-repo replication history.
- Remove — stop replicating (data already on the DR is left intact).
Watch live progress
While a sync runs you get a live progress bar that pulses blue with a percentage, turns green on success and red on failure — both in the Manage replication rows and in the Active Replications panel at the top of the DR page (so you can see what's running without opening anything). It shows the phase (Copying data → Copying snapshots), live transfer rate and ETA, and refreshes on its own.
The first sync (the seed) copies the whole repository, so it can take a while for large repos. Replication is append-only and incremental — subsequent syncs skip everything already on the DR and only copy new data, so they're fast.
restoreRestore / failover from the DR
If you need data back from the off-site copy — for example a file that was deleted and later pruned from the main server — restore directly from the DR.
Browse the DR's snapshots
In Manage replication, click the Restore (history) icon on a repo. The panel lists the snapshots present on the DR copy, tagged FILES or DB.
Choose where to restore
Click Restore… on the snapshot, optionally enter a single path, then pick a destination:
| Destination | What happens |
|---|---|
| This manager | Files land in a folder on the backup server (under an allowed restore path) so you can inspect or copy them. |
| Registered agent | Files are restored onto a server that already has the agent installed — pick it from the list. |
| New server (SSH) | Rebuild a failed server: enter the new machine's IP + SSH port and restore straight onto it. See below. |
Progress is shown live for all three.
Because the DR holds the same encrypted repository, restores use the main server's existing repo password automatically — there are no extra keys to enter.
dnsRebuild a failed server onto a fresh machine
When a source server is lost entirely, build a replacement, install the OS, then restore its data onto it directly. This works from either the primary backup (main server's Snapshots → Restore) or the off-site DR copy (DR → Manage replication → Restore) — both offer the same New / other server destination.
Authorise the manager on the new server
The panel shows a one-line command in the restore dialog. Run it on the new server as root — it adds the manager's SSH key so the manager can connect and drive the restore:
mkdir -p /root/.ssh && echo 'ssh-ed25519 AAAA…' >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys
Enter the new server's details and restore
Pick New / other server → By SSH details, enter its IP/hostname and SSH port, and set the destination path — use / for a full system recovery, or a subfolder to stage the files first. Click Start restore.
What the manager does
It SSHes into the new server, installs restic if it's missing, then either:
- Direct — the new server runs
resticitself and pulls the repository over the network from the backup server. Fast, streams straight onto the new machine. - Relay — if the new server can't reach the backup server's REST port, the manager restores the files itself and streams them to the new server over SSH. Used automatically as a fallback, and always for DR-copy restores.
Prefer the agent route? You can instead install the agent on the new server (pointing it at the same repository), then choose Registered agent as the destination. Either method restores the data onto the new machine.
The destination path is on the new server and is not restricted by the manager's allowed-prefix list — restoring to / overwrites system files in place, so use a staging folder if you only want to copy specific data across.
emergency_homeRecovering when the whole site / main server is down
The flows above assume the backup server is reachable to drive the restore. But what if the entire data centre holding the source servers and the backup server goes down? For that, each DR node carries everything needed to recover on its own:
- Recovery bundle — alongside the backup data, the manager replicates the repository decryption passwords and config (users, agents, settings) to the DR node, into
/etc/backupbloc-recovery/. Without these the off-site copy would be unreadable ciphertext; with them the DR node can decrypt and restore by itself. This syncs automatically after each replication. - Standby recovery panel — the DR installer also puts a read/restore-only copy of the BackupBloc panel on the DR node. It does not replicate or accept backups, and it keeps working even when the primary and the licence server are unreachable.
Use the standby panel
Browse to the DR node
Open https://<dr-node-ip>/ (the installer prints the exact URL; it uses a self-signed certificate, so accept the browser warning). Log in with your normal admin credentials — they're part of the replicated bundle.
Restore from the local copy
A STANDBY banner confirms you're on the recovery panel. Use Snapshots (or DR → Manage replication → Restore) exactly as on the primary — browse files, restore a single file, a folder, or rebuild a whole server onto a fresh machine. All restores read from the DR node's local mirror.
Manual fallback (no panel)
If you prefer the command line, the data is right there on the DR node — decrypt and restore directly with restic:
export RESTIC_PASSWORD="$(cat /etc/backupbloc-recovery/<repo>.password)"
restic -r /var/lib/restic-repos/<repo> snapshots
restic -r /var/lib/restic-repos/<repo> restore latest --target /recovery
The standby panel is restore-only. When your primary comes back, resume normal operations there — don't run backups against the standby. To install the standby panel on an existing DR node, re-run the DR installer (it's added automatically; pass --no-panel to skip).
Because the recovery bundle includes repository passwords, treat your DR node as security-sensitive — it holds the keys to decrypt your backups, which is exactly what makes off-site recovery possible.
groupUser roles
BackupBloc has two built-in roles. Both roles log in to the same admin UI — the interface simply adapts to what each role is allowed to do.
| Role | What they see | What they can do |
|---|---|---|
| admin | Full admin panel — Dashboard, Clients, Plans, Repositories, Snapshots, Databases, Jobs, Logs, Licenses, Users, Settings | Everything: manage clients, repositories, users, schedules, licenses, trigger global backups, perform restores |
| client | Simplified user panel — lands directly on the Backup Now page; sees only Snapshots, Logs, and Settings in the sidebar; admin-only sections are hidden | Trigger on-demand backups for their assigned clients, view snapshots and logs scoped to those clients, change their own password |
A client user is typically a website owner or developer who needs to snapshot their own server before making changes — without being exposed to the full admin interface.
Creating a client user
Open the Users page
In the admin UI, navigate to Users in the sidebar.
Add a new user
Click + Add User. Enter a username and password, then change the Role dropdown from admin to client.
Assign clients
Once the role is set to client, a Assigned Clients section appears. Tick every client (server) this user is allowed to see and back up. A user with no assigned clients will see an empty panel.
Optionally set a storage quota
The Quota (GB) field caps how much backup data this user can accumulate across all their assigned clients. Leave it at 0 for unlimited.
Save and share credentials
Click Save. Send the username and password to the user — they log in at the same URL as the admin panel. They will land directly on their personal Backup Now page.
shieldThe client user panel
When a client-role user logs in, the interface hides all admin-only navigation and takes them directly to the Backup Now page. The page is split into two columns:
| Left column | Right column |
|---|---|
|
Your Servers — live status cards for each of the user's assigned clients, showing last backup time and current state (Synced / In Progress / Failed). Backup form — type selector, paths tag-input, optional note, and the green Start Backup button. Inline status — after triggering, the form is replaced by a live progress ring that polls every 4 seconds and updates to Complete or Error automatically. |
Recent Snapshots — the last 12 snapshots scoped to this user's clients. Shows hostname, age, and size. Refreshes automatically when a new snapshot completes. |
The header also gains a green Backup Now button that navigates back to this page from any other section of the UI.
play_circleBackup Now — how it works
Backup Now creates an on-demand snapshot without waiting for the scheduled timer. It is available to both admin users (per-client trigger button on the Clients page) and client users (their dedicated panel).
Backup types
| Type | What it does | When to use it |
|---|---|---|
| Standard | Incremental — restic only reads and uploads files that have changed since the last snapshot. Fast and storage-efficient. | The default. Use this before any routine change (deploying code, updating a CMS, running database migrations). |
| Full | Forces restic to re-read every file from disk regardless of modification time (--force). Slower, but catches silent drift (clock skew, filesystem corruption). |
Use when you suspect data integrity issues or before a major upgrade where you want a fresh, unconditional baseline. |
Scoped paths
By default, Backup Now backs up everything configured for that client. You can narrow the scope by typing one or more absolute paths into the What to back up tag-input and pressing Enter after each one:
# Example — back up only the web root and the database export directory
/var/www/mysite
/var/backups/mysql
The paths are passed directly to restic backup. Leave the input empty to use the client's default configured paths.
Technical flow
- The UI posts
POST /api/backup/triggerwith{ client, full, paths }. - The server queues the request in
/etc/restic-manager/triggers.json. - The agent's restore-poller daemon (running every 30–60 seconds via systemd timer) polls
GET /api/backup/trigger/pending, finds the queued job, and ACKs it immediately to prevent duplicate runs. - The agent runs
restic backupwith the specified arguments and streams progress back viaPOST /api/backup/progress. - The UI polls
GET /api/backup/statusevery 4 seconds and updates the status ring in real time.
The poller runs every 30–60 seconds, so there may be a short delay before the backup starts. The status card will show Queued until the agent picks it up, then switch to Running.
securitySecurity hardening
The following hardening changes have been applied to the codebase. Each addresses a specific vulnerability class found during an internal security audit.
Shell injection via eval removed
Risk: The agent scripts (restic-backup, restic-mysql-backup, restic-restore-poller) previously used eval to build and run restic commands from environment variables. A malicious value in any variable (e.g. BACKUP_PATHS) could inject arbitrary shell commands that would be executed as root.
Fix: All eval calls replaced with explicit Bash arrays. Variables are word-split via read -ra and expanded as "${ARRAY[@]}", which is safe even if values contain spaces or shell metacharacters.
# Before — vulnerable
eval restic backup $BACKUP_PATHS --tag $CLIENT_ID
# After — safe
RESTIC_CMD=(restic backup)
read -ra _BP <<< "${BACKUP_PATHS:-}"
[[ ${#_BP[@]} -gt 0 ]] && RESTIC_CMD+=("${_BP[@]}")
RESTIC_CMD+=(--tag "${CLIENT_ID}")
"${RESTIC_CMD[@]}"
TLS certificate verification enforced
Risk: All curl calls in the agent scripts used -sk (skip TLS verification). This allowed a network-position attacker to MITM the connection between the agent and the backup server and intercept or modify responses.
Fix: The -k flag has been removed from every curl call. When a custom CA bundle is present at /etc/restic-agent/ca.crt (e.g. for self-signed server certificates) it is passed via --cacert; otherwise the system trust store is used.
CURL_TLS=()
[[ -f /etc/restic-agent/ca.crt ]] && CURL_TLS=(--cacert /etc/restic-agent/ca.crt)
# Every curl call now includes ${CURL_TLS[@]}
curl -s "${CURL_TLS[@]}" --max-time 15 \
"${API_BASE}/api/health"
Agent update scripts verified before execution
Risk: The restore-poller downloaded update-agent.sh from backupbloc.com and ran it directly with bash. A compromised CDN, DNS hijack, or HTTP downgrade could deliver a malicious script that would run as root on every agent.
Fix: The poller now downloads both the update script and a detached RSA signature (.sig), and verifies it against the public key at /etc/restic-agent/update-pubkey.pem before executing. The public key is installed both by the agent installer and by every update bundle, and the matching private key never leaves the release machine. If the signature is missing or fails to verify, the download is deleted and the update is skipped.
Signatures use RSA with SHA-256, verified by openssl dgst -sha256 -verify — which works on every OpenSSL (1.0.2 / 1.1.1 / 3.x) and LibreSSL, so it's portable across all agent OSes. Releases are signed by build-release.sh, which self-verifies the signature before the bundle is published.
PUBKEY=/etc/restic-agent/update-pubkey.pem
if [[ -f "$PUBKEY" ]] && \
openssl dgst -sha256 -verify "$PUBKEY" \
-signature "$_UPDATE_SIG" "$_UPDATE_SCRIPT"; then
bash "$_UPDATE_SCRIPT"
else
# Signature missing or invalid — refuse to execute
rm -f "$_UPDATE_SCRIPT" "$_UPDATE_SIG"
fi
SQL injection in MySQL backup script prevented
Risk: The restic-mysql-backup script interpolated the database name directly into a MySQL query string. A database name containing SQL metacharacters (e.g. a backtick or semicolon) could break out of the query context.
Fix: Database names are validated against a strict allowlist regex (^[a-zA-Z0-9_-]+$) before use. Any name that does not match is logged and skipped.
if [[ ! "$DB_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
log "WARN Skipping database with unsafe name: ${DB_NAME}"
continue
fi
Repository password endpoint authenticated
Risk: The POST /api/repos/<id>/password endpoint — used by the agent installer to register the repository encryption password — accepted any request without verifying that the caller was a known, registered agent. An unauthenticated attacker who could reach the API could overwrite any repository's password.
Fix: The endpoint now requires a valid client_id in the request body, checks it against the registered agents database (/etc/restic-manager/agents.json), and verifies that the target repository's config file actually exists before updating the password.
Login rate limiting
Risk: The POST /api/auth/login endpoint had no brute-force protection. An attacker could make unlimited login attempts.
Fix: flask-limiter (≥ 3.5.0) is now installed in the server virtualenv and applies a limit of 10 login attempts per minute per IP. If the package is unavailable (e.g. during an in-place update) the limit degrades gracefully to a no-op rather than crashing the API.
# /opt/restic-manager/venv/bin/pip install "flask-limiter>=3.5.0"
# Added to server/requirements.txt
Rate limits apply per source IP. If your agents or admin UI are behind a NAT that shares one external IP, the 10 req/min limit applies to the whole NAT group. Adjust the limit in backup-api.py if needed.
Session TTL reduced to 24 hours
Risk: Login session tokens previously lasted 7 days. A stolen token (e.g. from browser storage or a compromised machine) had a long exploitation window.
Fix: _SESSION_TTL reduced from 86400 * 7 to 86400 (24 hours). All existing sessions are unaffected until they next authenticate.
Restore path whitelist
Risk: The POST /api/restore/jobs endpoint accepted any absolute path as the restore target. An authenticated user (or a compromised session) could request a restore to /etc/restic-manager/ or another sensitive path, overwriting system config.
Fix: A RESTORE_ALLOWED_PREFIXES environment variable (comma-separated list of safe root paths) is checked before any restore job is accepted. Paths outside the whitelist are rejected with a 400 error.
# In /etc/restic-manager/config.env:
RESTORE_ALLOWED_PREFIXES=/home,/var/www,/opt/sites,/tmp/restore
If RESTORE_ALLOWED_PREFIXES is not set, restore jobs are accepted without path restriction (preserving backward compatibility with existing deployments). It is strongly recommended to set this variable in production.
Structured audit log
Risk: Previously there was no record of who performed sensitive operations (login, password change, backup trigger, restore request, user creation/deletion).
Fix: An _audit() function now appends a JSON line to /var/log/restic-manager/audit.log for every privileged action. Each entry includes a UTC timestamp, the action name, the actor (username or system), the source IP, and any relevant detail.
# Example audit.log entries
{"ts":"2026-05-09T03:12:44Z","action":"login_ok","actor":"admin","ip":"203.0.113.5","detail":""}
{"ts":"2026-05-09T03:14:01Z","action":"backup_trigger","actor":"admin","ip":"203.0.113.5","detail":"client=web-01 full=false"}
{"ts":"2026-05-09T03:44:17Z","action":"restore_job_queued","actor":"client1","ip":"10.0.0.2","detail":"snap=abc12345 target=/tmp/restore"}
{"ts":"2026-05-09T04:00:05Z","action":"user_deleted","actor":"admin","ip":"203.0.113.5","detail":"target=olduser"}
The log file is append-only (created with mode 0600, owned by root). Rotate it with logrotate or ship it to a centralised SIEM for alerting.
Keep audit logs off the backup server disk. Consider shipping them to a remote syslog or object store so an attacker who gains access to the server cannot tamper with the record of their activity.
folder_openFile layout reference
Server
/opt/restic-manager/api/ | Flask management API (backup-api.py, control_client.py, updater.py) |
/opt/restic-manager/admin/ | Static admin UI served by nginx |
/opt/restic-manager/venv/ | Python virtualenv (flask, cryptography, requests, …) |
/etc/restic-manager/config.env | Main server config (hostname, ports, TLS paths, admin user) |
/etc/restic-manager/control.json | Upstream control-server URL + grace-period + enabled flag |
/etc/restic-manager/update.json | Auto-updater channel, auto_apply, check interval |
/etc/restic-manager/update-pubkey.pem | Ed25519 key used to verify signed release bundles |
/etc/restic-manager/dr.json | DR servers, replication assignments, and pending enrolments |
/etc/restic-manager/trigger_key | SSH key the main server uses to reach agents and DR nodes |
/var/log/restic-manager/dr/ | Per-assignment DR replication logs |
/etc/restic-server/users | rest-server htpasswd (bcrypt) |
/var/lib/restic-repos/ | Repository data — big, keep on roomy storage |
/var/lib/restic-manager/updates/ | Auto-updater work dir (download/, stage/, rollback/) |
/var/log/restic-server/ | rest-server + API logs (also in journalctl) |
Linux agent
/etc/restic-agent/config.env | Server URL, repo, paths, retention |
/etc/restic-agent/repo.password | Repository encryption password (600 root) |
/etc/restic-agent/client-id | Server-assigned client UUID |
/usr/local/bin/restic | restic binary |
/etc/systemd/system/restic-backup.service | One-shot backup service |
/etc/systemd/system/restic-backup.timer | Schedule (OnCalendar or OnUnitActiveSec) |
macOS agent
/etc/restic-agent/config.env | Same config as Linux |
/Library/LaunchDaemons/com.backupbloc.agent.plist | launchd job (StartCalendarInterval) |
/usr/local/bin/restic or /opt/homebrew/bin/restic | Homebrew-installed restic |
/var/log/backupbloc/ | Per-run logs |
DR node
/var/lib/restic-repos/ | Append-only mirror of the replicated repositories |
/etc/restic-server/users | rest-server htpasswd (append-only mode) |
/etc/backupbloc-dr/version | DR node version marker |
/root/.ssh/authorized_keys | Holds the main server's trigger key (added at install) |
/usr/local/bin/restic, rclone, rest-server | Tools installed by install-dr.sh |
settingsService management
# --- SERVER ---
systemctl restart restic-rest-server # restic repo server
systemctl restart restic-api # Flask mgmt API
systemctl restart nginx # reverse proxy + UI
# --- LINUX AGENT ---
systemctl status restic-backup.timer
systemctl start restic-backup.service # run now
journalctl -u restic-backup.service -n 50
# --- macOS AGENT ---
sudo launchctl kickstart -k system/com.backupbloc.agent
sudo launchctl list | grep backupbloc
# --- DR NODE ---
systemctl status restic-rest-server # append-only repo server
tail -f /var/log/restic-manager/dr/*.log # replication logs (on the MAIN server)
bug_reportTroubleshooting
Server installer stops at Let's Encrypt
Make sure port 80 is open to the public internet and DNS for the domain resolves to this host. Certbot runs standalone and binds port 80 itself — nothing else can be there.
Server UI loads but login fails
Check journalctl -u restic-api -n 50. The most common causes are: cryptography missing from the venv (rerun the installer or pip install cryptography), or the admin hash in /etc/restic-server/users uses a format rest-server doesn't accept (only bcrypt and APR1-MD5 are supported — the installer picks one automatically).
Agent fails at "fetching REST credentials"
The license bootstrap endpoint returned an error. Either the IP hasn't been authorised (server license), the personal key is wrong / revoked, or the agent can't reach the server on port 443. curl -v https://SERVER/api/health from the agent will tell you which.
Agent registers but no snapshots appear
- Trigger a run:
sudo systemctl start restic-backup.service(Linux) orsudo launchctl kickstart -k system/com.backupbloc.agent(macOS). - Check the log:
journalctl -u restic-backup.service -n 100or/var/log/backupbloc/. - If restic complains about unable to open config file, the repository wasn't initialised. On the server:
RESTIC_PASSWORD=<pass> restic init --repo /var/lib/restic-repos/<name>.
macOS agent skips user directories
Almost always Full Disk Access. Re-check System Settings → Privacy & Security → Full Disk Access — restic and /bin/bash must both be present and toggled on.
Certbot renewal didn't restart the services
The installer drops a deploy-hook at /etc/letsencrypt/renewal-hooks/deploy/restic-perms-restart.sh. Verify it exists and is executable. Run certbot renew --dry-run — it should print "deploy hook" in the output.
DR server stuck on "Awaiting SSH key"
The main server can't SSH into the DR yet. Run the authorise command shown in the panel on the DR host (it appends the main server's key to /root/.ssh/authorized_keys), confirm the SSH port matches what you entered, then click Test connection. From the main server you can sanity-check with ssh -i /etc/restic-manager/trigger_key root@DR_IP echo ok.
DR replication shows "failed"
Open Manage replication → View log for the repo. Common causes: the DR ran out of disk (free space is shown on the server card), rclone/restic not installed on a manually-added DR, or a transient SSH drop (the next scheduled sync retries automatically). The replication is append-only, so a failed run never damages the existing DR copy.
Restore from DR can't read the copy
The main server reads the DR repository over SSH with restic's SFTP backend. Ensure the DR is Online (Test connection) and that the repo password file exists on the main server at /etc/restic-manager/<repo>.password — it's the same password used for the live repository.