Restore from a backup

v3.21.0 moves the restore wizard into the unified backup admin surface at Admin → Backup & Restore → Restore (backup_admin.php?tab=restore). The legacy restore_web.php URL still resolves but new bookmarks should use the unified surface. The CLI restore (restore.php) is unchanged.

What ships in v3.21.x: restoring Database backups (engine-native dumps produced by the same engine) — through the web wizard for SQLite and through the CLI (restore.php + engine-native tools) for MySQL/PostgreSQL. Logical-backup restore ships in v3.22.0 (backup_overhaul §C). The Logical-backup paths described below ("Logical backup — restore through restore.php") document the v3.22.0 end-state; in v3.21.x there are no Logical backup files to restore because the backup runner only produces Database backups.

See Backups for backup creation, destinations, schedules, and encryption.


Contents


Overview

There are two restore paths:

PathWhereWhen to use
Web wizardbackup_admin.php?tab=restoreRoutine restores from a configured destination. Stage → dry-run → confirm → apply, all in the browser.
CLIrestore.php (and engine-native tools for Database backups)Disaster recovery, automation, large databases, or when the IPAM web UI is not available.

Both paths converge on the same backup_runs table — a row with status = 'success' and a triggered_by of manual (web) or cli (CLI) is written on completion.

The wizard requires admin role and enforces CSRF on every step. There is intentionally no one-click restore — the apply step requires typing RESTORE to confirm.


Logical vs Database restore

A restore behaves differently depending on what the backup file contains:

Backup typeWhat's in the fileRestore behaviour
LogicalEngine-neutral SQL representing the IPAM data model.Restorable to any of SQLite / MySQL / PostgreSQL. The wizard applies the SQL through BackupEngine::applyLogicalDump(), which uses the project's SQL splitter (a real lexer; not regex — see #806) to split statements safely across BEGIN/END blocks, dollar-quoted bodies, and embedded comments.
DatabaseEngine-faithful native dump (.sqlite / mysqldump output / pg_dump output).Restorable only to the same engine that produced it. SQLite Database backups apply through the wizard; MySQL and PostgreSQL Database backups currently require the CLI path until the cross-engine wizard support in #797 / F18 ships.

Rule of thumb: if you are restoring to the same engine the backup came from, either type works and Database is faster. If you are migrating engines, you need a Logical backup.


Cross-version policy

Same-or-newer target only. A backup from IPAM version X.Y.Z can be restored into an install running X.Y.Z or any later version. Restoring into an older version is not supported.

When the target install is newer than the backup, apply_migrations() runs after the SQL is applied to bring the schema up to date. The dry-run preview shows how many migrations will be applied so the operator can see the upgrade path before confirming.

Backup versionTarget versionAllowed?Behaviour
3.20.03.20.0yesSchema is already current; no migrations run.
3.18.03.21.0yesBackup is forward-migrated through every closure between v3.18 and v3.21 in natural-sort order.
3.21.03.20.0noWizard refuses; downgrade is not supported. CLI does not bypass this check.

If you must restore an older backup into a new install, the recommended path is: install the same version the backup was taken on, restore there, then run the IPAM upgrade procedure as documented in Upgrading.


Prerequisites

  • Admin role; readonly users cannot reach the wizard or POST handlers.
  • At least one backup destination configured with accessible files (or a file you upload directly).
  • The right key for the archive's format — see backups.md → Disaster recovery for the full "can I recover this?" table:
    • .enc (legacy IPAMBKP1/IPAMBKP2): app_secret must be set in config.php and match the value in place when the backup was taken.
    • .ipambkp3 stored mode: the backup_vault_key must be available — i.e. you're restoring on the same install (it unwraps the DB-stored key via config.php's bootstrap_key), or you've pasted a previously-exported vault key into the install.
    • .ipambkp3 transitory mode: the wizard prompts for the passphrase that was used when the archive was encrypted. (v3.28.0 has no in-app creator for this format; you'd only see one if it was produced by another tool or a future release — see parked features.)
    • .ipambkl1.gz / .ipambku1 / .sql.gz / .sqlite: no key needed.
  • For MySQL or PostgreSQL Database backups: the engine-native CLI tool (mysql / psql) must be on the same host as the IPAM install.
  • For very large databases: enough free disk in data/tmp/ to stage the file before applying.

Web restore — wizard walkthrough

Open Admin → Backup & Restore → Restore (backup_admin.php?tab=restore).

Step 1: Stage a backup file

Select a destination from the dropdown, then a filename from the list of files on that destination. Click Stage.

What happens server-side:

  1. IPAM connects to the destination and downloads the selected file into data/tmp/ under a random hex filename (e.g. data/tmp/restore_a3f7c2b1.sqlite).
  2. A signed staging token (HMAC-SHA256 over the temp path + session ID) is stored in the session. The token is required for subsequent steps to prevent cross-request tampering.
  3. If the filename ends with .enc, the file is decrypted before the token is issued. A missing or wrong app_secret fails at this step with a clear error.
  4. A SHA-256 hash of the staged file is computed and stored in the session for comparison at apply time.

The audit action db.restore_stage is written. Staged files are cleaned up automatically by tmp_cleanup.php (run via cron.php) after one hour, or immediately on a successful restore or explicit cancellation.

Step 2: Dry-run preview

After staging, the wizard automatically runs a dry-run. This reads from the staged file and the live database — no data is changed.

The preview shows:

Table diff

TableCurrent rowsBackup rowsDelta
subnets4238−4
addresses1,2041,187−17
users550

Negative deltas show rows that would be lost; positive deltas show rows that would be gained.

Schema diff

  • Schema version in the backup (from schema_migrations max version).
  • Current schema version.
  • Number of pending migrations that would be applied after the SQL.

If the backup is from a newer version than the current install, the wizard shows a hard error and blocks Apply. See Cross-version policy.

Warnings (informational, do not block):

  • Tables in the backup but not in the current schema (may indicate a plugin or custom migration).
  • Row count deltas above 50% in any table.
  • Backup file age over 24 hours.

The audit action db.restore_dryrun is written.

Step 3: Live restore

Click Proceed to restore. A confirmation gate appears.

Type RESTORE exactly (case-sensitive) into the text field to enable the Apply button. This matches the GitHub repository-deletion pattern and prevents accidental clicks.

When confirmed:

  1. Pre-flight — the staged file's SHA-256 is re-verified against the value saved at stage time. A mismatch (file modified between stage and apply) aborts the restore.
  2. Begin exclusive transaction.
  3. Disable foreign keysPRAGMA foreign_keys = OFF is set outside the transaction boundary before BEGIN, as required by SQLite's FK pragma rules.
  4. Apply the dump — the Logical SQL splitter (real lexer) tokenises the staged file and applies each statement; for a Database SQLite backup the file is restored as the new database directly.
  5. Re-enable foreign keysPRAGMA foreign_keys = ON is restored unconditionally in all exit paths.
  6. Run apply_migrations() — pending migrations bring the schema to current.
  7. Commit — on success, the staged temp file is deleted and the session token cleared.

If any step fails, the transaction rolls back and the database is left unchanged. The staged file remains in data/tmp/ so the operator can re-attempt without re-downloading.

A backup_runs row is written with triggered_by = 'manual' and status = 'success' or 'failed'. The audit action db.restore is written on success.

After a successful restore, the page shows a success banner and redirects to the dashboard after five seconds.

Manual upload-and-restore

A future release (#797 / F13, v3.24) will let an operator upload a backup file directly from the browser instead of staging from a destination — useful when the original destination is no longer reachable. Until then, copy the file to a Local destination's directory (or onto the IPAM server) and stage from there.


CLI restore

The CLI restore is the only path for MySQL and PostgreSQL Database backups in v3.21.0, and is also the recommended path for very large SQLite databases or fully-automated disaster recovery.

php /path/to/Simple-PHP-IPAM/restore.php --from=<path> [--dry-run] [--force]
FlagMeaning
--from=<path>Path to the backup file (Logical SQL or SQLite Database). Required.
--dry-runValidate and report the restore plan without writing anything.
--forceAllow overwriting a non-empty target database. Without this, restore aborts if the target is not empty.

Web requests to restore.php always return HTTP 403 Forbidden — the script is CLI-only.

SQLite

Native dump (Database backup) — replace the live database file. Stop the web server first to release any open file handles:

sudo systemctl stop apache2          # or nginx / php-fpm
cp data/ipam.sqlite data/ipam.sqlite.before-restore
gunzip < /path/to/ipam-20260501.sqlite.gz > data/ipam.sqlite
chown www-data:www-data data/ipam.sqlite
sudo systemctl start apache2

Logical backup — restore through restore.php:

php Simple-PHP-IPAM/restore.php --from=/path/to/ipam-20260501.logical.sql --force

MySQL

Database backup (mysqldump output):

mysql -h <host> -u <user> -p <database> < /path/to/ipam-20260501.mysql.sql

If the dump is encrypted (.enc), decrypt it first using the IPAM helper:

php Simple-PHP-IPAM/tools/backup-decrypt.php /path/to/ipam-20260501.mysql.sql.enc > /tmp/ipam.sql
mysql -h <host> -u <user> -p <database> < /tmp/ipam.sql
shred -u /tmp/ipam.sql

Logical backup — restore through restore.php:

php Simple-PHP-IPAM/restore.php --from=/path/to/ipam-20260501.logical.sql --force

PostgreSQL

Database backup (pg_dump output, plain SQL):

psql -h <host> -U <user> -d <database> -f /path/to/ipam-20260501.pgsql.sql

If the dump is in custom format (.dump), use pg_restore:

pg_restore -h <host> -U <user> -d <database> --clean --if-exists /path/to/ipam-20260501.pgsql.dump

Logical backup — restore through restore.php:

php Simple-PHP-IPAM/restore.php --from=/path/to/ipam-20260501.logical.sql --force

After any CLI restore, run apply_migrations() once by visiting any IPAM page in the browser as an admin — the bootstrap path applies pending migrations automatically.


Audit log

Every restore step writes an audit entry:

ActionWhen
db.restore_stageA backup file is successfully staged from a destination into data/tmp/. Detail includes destination ID and the original filename.
db.restore_dryrunA dry-run preview is generated from the staged file. Detail includes backup schema version and pending migration count.
db.restoreA live restore apply completes successfully. Detail includes backup filename, schema version before and after, and row counts for key tables.

Failed restores are recorded in backup_runs with status = 'failed' and a detail string. No db.restore audit row is written for failed attempts (only on success); the failure row in History is the system of record for the failure.


Engine support

EngineDatabase backup restore (web)Database backup restore (CLI)Logical backup restore (v3.22.0)
SQLiteyes (web + CLI)yesv3.22.0 (web + CLI)
MySQLnot in v3.21.x — use CLIyesv3.22.0 (web + CLI)
PostgreSQLnot in v3.21.x — use CLIyesv3.22.0 (web + CLI)

The Logical-backup restore column lists the v3.22.0 end-state. In v3.21.x the runner doesn't exist, so there are no Logical-backup files to restore — every existing backup is a Database backup. Cross-engine Database-backup restore in the web wizard is tracked in #797 / F18 for v3.24.0.


Recovery scenarios

Encrypted backup, app_secret not set

Cannot decrypt backup: app_secret is not set in config.php.

Fix: add app_secret to config.php using the value that was in place when the backup was created (legacy .enc / IPAMBKP1/IPAMBKP2 archives only). If you do not have the original key, the backup cannot be recovered. For an offline decrypt without a running install, use tools/decrypt-backup.php --app-secret <hex>.

Encrypted backup (.ipambkp3 stored mode), vault key unavailable

Cannot decrypt backup: backup vault key not available for this archive.

The archive was encrypted with a backup_vault_key this install can't reproduce. Recover by either: (a) restoring on the original install (which still has config.php's bootstrap_key + the DB-stored wrapped key); (b) pasting a previously-exported copy of that vault key into Admin → Backups → Destinations → "Set vault key", then staging again; or (c) decrypting offline with tools/decrypt-backup.php --vault-key <base64>. If no copy of the vault key survives anywhere, the archive cannot be recovered — see backups.md → Disaster recovery for how to avoid this next time.

Checksum mismatch at apply time

Staged file integrity check failed. The file may have been modified.
Please stage the backup again.

The file in data/tmp/ was modified between Stage and Apply. Stage the backup again from the destination.

Dry-run reports a newer schema in the backup

The wizard refuses to proceed. Restoring into an older install is not supported. Either upgrade the install first, or set up a temporary install of the same version the backup was taken on, restore there, and then upgrade.

Partial restore — transaction rollback on apply failure

If the SQL apply or the migration step fails mid-way, the transaction rolls back. The database is left in its pre-restore state. The error is shown on the wizard page and recorded in backup_runs. Check PHP's error log for the full exception. The internal runbook lists common causes and fixes.

Post-restore verification

After any restore — wizard or CLI — verify:

  1. Sign in as the original admin (credentials in the backup).
  2. Visit Admin → Health and confirm no red rows.
  3. Spot-check 2–3 known subnets and addresses.
  4. Confirm Admin → Audit log shows the db.restore row at the top.

Limits

  • Live-restore round-trip is not in the default Playwright suite. Enable it by setting IPAM_PW_RESTORE_LIVE=1 before running the suite. Without this flag the suite tests staging and dry-run but skips live apply.
  • File size — very large backups may hit PHP memory_limit or max_execution_time for the Logical apply path. The Database SQLite path is bounded by disk I/O only. For Logical SQL larger than the engine's reasonable transaction size, prefer the CLI.
  • Concurrent restore is not supported. If a restore is in progress, do not start a second one in another tab — both will race on the staging directory and the live transaction.