Technical whitepaper — PodHeitor SFTP for Bacula

Technical whitepaper — PodHeitor SFTP for Bacula

Technical Whitepaper — Version 2.0.0 — May 2026

Author: Heitor Faria · Website: https://podheitor.com · Email: heitor@opentechs.lat · Phone / WhatsApp: +1 786 726-1749 | +55 61 98268-4220

Special offer. Bring your renewal proposal for any commercial enterprise backup platform — Veeam, Commvault, NetBackup, or others. We will benchmark a head-to-head proposal targeting at least 50% savings with stronger agentless SSH/SFTP functionality. Contact heitor@opentechs.lat for a written quote.

Table of contents

  1. Executive summary
  2. Introduction & market context
  3. Architecture overview
  4. Backup modes deep dive
  5. Feature matrix
  6. Installation guide
  7. Configuration reference
  8. FileSet examples
  9. Sizing & capacity planning
  10. Performance considerations
  11. Compatibility matrix
  12. Security
  13. Monitoring
  14. Troubleshooting guide
  15. Use cases & deployment scenarios
  16. Comparison with other approaches
  17. Roadmap
  18. Conclusion
  19. Contact information
  20. Legal / copyright

1. Executive summary

Modern IT infrastructure is heterogeneous. A typical enterprise operates Linux servers alongside Cisco switches, Juniper firewalls, Synology NAS appliances, Raspberry Pi IoT gateways, legacy AIX or Solaris machines, and dozens of cloud VMs spread across AWS, GCP, and Azure. Backing up all of these assets with a single platform has historically required either installing a backup agent on every device — impossible for network equipment and many embedded systems — or deploying a maze of cron jobs, rsync scripts, and one-off transfer mechanisms that offer no catalog, no retention policy, and no reliable restore path.

The PodHeitor SFTP Backup Plugin for Bacula resolves this fragmentation. It extends Bacula Community Edition with native SSH/SFTP backup and restore capabilities, enabling any SSH-accessible device — server, NAS, switch, router, firewall, IoT gateway, or cloud service — to be brought under the same Bacula umbrella that already covers the rest of the infrastructure. No software installation is required on the remote side. Port 22 is the only requirement.

The plugin is built entirely in Rust, following Bacula’s official Metaplugin architecture — the same framework used by the upstream Kubernetes and Docker plugins. Two components ship in each package: a lightweight cdylib loaded by bacula-fd, and a Rust backend binary that performs all SSH/SFTP I/O via libssh2. The cdylib-backend split keeps the FD process stable even if a network device misbehaves mid-transfer. Backup levels (Full, Incremental, Differential) are determined by standard mtime comparison. Include/exclude glob filters, multi-server FileSets, SHA256 signatures, LZ4/GZIP compression, and AES encryption all come from Bacula’s native feature set at no additional cost.

Version 2.0.0, released April 2026, completed a full rewrite of both components from legacy C++/Python to pure Rust, eliminated the Python runtime dependency, and sealed both artifacts under a proprietary license with zero AGPL-licensed static dependencies. The result is a leaner, faster, fully proprietary plugin that drops into any existing Bacula Community installation with no Director-side configuration changes.


2. Introduction & market context

2.1 The agentless backup problem

The standard Bacula Community deployment model requires a File Daemon (FD) running on every host to be backed up. The FD listens on TCP port 9102, transfers file data to the Storage Daemon, and maintains metadata for the catalog. This model works well for homogeneous Linux or Windows server fleets where packages exist and port access is controllable.

It breaks down in five classes of environments that are increasingly common:

  • Network equipment. Cisco IOS-XE, Junos, FortiOS, PAN-OS, RouterOS, and similar network operating systems do not support Linux packages. These devices expose SSH/SFTP as the only programmatic file-access interface. Configuration backup — the most critical DR artefact for network operations — is therefore outside the reach of standard Bacula without external scripting.
  • NAS appliances. Synology DSM, QNAP QTS, TrueNAS, and similar platforms run heavily customised Linux environments where installing third-party daemons is either unsupported or impractical. All of them have native SFTP/SSH.
  • Legacy and cross-platform servers. AIX, HP-UX, and Solaris systems may not have current Bacula FD packages. Old CentOS 5/6 servers still in production often lack a compatible FD. Any system where SSH is the only viable remote access mechanism.
  • IoT and embedded devices. Raspberry Pi gateways, industrial PLCs, and similar devices with constrained storage and CPU cannot absorb the overhead of a persistent daemon. SFTP access is lightweight and universally available.
  • Cloud SFTP services. AWS Transfer Family, Azure Blob SFTP, Hetzner Storage Box, and similar managed services expose data exclusively via SFTP. No agent can be installed on the server side.

2.2 Why existing approaches fall short

Tool Approach Limitation
Bacula Community (native, no plugin) Requires FD on every host Impossible on network equipment, NAS, cloud SFTP services
Veeam Agent-based or agentless via hypervisor No native SSH/SFTP path; no network device support
Commvault File System iDataAgent Agent install required; no network OS support
NetBackup Client agent Same limitation; proprietary and expensive
rsync + cron Agentless SSH No catalog, no retention, no point-in-time restore, no monitoring
Custom scp scripts Ad-hoc file copy No versioning, no verification, unmanageable at scale
SSHFS mountpoints FUSE mount over SSH Stale mounts, FUSE kernel module required, hung stat calls blocking FD

2.3 The PodHeitor approach

The plugin transforms Bacula Community into a true agentless backup platform for SSH-accessible resources. One Bacula File Daemon, with the plugin installed, can back up an unlimited number of remote systems. The Bacula Director, catalog, storage pool, retention policies, schedules, and restore workflow all apply identically — the plugin simply adds SSH/SFTP as the data transport layer.

This approach is consistent with the broader PodHeitor plugin philosophy: Rust-native, zero runtime dependencies beyond libssh2 and OpenSSL, PTCOMM protocol architecture for clean cdylib/backend separation, and full integration with Bacula’s native features rather than reimplementing them.


3. Architecture overview

3.1 Two-component design

The plugin ships as two binaries in the same package:

Component File Role
Bacula FD plugin (cdylib) /opt/bacula/plugins/podheitor-sftp-fd.so Loaded by bacula-fd at startup; implements Bacula plugin API; defines the @sftp namespace; spawns and bridges the backend
Backend binary /opt/bacula/bin/podheitor-sftp-backend Forked per-job by the cdylib; performs all SSH/SFTP I/O via libssh2; handles directory walking, mtime filtering, and file transfer

This separation provides three key advantages:

  1. Isolation. All network I/O and SSH logic lives in the backend. A crash, hang, or timeout in the backend cannot corrupt the Bacula FD process or affect other concurrent jobs.
  2. Upgradability. The backend binary can be replaced without restarting bacula-fd, since only the cdylib touches the Bacula plugin ABI.
  3. Testability. The backend can be exercised directly in integration tests without involving Bacula at all — SSH behaviour, include/exclude logic, and PTCOMM framing are all independently verifiable.

3.2 PTCOMM protocol

The cdylib and backend communicate over the child process’s stdin/stdout using PTCOMM (PodHeitor Transport Communications), a length-tagged framing protocol shared across all PodHeitor plugins:

┌───────┬──────────────┬─────┬──────────────────────┐
│ Status│ Length (6d)  │ n  │ Payload (N bytes)    │
│ 1 byte│  6 bytes     │1byte│ 0..999999 bytes      │
└───────┴──────────────┴─────┴──────────────────────┘

Packet types used by the SFTP plugin:

Status Type Direction Description
C Command Bidirectional FNAME, STAT, HELLO, BackupStart, etc.
D Data Backend → FD Raw file data payload (64 KB chunks)
F EOD Bidirectional End of Data / ACK
E Error Backend → FD Error message
I Info Backend → FD Informational / progress message
A Abort Backend → FD Abort the current job
T Terminate FD → Backend Graceful shutdown signal

3.3 Architecture diagram

┌─────────────────────────────────────────────────────────────────────┐
│                        BACULA DIRECTOR                              │
│                                                                     │
│   Job "SFTP-Daily"  ──▶  FileSet "SFTP-Backup"                     │
│   Type = Backup          Plugin = "podheitor-sftp: host=... "       │
│   Level = Incremental    Signature = SHA256; Compression = LZ4      │
└───────────────────────────────────┬─────────────────────────────────┘
                                    │  Bacula Director Protocol
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      BACULA FILE DAEMON (FD)                        │
│                                                                     │
│   ┌──────────────────────────────────────────────────────────────┐  │
│   │  podheitor-sftp-fd.so  (Pure Rust cdylib, metaplugin-rs)     │  │
│   │  • Registers with FD via bpFuncs API                         │  │
│   │  • Defines @sftp namespace                                   │  │
│   │  • Spawns Rust backend subprocess                            │  │
│   │  • Bridges FD callbacks <—> PTCOMM packets                   │  │
│   └───────────────────────┬──────────────────────────────────────┘  │
│                           │ stdin/stdout (PTCOMM binary protocol)   │
│   ┌───────────────────────▼──────────────────────────────────────┐  │
│   │  podheitor-sftp-backend  (Rust binary, ssh2 crate/libssh2)   │  │
│   │  • PTCOMM protocol handler                                   │  │
│   │  • SSH/SFTP connection via libssh2                           │  │
│   │  • Recursive directory walker with include/exclude           │  │
│   │  • File data transfer (FNAME/STAT/DATA)                      │  │
│   │  • Full/Incremental/Differential via mtime comparison        │  │
│   └───────────────────────┬──────────────────────────────────────┘  │
│                           │ SSH/SFTP (TCP port 22)                  │
└───────────────────────────┼─────────────────────────────────────────┘
                            ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      REMOTE SFTP HOST                               │
│   OpenSSH Server — no Bacula agent required                         │
│   Read access to backup paths only                                  │
└─────────────────────────────────────────────────────────────────────┘

3.4 Separation of concerns

podheitor-sftp-fd.so (Rust cdylib) podheitor-sftp-backend (Rust binary)
FD registration and lifecycle SSH/SFTP via libssh2
PTCOMM bridge to FD callbacks Recursive directory walking
@sftp namespace definition File reading and data chunking
Zero SFTP logic — ~600 LOC mtime-based incremental comparison
metaplugin-rs framework Include/exclude glob filtering
Bacula ABI surface Full PTCOMM protocol — ~1500 LOC

3.5 Data flow — incremental backup

 FD Plugin (.so)          Backend (Rust)           SFTP Server
     │                         │                        │
  1. │── Hello podheitor ─────▶│                        │
     │◀── Hello Bacula ────────│                        │
     │                         │                        │
  2. │── Level=Incremental ───▶│                        │
     │── Since=<timestamp> ───▶│                        │
     │── EOD ─────────────────▶│                        │
     │                         │                        │
  3. │── host=server ─────────▶│                        │
     │── user=backup ─────────▶│                        │
     │── keyfile=/path/key ───▶│                        │
     │── path=/data ──────────▶│                        │
     │── include=*.pdf ───────▶│                        │
     │── exclude=*.tmp ───────▶│                        │
     │── EOD ─────────────────▶│                        │
     │                         │                        │
  4. │── BackupStart ─────────▶│                        │
     │                         │── SSH Connect ────────▶│
     │                         │◀── Auth OK ────────────│
     │                         │── SFTP Open Channel ──▶│
     │                         │                        │
  5. │                         │── listdir /data ──────▶│
     │                         │◀── [files + mtimes] ───│
     │                         │   (skip mtime < Since) │
     │◀── FNAME/@sftp/.../f2 ─│                        │
     │◀── STAT:F 2048 ...    ─│                        │
     │◀── DATA [chunks] ──────│◀── read file2 ─────────│
     │◀── EOD (end data) ─────│                        │
     │                         │                        │
  6. │◀── EOD (end files) ────│                        │
     │── TERM ────────────────▶│── Close SFTP ─────────▶│

4. Backup modes deep dive

4.1 Full backup

A Full backup transfers every file and directory under the configured path, regardless of modification time. The backend connects to the SFTP server, performs a recursive directory walk, and sends every file to the FD plugin via PTCOMM DATA packets.

Since = 0 (all timestamps included)

/data/
├── report.pdf          ──▶ TRANSFER
├── spreadsheet.xlsx    ──▶ TRANSFER
├── notes.txt           ──▶ TRANSFER
└── archive/
    ├── 2024-q1.pdf     ──▶ TRANSFER
    └── 2024-q2.pdf     ──▶ TRANSFER

Total: ALL files

A Full backup is self-contained — it can be restored without any other backup set. Full backups are the recommended starting point for all schedules and should be run at minimum weekly for most environments.

4.2 Incremental backup

An Incremental backup transfers only files whose mtime (modification timestamp) is greater than or equal to the timestamp of the most recent backup — whether that backup was a Full, Differential, or prior Incremental.

Since = <timestamp of last backup>

/data/
├── report.pdf   (mtime: before Since) ──▶ SKIP
├── draft.pdf    (mtime: after Since)  ──▶ TRANSFER
├── notes.txt    (mtime: before Since) ──▶ SKIP
└── archive/                           ──▶ ALWAYS traversed (dirs)
    ├── 2024-q1.pdf (mtime: before)    ──▶ SKIP
    └── 2025-q1.pdf (mtime: after)     ──▶ TRANSFER

Total: only CHANGED files

Incrementals are smallest and fastest. The trade-off is that a restore requires the Full backup plus every Incremental in the chain. For most SFTP use cases (configuration files, documents, NAS data), file change rates are low and Incremental chains remain short.

4.3 Differential backup

A Differential backup transfers all files changed since the last Full backup, ignoring any Incrementals in between. Each Differential is therefore larger than an Incremental but smaller than a Full.

Restore from Differential:
  Full (Day 1) + Differential (Day 4) = 2 volumes only

Restore from Incremental chain:
  Full (Day 1) + Incr (Day 2) + Incr (Day 3) + Incr (Day 4) = 4 volumes

Differentials are particularly useful for network equipment config backups where change frequency is low but the total config corpus is small and a simpler restore path is preferred.

4.4 Backup level comparison

Level What is transferred Restore requires Best for
Full All files This volume only Weekly anchor; first backup
Incremental Files changed since last backup (any level) Full + all Incrementals Daily backups; low-change SFTP targets
Differential Files changed since last Full Full + this Differential Network configs; simpler restore path desired

4.5 Include/exclude filtering

Before transferring any file, the backend applies glob-based include and exclude filters matched against the filename (not the full path). The evaluation order is: exclude first, then include.

Parameter Syntax Effect
include Comma-separated glob patterns Only files matching at least one pattern are transferred. Directories are always traversed.
exclude Comma-separated glob patterns Files and directories matching any pattern are skipped.

When include is specified, all files that do not match any include pattern are silently skipped. This is the recommended approach for network equipment config backups where only *.conf, *.cfg, *.backup, or *.rsc files matter.


5. Feature matrix

Feature Supported Notes
Agentless backup (SSH only) Yes No software installation on remote host
Full backup Yes All files under configured path
Incremental backup Yes mtime-based comparison
Differential backup Yes mtime since last Full
SSH key authentication (ed25519) Yes Recommended for production
SSH key authentication (RSA, ECDSA) Yes Legacy key types
SSH password authentication Yes Not recommended for production
SSH agent forwarding Yes Integrates with system ssh-agent
Host key verification (known_hosts) Yes MITM protection; enabled by default
Custom SSH port Yes port= parameter; default 22
Custom connection timeout Yes timeout= in seconds; default 30
Include glob filters Yes POSIX glob; comma-separated
Exclude glob filters Yes POSIX glob; comma-separated
Metadata preserved (permissions, UID/GID, timestamps) Yes Restored to local FS
Symlink handling Yes Symlinks cataloged as type S
Multiple SFTP servers in one FileSet Yes N Plugin= lines in Include block
SHA256 file signatures Yes Native Bacula Options
LZ4 / GZIP compression Yes Native Bacula Options
AES encryption Yes Native Bacula Options
Pre-backup file listing (estimate) Yes estimate job=… listing
Post-backup catalog query Yes list files jobid=X
Interactive restore tree browsing Yes restore → cd/ls/mark
Selective file restore Yes mark individual files in bconsole
Abort on file error Yes abort_on_error= parameter
Debug logging Yes PODHEITOR_DEBUG=1 env var
RPM package (RHEL/OL/Rocky 9) Yes Single-command install
Arch Linux PKGBUILD Yes makepkg -si
Build from source Yes make all; no Bacula source tree needed
Windows OpenSSH Server target Yes Native OpenSSH in Windows 10/11/Server 2019+
Network equipment targets (Cisco, Juniper, MikroTik, etc.) Yes Any device with SSH/SFTP
Large file support (> 1 GB) Yes Streamed in 64 KB chunks; no size limit

6. Installation guide

6.1 Prerequisites

  • Bacula Community 13.0.0 or later installed with bacula-fd running
  • Plugin Directory configured in bacula-fd.conf (typically /opt/bacula/plugins)
  • Runtime libraries: libssh2 and openssl (system packages)
  • SSH key pair generated for backup operations (ed25519 recommended)
  • Read access granted to the SSH user on all remote paths to be backed up

6.2 RPM installation (RHEL / Oracle Linux / Rocky Linux 9)

# 1. Install runtime dependencies
sudo dnf install libssh2 openssl-libs

# 2. Install the plugin RPM
sudo rpm -ivh podheitor-sftp-fd-plugin-2.0.0-1.el9.x86_64.rpm

# 3. Verify installed files
ls -l /opt/bacula/plugins/podheitor-sftp-fd.so
ls -l /opt/bacula/bin/podheitor-sftp-backend

# 4. Confirm libssh2 is reachable
ldconfig -p | grep libssh2

# 5. Restart FD to load the plugin
sudo systemctl restart bacula-fd

# 6. Confirm the plugin loaded
echo "status client" | bconsole | grep -i podheitor
# Expected: Plugin: podheitor-sftp-fd.so

6.3 DEB installation (Debian 12 / Ubuntu 22.04+)

# 1. Install runtime dependencies
sudo apt install libssh2-1 openssl

# 2. Install the DEB package
sudo dpkg -i podheitor-sftp-fd-plugin_2.0.0-1_amd64.deb

# 3. Verify installed files
ls -l /opt/bacula/plugins/podheitor-sftp-fd.so
ls -l /opt/bacula/bin/podheitor-sftp-backend

# 4. Restart FD
sudo systemctl restart bacula-fd

6.4 Arch Linux (PKGBUILD)

makepkg -si

6.5 Build from source

# Build dependencies: Rust 1.75+, cargo, libssh2-dev, libssl-dev, pkg-config
# No Bacula source tree required.

git clone https://github.com/podheitor/podheitor-sftp-bacula.git
cd podheitor-sftp-bacula
make all
sudo make install PREFIX=/opt/bacula

Installed files:

/opt/bacula/
├── plugins/
│   └── podheitor-sftp-fd.so       ← FD plugin module (Rust cdylib)
└── bin/
    └── podheitor-sftp-backend     ← Rust backend binary

6.6 FD plugin directory configuration

Ensure bacula-fd.conf points to the correct directory:

FileDaemon {
  Name = myserver-fd
  Plugin Directory = /opt/bacula/plugins
}

6.7 SSH key setup

# Generate a dedicated ed25519 key for Bacula (no passphrase for automation)
sudo mkdir -p /etc/bacula/.ssh
sudo ssh-keygen -t ed25519 -f /etc/bacula/.ssh/id_ed25519 -N "" -C "bacula-backup"

# Set secure permissions
sudo chown -R root:bacula /etc/bacula/.ssh
sudo chmod 700 /etc/bacula/.ssh
sudo chmod 640 /etc/bacula/.ssh/id_ed25519
sudo chmod 644 /etc/bacula/.ssh/id_ed25519.pub

# Copy public key to remote server
sudo ssh-copy-id -i /etc/bacula/.ssh/id_ed25519.pub backup@server.local

# Pre-populate known_hosts for host key verification
sudo ssh-keyscan -H server.local | sudo tee -a /etc/bacula/.ssh/known_hosts

7. Configuration reference

7.1 Plugin parameter string

Plugin = "podheitor-sftp: <param1>=<value1> <param2>=<value2> ..."

7.2 Parameter reference

Parameter Required Default Type Description
host Yes string SFTP server hostname or IP address
port No 22 int SSH/SFTP port number
user Yes string SSH username on remote server
password No string SSH password (prefer keyfile)
keyfile No string Path to SSH private key file
passphrase No string Passphrase for encrypted private key
path Yes / string Remote base directory to back up
known_hosts No string Path to SSH known_hosts file
verify_host No yes bool Verify SSH host key (MITM protection)
timeout No 30 int Connection timeout in seconds
include No string Comma-separated glob patterns to include (e.g. *.pdf,*.docx)
exclude No string Comma-separated glob patterns to exclude (e.g. *.tmp,*.log)
abort_on_error No no bool Abort job on file read error

7.3 Minimal FileSet configuration

FileSet {
  Name = "SFTP-Minimal"
  Include {
    Options { Signature = SHA256 }
    Plugin = "podheitor-sftp: host=server.local user=backup keyfile=/etc/bacula/.ssh/id_ed25519 path=/data"
  }
}

7.4 Full configuration with all options

FileSet {
  Name = "SFTP-Full"
  Include {
    Options {
      Signature = SHA256
      Compression = LZ4
    }
    Plugin = "podheitor-sftp: host=fileserver.example.com port=22 user=svc-backup keyfile=/etc/bacula/.ssh/id_ed25519 path=/srv/data known_hosts=/etc/bacula/.ssh/known_hosts verify_host=yes timeout=60 include=*.pdf,*.docx,*.xlsx exclude=*.tmp,*.cache,*.log"
  }
}

Job {
  Name = "SFTP-Documents-Backup"
  Type = Backup
  Level = Incremental
  Client = mybacula-fd
  FileSet = "SFTP-Full"
  Schedule = "SFTP-WeeklyCycle"
  Storage = File1
  Pool = Default
  Messages = Standard
  Priority = 10
}

Schedule {
  Name = "SFTP-WeeklyCycle"
  Run = Full 1st sun at 23:05
  Run = Differential 2nd-5th sun at 23:05
  Run = Incremental mon-sat at 23:05
}

7.5 Restore job

Job {
  Name = "SFTP-Documents-Restore"
  Type = Restore
  Client = mybacula-fd
  FileSet = "SFTP-Full"
  Storage = File1
  Pool = Default
  Messages = Standard
  Where = /tmp/sftp-restore
}

8. FileSet examples

8.1 NAS shared folder backup

FileSet {
  Name = "NAS-Synology"
  Include {
    Options { Signature = SHA256; Compression = LZ4 }
    Plugin = "podheitor-sftp: host=synology.local user=admin keyfile=/etc/bacula/.ssh/nas_key path=/volume1/shared"
    Plugin = "podheitor-sftp: host=synology.local user=admin keyfile=/etc/bacula/.ssh/nas_key path=/volume1/photos"
    Plugin = "podheitor-sftp: host=synology.local user=admin keyfile=/etc/bacula/.ssh/nas_key path=/volume1/documents"
  }
}

8.2 Network equipment configuration backup

FileSet {
  Name = "Network-Equipment-Configs"
  Include {
    Options { Signature = SHA256 }
    # Cisco Catalyst — running-config only
    Plugin = "podheitor-sftp: host=10.0.0.1 user=admin keyfile=/etc/bacula/.ssh/net_key path=/ include=*.conf,*.cfg verify_host=no"
    # Juniper SRX — /config directory
    Plugin = "podheitor-sftp: host=10.0.0.2 user=backup keyfile=/etc/bacula/.ssh/net_key path=/config verify_host=no"
    # MikroTik RouterOS — backup and export files
    Plugin = "podheitor-sftp: host=10.0.0.3 user=admin keyfile=/etc/bacula/.ssh/net_key path=/ include=*.backup,*.rsc verify_host=no"
    # FortiGate — config backup
    Plugin = "podheitor-sftp: host=10.0.0.4 user=admin keyfile=/etc/bacula/.ssh/net_key path=/ include=*.conf verify_host=no"
  }
}

Schedule {
  Name = "Network-Daily"
  Run = Full daily at 02:00
}

Job {
  Name = "Network-Config-Backup"
  Type = Backup
  Level = Full
  Client = bacula-fd
  FileSet = "Network-Equipment-Configs"
  Schedule = "Network-Daily"
  Storage = File1
  Pool = Default
  Messages = Standard
}

8.3 Multiple web servers

FileSet {
  Name = "WebServers-Backup"
  Include {
    Options { Signature = SHA256; Compression = LZ4 }
    Plugin = "podheitor-sftp: host=web1.prod.com user=deploy keyfile=/etc/bacula/.ssh/web_key path=/var/www exclude=*.log,*.tmp,cache"
    Plugin = "podheitor-sftp: host=web1.prod.com user=deploy keyfile=/etc/bacula/.ssh/web_key path=/etc/nginx"
    Plugin = "podheitor-sftp: host=web2.prod.com user=deploy keyfile=/etc/bacula/.ssh/web_key path=/var/www exclude=*.log,*.tmp,cache"
    Plugin = "podheitor-sftp: host=web2.prod.com user=deploy keyfile=/etc/bacula/.ssh/web_key path=/etc/nginx"
  }
}

8.4 Cloud SFTP services

FileSet {
  Name = "Cloud-SFTP-Services"
  Include {
    Options { Signature = SHA256; Compression = LZ4 }
    # AWS Transfer Family
    Plugin = "podheitor-sftp: host=s-1234abcd.server.transfer.us-east-1.amazonaws.com user=myuser keyfile=/etc/bacula/.ssh/aws_sftp_key path=/data"
    # Hetzner Storage Box (port 23)
    Plugin = "podheitor-sftp: host=u12345.your-storagebox.de port=23 user=u12345 keyfile=/etc/bacula/.ssh/hetzner_key path=/backups"
  }
}

8.5 IoT and legacy systems

FileSet {
  Name = "IoT-And-Legacy"
  Include {
    Options { Signature = SHA256 }
    # Raspberry Pi IoT gateway
    Plugin = "podheitor-sftp: host=iot-gw.local user=pi keyfile=/etc/bacula/.ssh/iot_key path=/etc timeout=15"
    Plugin = "podheitor-sftp: host=iot-gw.local user=pi keyfile=/etc/bacula/.ssh/iot_key path=/opt/iot/config timeout=15"
    # AIX legacy server
    Plugin = "podheitor-sftp: host=aix-prod.local user=root keyfile=/etc/bacula/.ssh/legacy_key path=/etc"
    # Solaris server
    Plugin = "podheitor-sftp: host=solaris.local user=root keyfile=/etc/bacula/.ssh/legacy_key path=/export/home"
  }
}

9. Sizing & capacity planning

9.1 Estimating backup size

Use estimate in bconsole before committing to a schedule:

* estimate job=SFTP-Documents-Backup level=Full listing

This connects to the SFTP server, walks the directory tree applying all include/exclude filters, and reports the file count and estimated size — without transferring any data:

@sftp/fileserver.local:22/data/report.pdf              1,234,567
@sftp/fileserver.local:22/data/spreadsheet.xlsx           456,789
@sftp/fileserver.local:22/data/images/photo1.jpg        2,345,678
...
2,000 files found, estimated size: 1.2 GB

9.2 Storage requirements

Backup type Typical size relative to Full Retention recommendation
Full 100% 4 weeks (monthly rotation)
Differential 5–40% of Full Keep last 7 Differentials
Incremental 0.1–5% of Full per day Keep last 30 Incrementals

For network equipment configurations, the total corpus is typically under 10 MB per device, making Full daily backups cost-free in storage terms. For large NAS shares or cloud SFTP repositories, enable LZ4 compression (typically 30–60% size reduction for text and document files) and set appropriate Pool retention windows.

9.3 Network bandwidth planning

The plugin streams file data directly from the SFTP server to Bacula Storage. Network bandwidth requirements depend on:

  • Full backup size — the initial Full establishes the baseline network load
  • Daily change rate — Incremental size determines ongoing daily bandwidth
  • Transfer window — backups should complete before the next scheduled run

For constrained environments (WAN links, metered connections), schedule Full backups at off-peak hours and use Incremental for daily runs. The timeout= parameter should be set to at least 2× the expected time to transfer the largest single file over the available bandwidth.

9.4 Catalog impact

Each backed-up file generates one catalog record. For FileSets with hundreds of thousands of small files (e.g., web server /var/www), catalog growth can be significant. Apply exclude filters aggressively to skip ephemeral files (*.log, *.tmp, *.cache, node_modules) that do not need backup and would otherwise inflate the catalog.


10. Performance considerations

10.1 SSH connection overhead

Each plugin invocation (one per Plugin = line in the FileSet) establishes one SSH connection for the duration of the job. Key exchange and authentication typically complete in under 200 ms on a LAN. For WAN targets, the timeout= parameter controls the maximum wait for the initial connection; subsequent file-level operations use per-operation timeouts inherited from libssh2.

10.2 Directory walking speed

The backend performs a recursive SFTP directory walk using libssh2’s sftp_readdir. On a typical LAN, the walk processes approximately 1,000–5,000 entries per second depending on directory depth and network latency. For FileSets with deep directory trees, enable exclude patterns to prune subtrees early (e.g., exclude=node_modules,.git,__pycache__).

10.3 File transfer throughput

File data is transferred in 64 KB chunks over the SFTP channel. Effective throughput is bounded by the SSH encryption overhead and available network bandwidth. In LAN conditions (1 Gbps), expect 50–200 MB/s depending on CPU speed and file sizes. For small files (configuration files under 1 KB), throughput is IOPS-bound rather than bandwidth-bound; use include filters to limit transfers to necessary files only.

10.4 Multiple servers in parallel

Multiple Plugin = lines in a single FileSet are processed sequentially within one Bacula job. To back up multiple large servers in parallel, create separate Bacula Jobs each with their own FileSet and schedule them to run concurrently. Bacula’s Maximum Concurrent Jobs and Priority directives control parallelism.


11. Compatibility matrix

11.1 Bacula File Daemon host (where plugin runs)

Platform Architecture Status
RHEL / Rocky / Oracle Linux 8–9 x86_64, aarch64 Tested
Debian 12 (Bookworm) x86_64, aarch64 Tested
Ubuntu 22.04 / 24.04 LTS x86_64, aarch64 Expected
SUSE / openSUSE 15 x86_64 Expected
Arch Linux x86_64 PKGBUILD available
FreeBSD 13+ amd64 Untested (gmake required)

11.2 Remote SFTP targets (what can be backed up)

Category Examples Notes
Linux servers Any distribution with OpenSSH Full filesystem access
Windows servers Windows 10/11, Server 2019+ Native OpenSSH Server required
macOS macOS 10.15+ Enable Remote Login in System Settings
BSD FreeBSD, OpenBSD, NetBSD OpenSSH native
NAS appliances Synology DSM, QNAP QTS, TrueNAS Enable SFTP/SSH in NAS settings
Cisco Catalyst 9000 (IOS-XE), Nexus (NX-OS), ISR/ASR SSH/SFTP must be enabled
Juniper EX, QFX, SRX, MX (Junos) SSH/SFTP native in Junos
MikroTik All RouterOS devices SFTP native
Arista 7000, 7500 (EOS) SSH/SFTP native
Fortinet FortiGate (FortiOS) SFTP must be enabled on management interface
Palo Alto PA series (PAN-OS) SSH/SFTP via management plane
HPE Aruba CX switches, Instant AP SSH native
Ubiquiti UniFi, EdgeRouter SSH native
VyOS / OPNsense / pfSense Community/open firewalls SSH native
AWS Transfer Family Managed SFTP endpoints Key auth only
Azure Blob SFTP Azure Storage SFTP access Key auth; preview/GA feature
Hetzner Storage Box Native SFTP (port 23) Custom port= required
Raspberry Pi / IoT OpenWrt, DietPi, Raspberry Pi OS Lightweight; use timeout=15
Legacy Unix AIX, HP-UX, Solaris Any version with OpenSSH

11.3 Minimum Bacula version

Bacula Community 13.0.0+ is required for the Metaplugin framework on which the plugin is built. Earlier versions are not supported. The plugin has been tested against Bacula 13.x and 15.x.


12. Security

12.1 Authentication best practices

Practice Description
Use SSH keys, never passwords Passwords in Bacula config are stored in plaintext; SSH keys are never transmitted
Use ed25519 keys Modern algorithm; small key size; resistant to side-channel attacks
Dedicated backup key pair Generate a key exclusively for Bacula; never reuse personal or admin keys
Restrict key permissions chmod 640 on the private key file; owned by root:bacula
Dedicated SSH user on remote Create a backup user with only read access to backup paths
Read-only ACLs Backup user must not have write access — breach of the Bacula server cannot modify remote data
Enable verify_host=yes Always verify host keys in production; pre-populate known_hosts before first backup
Separate known_hosts file Use /etc/bacula/.ssh/known_hosts rather than root’s personal known_hosts
Firewall to port 22 only Allow only TCP 22 between the FD host and SFTP targets; block all other ports

12.2 Host key verification flow

verify_host=yes (DEFAULT — RECOMMENDED for production)
  ├── Load known_hosts file (if configured)
  ├── If host key NOT in known_hosts → ERROR: connection refused
  └── If host key in known_hosts → compare fingerprint → OK or ERROR

verify_host=no (LAB / testing ONLY)
  ├── AutoAddPolicy — accepts any host key on first connection
  └── WARNING: Vulnerable to Man-in-the-Middle attacks

12.3 Credential storage

SSH private key files referenced by keyfile= remain on the Bacula FD host filesystem and are never transmitted to remote servers — only the public key is used for authentication. The private key should be stored in a directory readable only by the Bacula FD process (typically root or the bacula user). If the Bacula Director configuration file is world-readable, use SSH keys rather than the password= parameter, which stores credentials in the Director config in plaintext.

12.4 Transport security

All data transfer occurs over the SSH encrypted channel. The SSH protocol (via libssh2) negotiates the cipher suite during key exchange; modern OpenSSH defaults use ChaCha20-Poly1305 or AES-256-GCM. For additional at-rest encryption in Bacula Storage, enable AES encryption in the FileSet Options block — this is independent of and additive to SSH transport encryption.


13. Monitoring

13.1 Standard Bacula job monitoring

The plugin integrates fully with Bacula’s built-in monitoring infrastructure. All job outcomes appear in the Bacula Messages log and are delivered to the configured Messages resource (email, syslog, file):

* list jobs
* show job=SFTP-Documents-Backup
* messages

13.2 File listing verification

After a completed backup, verify the expected files were cataloged:

* list files jobid=42
+----------------------------------------------------------+
| filename                                                 |
+----------------------------------------------------------+
| @sftp/fileserver.local:22/data/                          |
| @sftp/fileserver.local:22/data/report.pdf                |
| @sftp/fileserver.local:22/data/spreadsheet.xlsx          |
+----------------------------------------------------------+

13.3 File listing methods summary

Method When Command Requires backup?
Pre-backup estimate Before backup — live SFTP walk estimate job=... level=Full listing No
Post-backup catalog After backup — catalog query list files jobid=X Yes
Interactive restore tree During restore — browse from catalog restore → cd/ls Yes

13.4 Enabling debug logging

# RHEL / Oracle Linux — add to /etc/sysconfig/bacula-fd
PODHEITOR_DEBUG=1

# Debian / Ubuntu — add to /etc/default/bacula-fd
PODHEITOR_DEBUG=1

# Then restart FD
sudo systemctl restart bacula-fd

Backend logs (stderr) are captured by the FD and appear inline in the Bacula job messages. Each SFTP operation, mtime comparison decision, and include/exclude filter result is logged when PODHEITOR_DEBUG=1 is set.


14. Troubleshooting guide

14.1 Common issues and solutions

Symptom Likely cause Solution
Command plugin not found .so not in Plugin Directory Check /opt/bacula/plugins/podheitor-sftp-fd.so exists; restart FD
Unable to use backend Backend binary missing or not executable Check /opt/bacula/bin/podheitor-sftp-backend with chmod +x
SSH auth failed Invalid SSH key or wrong user Test: sudo ssh -i /path/key user@host
Permission denied on files SSH user lacks read access setfacl -R -m u:backup:rX /data on remote server
Backend hangs / timeout Network issue or firewall Test: nc -zv host 22; increase timeout=60
libssh2 not found Runtime library missing apt install libssh2-1 or dnf install libssh2
Host key verification failed Host key not in known_hosts Run ssh-keyscan host >> /etc/bacula/.ssh/known_hosts
Job runs but 0 files backed up Incorrect path or empty directory Verify remote path: sftp user@host; ls /data
0 files with include set Include pattern too restrictive Check patterns: include=*.pdf only matches .pdf files exactly
Incrementals backing up all files mtime timestamps incorrect on remote Verify clock sync (NTP) on remote host; consider Full schedule

14.2 Manual connectivity test sequence

# 1. TCP reachability
nc -zv server.local 22

# 2. SSH authentication test (run as root — same as FD)
sudo ssh -i /etc/bacula/.ssh/id_ed25519 -o BatchMode=yes backup@server.local echo OK

# 3. SFTP directory listing test
sudo sftp -i /etc/bacula/.ssh/id_ed25519 backup@server.local <<< "ls /data"

# 4. Host key test (confirm known_hosts entry)
ssh-keygen -F server.local -f /etc/bacula/.ssh/known_hosts

14.3 Verifying plugin load

sudo systemctl restart bacula-fd
echo "status client" | bconsole | grep -i podheitor
# Expected line: Plugin: podheitor-sftp-fd.so

14.4 Log locations

Component Log location
Bacula Director /opt/bacula/log/bacula.log
Bacula FD /opt/bacula/log/bacula.log
Backend debug output Stderr captured by FD, appears in job messages
Systemd FD journal journalctl -u bacula-fd

15. Use cases & deployment scenarios

15.1 Enterprise network infrastructure backup

A financial institution operates 120 network devices across three data centres: Cisco Catalyst 9300 core switches, Juniper SRX firewalls, MikroTik border routers, and Fortinet FortiGate perimeter firewalls. The network team historically ran ad-hoc TFTP/SCP transfers to a shared folder — no versioning, no retention, no restore process.

With PodHeitor SFTP Plugin, the entire network device fleet is integrated into the existing Bacula infrastructure. Each device type maps to a targeted FileSet with appropriate include patterns (*.conf, *.backup, *.rsc). Daily Full backups run at 02:00 with a 30-day retention pool. The operations team can restore any device configuration to any point in the last month via standard bconsole restore commands. Total backup corpus: under 50 MB per daily run across all 120 devices.

15.2 NAS consolidation backup

A manufacturing company operates four NAS appliances — two Synology DiskStation units and two QNAP NAS devices — distributed across factory floor and office environments. Each NAS stores CAD files, production reports, and scan archives. Installing Bacula FDs on these appliances is not supported by the vendor.

PodHeitor SFTP Plugin backs up all four NAS volumes from a single Bacula FD host located in the server room. Incremental backups run nightly; Full backups run every Sunday. The exclude=*.tmp,*.part filter eliminates incomplete uploads. Restoration of individual files is performed via bconsole’s interactive tree browse, which maps directly to the @sftp/nashost:22/volume1/path namespace.

15.3 Cloud VM agentless backup

A SaaS provider runs 40 application VMs across AWS EC2 and GCP Compute Engine. Installing and managing Bacula FD packages across this fleet, with IAM key rotation, security group rules, and agent update cadence, was a significant operational burden.

PodHeitor SFTP Plugin eliminates the per-VM agent. Each VM’s application data directory is backed up via a single SSH key pair shared across the fleet. Incremental backups run every 6 hours, capturing only changed application state. The AWS key pair is stored in /etc/bacula/.ssh/aws_key with 640 permissions. Total Bacula infrastructure: one FD host with the plugin, one Storage Daemon, one Director.

15.4 Legacy system integration

A healthcare provider runs critical patient scheduling software on a Solaris 10 server and an AIX 7.2 server that predate current Bacula FD packaging. The risk of touching these systems with new software installations is considered unacceptable by the compliance team.

PodHeitor SFTP Plugin backs up both servers without any installation: the plugin connects via their existing OpenSSH servers, backs up /export/home and /var/lib/app respectively, and integrates with the rest of the organization’s Bacula infrastructure. The legacy servers remain untouched. Restore is performed to a staging environment for verification before production recovery.

15.5 IoT configuration management

An energy company deploys 200 Raspberry Pi gateways across power substations. Each gateway runs custom monitoring software whose configuration files represent months of field calibration work. Loss of configuration requires manual recalibration at each site.

PodHeitor SFTP Plugin backs up /etc and /opt/monitor/config from each gateway daily. The timeout=15 parameter accommodates slower gateway SSH responses. Include filters limit backup scope to configuration files only, keeping each gateway’s backup under 5 MB. The entire 200-gateway fleet is backed up in under 30 minutes from a single Bacula FD host over the company’s MPLS WAN.


16. Comparison with other approaches

16.1 PodHeitor SFTP Plugin vs rsync + cron

Capability PodHeitor SFTP Plugin rsync + cron
Bacula catalog integration Yes — full catalog record No — external tool
Point-in-time restore Yes — any job in catalog No — last rsync only
Incremental backup Yes — mtime via Bacula levels Partial — rsync –link-dest
Retention policies Yes — Bacula Pool retention No — manual cleanup
Multiple storage targets Yes — Bacula Storage Daemon No — local disk only
Tape / cloud storage Yes — via Storage Daemon No
Monitoring and alerting Yes — Bacula Messages Manual (cron email)
Interactive restore tree Yes — bconsole restore No — manual FS browse
Multi-server in one job Yes — N Plugin= lines No — N separate scripts
SHA256 file integrity Yes — Bacula Options No
Encryption at rest Yes — AES via Bacula No — plaintext files

16.2 PodHeitor SFTP Plugin vs SSHFS mountpoints

Aspect PodHeitor SFTP Plugin SSHFS mount
Stale mount risk No — fresh connection per job Yes — mounts go stale and fail silently
Kernel dependency No — pure userspace (libssh2) Yes — FUSE kernel module required
Hung FD risk No — SFTP timeout per operation Yes — hung stat calls block the daemon
Network device support Yes — SSH native on switches/routers No — cannot mount network OS filesystems
Security surface Minimal — scoped to path= only Larger — full mount exposes FS to FD host
Firewall complexity Single port 22 Port 22 plus FUSE/NFS overhead
Credential management In Bacula config (encrypted at rest) In /etc/fstab or systemd mount files

16.3 PodHeitor SFTP Plugin vs Veeam / Commvault / NetBackup

Aspect PodHeitor SFTP Plugin + Bacula Commercial platforms
Network equipment backup Yes — native SSH/SFTP No native support; scripted workarounds
NAS agentless backup Yes — SSH only Limited; vendor-specific connectors
Legacy Unix (AIX, Solaris) Yes — SSH only Agent required; old agent versions
Cloud SFTP services Yes — direct SFTP connection Not supported natively
Licensing cost Fraction of commercial cost Per-socket or per-TB pricing
Open infrastructure Yes — Bacula Community Vendor lock-in

17. Roadmap

The following capabilities are planned or under investigation for future releases:

  • Pre/post-backup hooks. Execute arbitrary commands on the remote host via SSH before and after the SFTP walk (e.g., flush application write buffers, rotate log files).
  • Bandwidth throttle parameter. Limit SFTP transfer rate (KB/s) to avoid saturating WAN links during business hours — consistent with the throttle support in other PodHeitor plugins.
  • Parallel directory walkers. Walk multiple top-level subdirectories concurrently within a single plugin invocation, reducing total walk time for deep directory trees.
  • Prometheus metrics endpoint. Expose per-job transfer bytes, file counts, connection latency, and error counts on a configurable HTTP endpoint — aligned with the monitoring integration available in other PodHeitor plugins.
  • DEB package in upstream repository. Pre-built DEB packages for Debian 12 and Ubuntu 22.04/24.04 distributed via the PodHeitor APT repository.
  • Windows remote target improvements. Enhanced handling of Windows NTFS metadata (ACLs, alternate data streams) when backing up Windows OpenSSH Server targets.

18. Conclusion

The PodHeitor SFTP Backup Plugin for Bacula delivers what no standard open-source backup platform has offered before: a production-grade, catalog-integrated, agentless backup solution for any SSH-accessible device. From enterprise network switches to cloud SFTP services, from legacy AIX servers to IoT gateways, from NAS appliances to web hosting environments — if the device speaks SSH, it can now be part of the same Bacula infrastructure that protects the rest of the organisation.

The plugin’s Rust-native implementation, clean cdylib/backend separation, zero Python dependency (v2.0.0+), and full integration with Bacula’s native features (scheduling, retention, catalog, compression, encryption, monitoring) make it the most complete agentless SSH/SFTP backup integration available for Bacula Community Edition. The comparison with rsync scripts, SSHFS mounts, and commercial platforms makes clear that the plugin closes a real and previously unaddressed gap in the open-source backup ecosystem.

For organisations already running Bacula Community, the migration path is a single RPM or DEB install and three lines of FileSet configuration. For organisations evaluating backup platforms, the combination of Bacula Community and PodHeitor plugins delivers enterprise-grade coverage at a fraction of the cost of commercial alternatives — with no vendor lock-in and full control over data and infrastructure.


19. Contact information

Channel Details
Website https://podheitor.com
Email heitor@opentechs.lat
WhatsApp / Phone +1 786 726-1749 | +55 61 98268-4220
GitHub https://github.com/podheitor
LinkedIn https://linkedin.com/company/podheitor

For licensing inquiries, enterprise support agreements, or custom development requests, contact heitor@opentechs.lat directly. A written quote comparing the PodHeitor + Bacula stack against your current commercial backup platform renewal can be provided upon request — targeting at least 50% cost reduction.


20. Legal / copyright

PodHeitor SFTP Backup Plugin for Bacula — Version 2.0.0

Copyright (C) 2025–2026 PodHeitor International / Heitor Faria. All rights reserved.

Both shipped artifacts — podheitor-sftp-fd.so (Rust cdylib built on the in-house metaplugin-rs framework) and podheitor-sftp-backend (Rust binary using the ssh2 crate / libssh2) — are proprietary software distributed under LicenseRef-PodHeitor-Proprietary.

No AGPL-licensed code is statically linked into either artifact. Transitive Rust dependencies (ssh2, libssh2-sys, libc, parking_lot, openssl-sys, and others) are all permissively licensed (MIT / Apache-2.0) and are compatible with proprietary redistribution.

Use, redistribution, modification, reverse engineering, or commercial exploitation of either artifact requires a separate written license agreement with the copyright holder. Contact: heitor@opentechs.lat.

Bacula is a registered trademark of Bacula Systems SA. All other product names are trademarks or registered trademarks of their respective owners. PodHeitor International is not affiliated with Bacula Systems SA.

This whitepaper is provided for informational purposes. Specifications are subject to change without notice.

Disponível em: pt-brPortuguês (Portuguese (Brazil))enEnglishesEspañol (Spanish)

Leave a Reply