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
- Executive summary
- Introduction & market context
- Architecture overview
- Backup modes deep dive
- Feature matrix
- Installation guide
- Configuration reference
- FileSet examples
- Sizing & capacity planning
- Performance considerations
- Compatibility matrix
- Security
- Monitoring
- Troubleshooting guide
- Use cases & deployment scenarios
- Comparison with other approaches
- Roadmap
- Conclusion
- Contact information
- 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:
- 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.
- Upgradability. The backend binary can be replaced without restarting
bacula-fd, since only the cdylib touches the Bacula plugin ABI. - 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-fdrunning - Plugin Directory configured in
bacula-fd.conf(typically/opt/bacula/plugins) - Runtime libraries:
libssh2andopenssl(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 |
| heitor@opentechs.lat | |
| WhatsApp / Phone | +1 786 726-1749 | +55 61 98268-4220 |
| GitHub | https://github.com/podheitor |
| 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:
Português (Portuguese (Brazil))
English
Español (Spanish)