No description
Find a file
2026-03-27 02:38:17 +02:00
example.conf init commit 2026-03-26 23:00:04 +02:00
install.sh added forgotten SSH_USER config 2026-03-27 01:36:09 +02:00
jabs-completion.bash added tab completion 2026-03-27 01:12:58 +02:00
jabs.sh works! 2026-03-27 02:38:17 +02:00
postgres.sh works! 2026-03-27 02:38:17 +02:00
README.md fixing restic trying to speak sftp while host expects restic over rclone 2026-03-27 01:50:49 +02:00

JABS

JABS stands for Just Another Backup Service.

JABS is a small backup orchestrator for Linux systems. Its job is not to be a complete backup platform on its own, and it is not trying to hide what it is doing behind a large framework. Instead, it gives you a simple command, a small set of config files, and a predictable execution model for running backups from a machine that already knows how to reach the data you care about.

At a practical level, JABS does this:

  • reads a list of backup targets from a config file
  • looks at each target's URI or path
  • decides which backup backend should handle that target
  • runs the matching backup logic
  • writes logs per target
  • uses lock files so the same target path is not processed concurrently

That means you can define a PostgreSQL database, a filesystem path, or other future backend types in one place and run them through the same entrypoint. JABS is especially suited to environments where you want a Unix-friendly tool that can be installed as a single executable, configured with plain text files, and scheduled externally with cron, systemd timers, or your orchestration tooling.

The repository currently ships with:

  • the main jabs runner
  • an installer that bundles the project into one executable script
  • a PostgreSQL backup backend
  • an example targets file

Table of Contents

What JABS Does

JABS exists to answer a simple operational need: "I have several things that need backing up, they are not all the same kind of thing, and I want one small Linux command that can process them consistently."

Instead of giving every backup target its own ad hoc shell script, JABS lets you describe targets in a single file with a common structure. Each entry says what the target is, where it lives, how it should be reached, where its restic output should go, which SSH key should be used, and whether it should run natively, in a local Docker container, or in Docker Swarm.

JABS then walks through those entries one by one and performs the backup for each target using the backend that matches the target URI.

In other words, JABS is the coordinator. The module code does the backend-specific work.

How JABS Works

The high-level lifecycle of a jabs run is:

  1. JABS starts and loads /etc/jabs/config.conf.
  2. It ensures the configured lock and log directories exist.
  3. It reads /etc/jabs/targets.conf line by line.
  4. For each valid target entry, it detects the backend from the URI.
  5. It loads the corresponding backend module.
  6. It creates a lock file tied to the target's restic_path.
  7. It runs the selected backend handler for the chosen execution mode.
  8. It appends output to that target's log file.

This model keeps the runtime straightforward:

  • one executable
  • one main config file
  • one targets file
  • backend-specific functions for actual backup work

Current Capabilities

At the moment, this repository provides:

  • a Bash-based main runner
  • single-file installation into /usr/local/bin/jabs
  • automatic creation of /etc/jabs and /etc/jabs/targets.conf
  • PostgreSQL backup support
  • native mode support
  • local Docker mode support
  • Docker Swarm mode support

The code already includes backend detection rules for MariaDB and MySQL URIs, MongoDB, and filesystem paths, but those handlers are not yet shipped in this repository. Right now, PostgreSQL is the only backend implementation included here.

Installation

Install JABS as root:

sudo ./install.sh

The installer does not "compile" JABS in the traditional binary sense. Since JABS is Bash, installation is simply the process of building a single runnable script and placing it somewhere standard in the filesystem.

More specifically, the installer:

  • creates /etc/jabs if it does not exist
  • creates /etc/jabs/config.conf if it does not exist
  • creates /etc/jabs/targets.conf if it does not exist
  • creates /etc/jabs/ssh if it does not exist
  • bundles jabs.sh together with the shipped module files
  • installs the bundled result as /usr/local/bin/jabs
  • installs Bash completion to /etc/bash_completion.d/jabs
  • marks the installed file executable

After installation, jabs can be called directly like any other normal Linux command:

jabs

Filesystem Layout

JABS uses a simple Linux-style layout:

/usr/local/bin/jabs
/etc/jabs/config.conf
/etc/jabs/targets.conf
/etc/jabs/ssh/
/etc/jabs/runtime-state.json

Optional external modules may also live under:

/etc/jabs/modules/

What each path is for:

  • /usr/local/bin/jabs: the installed executable entrypoint
  • /etc/jabs/config.conf: global runtime configuration
  • /etc/jabs/targets.conf: list of backup targets to process
  • /etc/jabs/ssh/: service-managed SSH key directory
  • /etc/jabs/runtime-state.json: learned per-target runtime and timeout state
  • /etc/jabs/modules/: optional out-of-tree backend modules

Even though the installer produces a bundled single-file executable, the runtime still supports loading extra modules from /etc/jabs/modules/ if you want to extend it later without rebuilding the installed file.

Configuration

JABS expects a global configuration file at:

/etc/jabs/config.conf

This file is sourced by Bash, so it should contain shell variable assignments.

A minimal example looks like this:

LOCK_BASE="/var/lock/jabs"
LOG_BASE="/var/log/jabs"
SSH_USER=""
SSH_PORT="22"
RESTIC_BASE="sftp:backup@example.com:/backups"
# Optional:
# RESTIC_PASSWORD_FILE="/etc/jabs/restic-password"
# RESTIC_PASSWORD="replace-me"
# JABS_BACKUP_TIMEOUT_DEFAULT="900"
# JABS_BACKUP_TIMEOUT_STEP="600"
# JABS_BACKUP_TIMEOUT_MAX="7200"
# JABS_BACKUP_TIMEOUT_WARN_PCT="80"
# JABS_BACKUP_TIMEOUT_EXPAND_PCT="92"
# JABS_BACKUP_TIMEOUT_CRITICAL_PCT="90"
# JABS_BACKUP_RUNTIME_WINDOW="5"
# JABS_SWARM_LOCK_WAIT_TIMEOUT="60"
DOCKER_IMAGE_ALPINE="alpine:latest"

On a first install, install.sh creates this file automatically with the default template shown above. If the file already exists, it is left untouched.

Configuration Variables

LOCK_BASE

Directory where JABS stores lock files. Locks are used to prevent multiple runs from handling the same restic_path at the same time.

LOG_BASE

Directory where JABS writes per-target log files.

SSH_USER

Optional SSH username for the restic transport. If left empty, JABS warns and falls back to the user running jabs.

SSH_PORT

SSH port used for the SSH connection that restic opens to the remote repository endpoint derived from RESTIC_BASE.

RESTIC_BASE

Base repository location passed to restic. Each target appends its own restic_path under this base. JABS also derives the SSH target host from this value for the current PostgreSQL backend.

RESTIC_PASSWORD_FILE

Optional explicit file path containing the restic repository password.

RESTIC_PASSWORD

Optional inline restic repository password.

JABS_BACKUP_TIMEOUT_DEFAULT

Default backup timeout in seconds for new targets. Defaults to 900.

JABS_BACKUP_TIMEOUT_STEP

Per-target timeout increase step in seconds when a target repeatedly gets too close to its limit. Defaults to 600.

JABS_BACKUP_TIMEOUT_MAX

Maximum learned timeout in seconds for any target. Defaults to 7200.

JABS_BACKUP_TIMEOUT_WARN_PCT

Warn threshold, as a percentage of the current target timeout. Defaults to 80.

JABS_BACKUP_TIMEOUT_EXPAND_PCT

Auto-expand threshold, as a percentage of the current target timeout. Defaults to 92.

JABS_BACKUP_TIMEOUT_CRITICAL_PCT

Critical review threshold, as a percentage of the maximum timeout. Defaults to 90.

JABS_BACKUP_RUNTIME_WINDOW

Number of recent successful runs used for the rolling mean. Defaults to 5.

JABS_SWARM_LOCK_WAIT_TIMEOUT

Optional timeout in seconds for waiting on a Swarm backup container to acquire its shared lock file. Defaults to 60.

DOCKER_IMAGE_ALPINE

Container image used by the Docker-based PostgreSQL backup handlers.

Why RESTIC_BASE and SSH_PORT Matter

In the shipped PostgreSQL backend, JABS does not simply write backups to an arbitrary local directory. It runs restic with a server-side remote command path over SSH.

In practice, the current backend uses a command pattern equivalent to:

restic -o rclone.program="ssh -i ... -p $SSH_PORT <derived-from-RESTIC_BASE>" -o rclone.args="" ...

That means the backup client talks to a controlled remote endpoint through SSH, and the remote side is expected to expose only the backup command path you intend it to expose.

Restic Password Resolution

JABS resolves the restic password in this order:

  1. RESTIC_PASSWORD_FILE
  2. RESTIC_PASSWORD
  3. /etc/jabs/restic-password

If none of those are available, the backup run fails.

For a Linux service, the recommended default is to store the password in:

/etc/jabs/restic-password

and keep that file readable only by root.

Important Runtime Expectation

The generated default config is only a starting point. You should edit /etc/jabs/config.conf to match your actual environment before relying on JABS in production.

Targets File

JABS reads targets from:

/etc/jabs/targets.conf

Each non-empty, non-comment line represents one backup target.

Expected format:

name uri network restic_path ssh_key docker_mode

Field Reference

name

A human-readable identifier for the backup target. This is also used in log messages and temporary naming.

uri

The source location. This determines which backend module JABS selects. For example, a postgres://... URI selects the PostgreSQL backend.

network

The Docker network name used for containerized execution modes. If the target does not need a network, you can use a placeholder like none.

restic_path

The destination path segment used under your configured RESTIC_BASE. JABS also uses this field when creating lock and log file names.

ssh_key

The SSH key identifier used by the backend implementation.

When this value is just a name, JABS resolves it in this order:

  • ${HOME}/.ssh/<ssh_key>
  • /etc/jabs/ssh/<ssh_key>
  • absolute path given directly in the field

If none of those exist, containerized modes treat the value as a Docker secret name.

If you want to force secret handling explicitly, use:

secret=<secret_name>

Examples:

  • backup-key-main -> try local file lookup first, then fall back to a secret named backup-key-main
  • secret=backup-key-main -> skip file lookup and use the Docker secret directly
  • /etc/jabs/ssh/backup-key-main -> use that exact file

In containerized modes, JABS injects one of these into the container:

  • SSH_KEY when the key was resolved outside from a real file
  • SSH_KEY_FILE when the key is provided through a Docker secret

Native mode still requires a real key file on the daemon side.

docker_mode

Execution selector:

  • 0: native execution on the host
  • 1: local Docker container execution
  • 2: Docker Swarm service execution

Example

# name        uri                                                      network           restic_path        ssh_key                             docker (0=no,1=local,2=swarm)
auth-db       postgres://appuser:app-password@postgres-db:5432/appdb   app-net           auth-db            backup-key-auth                     2
web-db        mysql://webuser:web-password@mysql-db:3306/webapp        web-net           web-db             backup-key-web                      2
app-logs      /var/log/myapp                                           none              app-logs           backup-key-logs                     1
local-files   /srv/data                                                none              local-files        backup-key-files                    0

A sample file is included at example.conf.

Execution Modes

JABS supports three execution modes at the target level.

Native Mode

Native mode is selected with:

docker_mode = 0

In this mode, the backup logic runs directly on the host system. This is the simplest mode and assumes all required tools are already installed on the machine running JABS.

Local Docker Mode

Local Docker mode is selected with:

docker_mode = 1

In this mode, the backend runs inside a local Docker container attached to the configured Docker network.

Docker Swarm Mode

Docker Swarm mode is selected with:

docker_mode = 2

In this mode, the backend is executed as a Docker Swarm service.

Swarm mode is constrained back onto the same node that launched the backup. This allows JABS to keep using kernel-backed local file locks instead of adding a separate distributed lock service.

The Swarm backup container receives a bind mount of the JABS lock directory and acquires the target lock itself inside the container. The daemon then waits for that lock to appear, watches for it to be released, and removes the Swarm service when the backup is done.

This keeps the locking model simple:

  • the container owns the flock
  • the kernel releases that lock automatically if the container exits or crashes
  • no Redis or other coordination service is required

The daemon waits up to JABS_SWARM_LOCK_WAIT_TIMEOUT seconds for the container to acquire the lock after the service is created.

When the configured network name is a logical stack network name, JABS will look for matching real Swarm network names. If multiple candidates exist, it tries them one by one. A candidate can only be ignored in interactive mode when an operator explicitly confirms it. jabs --daemon fails hard instead of silently skipping networks.

Security Model

JABS is designed around the idea that backups should remain protected even if the machine running JABS is later compromised.

The intended model is:

  • JABS runs on the primary machine or orchestration node
  • backup data is sent to a remote backup host
  • restic is used as the backup engine
  • the remote path is reached through an SSH-based rclone.program / forced-command setup
  • the backup side is configured in append-only mode

The important point is that the source machine should not have unrestricted delete or repository-admin rights on the backup server.

What This Protects Against

If the main application server is compromised, an attacker may still be able to run jabs or read local config and keys available on that host. But if the remote backup side is set up correctly, that does not automatically give the attacker the ability to rewrite history or destroy existing snapshots.

With a restricted remote command and append-only enforcement on the backup side:

  • the source host can add new backup data
  • the source host cannot freely prune, rewrite, or delete existing backup state
  • old snapshots remain immutable unless someone with separate administrative access changes that policy

This is the security property you want from a backup system: compromise of the production host should not mean immediate loss of the backups.

Important Boundary

That immutability guarantee does not come from JABS alone. It depends on how the remote backup side is configured.

For the model to hold, the backup server must enforce:

  • a restricted SSH command path, such as the forced-command flow used by the current backend
  • append-only permissions for the backup client identity
  • separate administrative credentials for destructive operations such as pruning or repository maintenance

If the same credentials available on the source machine also have full administrative control over the repository, then backups are not meaningfully immutable.

Practical Interpretation

So the correct way to think about JABS is:

  • JABS is the orchestrator
  • restic is the backup engine
  • the remote server policy is what provides immutability

When deployed that way, the repository becomes append-only from the perspective of the production host, which is exactly the behavior you want for high-trust backup retention.

Example Remote SSH Setup

One practical way to enforce this on the backup server is to pin the client's SSH key to a forced rclone serve restic command with append-only mode enabled.

Actual example:

File:

/home/backups/.ssh/authorized_keys

Line to add:

command="rclone serve restic --stdio --append-only /home/backups/backuprepo",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 add-a-random-key/CegyieMNRUkJzxOo/qpyzxs5V3 somecomment

What this does:

  • forces that key to run only rclone serve restic
  • exposes the repository over stdio instead of a general-purpose shell
  • enables append-only mode on the remote side
  • disables port forwarding, X11 forwarding, agent forwarding, and PTY allocation for that key

Operationally, this means the key used by JABS can push backup data into /home/backups/backuprepo, but it does not receive a normal interactive SSH session. Combined with append-only enforcement, this is the pattern that keeps previously written backup data protected from routine compromise of the source host.

If you use this pattern, your SSH_PORT, key material, and RESTIC_BASE values should all be aligned with that remote repository layout and access model.

Backend Detection

JABS chooses a backend by inspecting the target URI.

The current detection rules are:

  • postgres:// -> postgres
  • mysql:// or mariadb:// -> mariadb
  • mongodb:// -> mongodb
  • anything that is not a recognized URI scheme is treated as a filesystem path and maps to files

This means the target definition itself tells JABS what kind of thing it is backing up. You do not need a separate "type" field as long as the URI format is consistent.

For filesystem targets, JABS intentionally accepts plain POSIX-style paths broadly. That includes values like:

  • /var/log/app
  • ./backups/data
  • data/archive
  • data

In practice, JABS treats anything that does not match a known ...:// scheme as a filesystem path. The unknown result is reserved for values that still look like a URI, but use an unsupported scheme.

Important detail: detection support does not automatically mean implementation support. At the moment, this repository only ships the PostgreSQL backend module.

Running JABS

Once installed and configured, running JABS is intentionally simple:

jabs

This shows the command help.

To run backups explicitly, use:

jabs run

By default, jabs runs in interactive operator mode:

  • backend output is shown on the terminal
  • the same output is still written to the per-target log file
  • safety prompts are allowed when JABS needs operator confirmation
  • high-level progress messages are shown while JABS is working

For unattended runs, use:

jabs --daemon

In daemon mode, JABS is non-interactive and writes only to the configured log files.

For detailed troubleshooting, use:

jabs --debug

Debug mode keeps interactive behavior but also prints internal execution decisions such as resolved network candidates, lock handling, and backend launch attempts.

Additional commands:

jabs list
jabs ps
  • jabs list shows configured targets
  • jabs ps shows currently active runs based on held lock files

If Bash completion is enabled on the system, jabs command and option completion is installed automatically.

JABS also keeps learned per-target runtime state in:

/etc/jabs/runtime-state.json

That state is used to:

  • track recent successful runtimes per target
  • compute a rolling mean over the last few runs
  • warn in interactive mode when a target is getting close to its current timeout
  • auto-expand only that target's timeout in fixed steps when it repeatedly runs too close to the limit
  • raise a critical warning when a target is getting too close to the configured maximum timeout and should be reviewed by a human

If a daemon run fails, JABS writes a failure flag file at:

/etc/jabs/daemon-failure.flag

The next interactive jabs run prints a large warning and shows the first lines of that file so the failure is hard to miss. The warning stays until you remove or rename the flag file after investigation.

JABS processes the configured targets sequentially in the order they appear in /etc/jabs/targets.conf.

A normal operational flow is usually:

  1. install JABS
  2. create /etc/jabs/config.conf
  3. define targets in /etc/jabs/targets.conf
  4. run jabs manually to verify behavior
  5. schedule it with cron, a systemd timer, or your preferred orchestration layer

Logging and Locking

JABS keeps runtime behavior explicit and inspectable.

Logging

For each target, JABS writes to a log file under:

LOG_BASE/<restic_path>.log

These logs include start and completion messages as well as backend command output.

Locking

For each target, JABS creates a lock file under:

LOCK_BASE/<restic_path>.lock

The lock is acquired with flock. This prevents concurrent work on the same logical backup path, which is useful when JABS is triggered by schedule overlap or multiple invocations.

Modules and Extensibility

JABS is structured around backend modules.

The main runner is responsible for orchestration:

  • reading config
  • reading targets
  • selecting a backend
  • managing locks
  • writing logs

The backend module is responsible for the backup mechanics for that backend.

The repository currently includes:

At install time, shipped modules are bundled into the installed executable. In addition, JABS can still source external modules from /etc/jabs/modules/ if you want to add new backend handlers without changing the repository layout.

Current Limitations

The current codebase is intentionally small, but it has some important boundaries that should be understood clearly:

  • targets are processed sequentially - multithreading/multicontainers coming up
  • backend behavior depends on external tools such as pg_dump, restic, docker, ssh, and related runtime setup - checks for these will be added asap