Upgrading

Contents

  • Overview
  • Upgrade steps
  • upgrade.sh options
  • Environment variables
  • What the backup looks like
  • CLI utilities
  • Version-specific upgrade notes
    • v3.35.0 — refactor wave 4 (lib polish + admin refactor), TOTP backup-code O(1) lookup, @-suppression CI linter, init.php decomposition, scan consolidation (no breaking changes)
    • v3.33.0 — refactor wave 2 (api.php / import_csv / migrations), ADR-003 global $config sweep complete; API list-response flat shape soft-deprecated (no breaking changes)
    • v3.32.0 — code-quality, hardening, and dependency-maintenance release; PHPMailer 7.x, server-side site-hierarchy enforcement (no breaking changes)
    • v3.31.0 — encrypt-at-rest for settings secrets (IPAMSEC1 envelopes), webhook-secret consolidation, lib.php refactor wave 1 finish (no breaking changes)
    • v3.30.0 — per-user theme preference table with backfill, settings type system moved to PHP, lib.php decomposed into modules (no breaking changes)
    • v3.25.0 — IPAMBKL1 backend surfaced via picker UI, retention re-homed to destination, U-series UX polish (no breaking changes)
    • v3.24.0 — IPAMBKP3 three-mode encryption format, manual upload-and-restore, standalone decrypt-backup CLI (no breaking changes)
    • v3.23.0 — IPAMBKL1 engine-agnostic backups, per-schedule notification overrides, legacy backup config deprecation + auto-migration (no breaking changes)
    • v3.20.0 — Backup destinations UX polish: inline Edit drawers, Run-now per destination, frequency-aware schedule fields, auto-Test on Save, TZ-correct timestamps, notify wiring, S3 redaction scope fix (no breaking changes)
    • v3.19.1Hotfix: S3 destinations actually work (SigV4 fix), MySQL/PG cloud backups unblocked, S3 download body-leak fixed (no breaking changes)
    • v3.18.0 — Per-toggle Settings save, backup/restore polish, contacts docs, pgsql test flake fix (no breaking changes)
    • v3.17.0 — Backup destinations, schedules, GFS retention, encryption, web restore wizard (no breaking changes)
    • v3.16.0 — Admin TOTP toggle, preferred MFA method, unified MFA card, Settings tabs, portable case-insensitive search (no breaking changes)
    • v3.15.2 — Bug fixes: passkey registration with password managers, MFA method choice, stale-session redirect (no breaking changes)
    • v3.15.1 — Bug fixes: post-login redirect, email charset, banner false positive (no breaking changes)
    • v3.15.0 — WebAuthn / Passkey 2FA (no breaking changes)
    • v3.14.0 — Email OTP 2FA, MFA enforcement (no breaking changes)
    • v3.13.0 — Settings cascade migration, UI/UX polish (no breaking changes)
    • v3.12.0 — Dashboard responsive fix, health alert indicators, webhooks cleanup (no breaking changes)
    • v3.11.0 — UI polish (no breaking changes)
    • v3.10.0 — IPAM_VERSION load-order fix, webhook test_fire (no breaking changes)
    • v3.9.0 — Site filter strip, cascading address filter, DB admin consolidation (no breaking changes)
    • v3.8.1 — Dashboard bug fixes, documentation refresh (no breaking changes)
    • v3.8.0 — Sidebar navigation, command palette, uPlot dashboard, SVG icons, Fira Sans (no breaking changes)
    • v3.7.0 — Backup/restore, health dashboard, audit retention, test coverage (no breaking changes)
    • v3.6.0 — TOTP 2FA, per-API-key rate limiting, session hardening (no breaking changes)
    • v3.5.0 — Custom fields (no breaking changes)
    • v3.4.0 — DHCP config export (no breaking changes)
    • v3.3.0 — webhooks, login history, brand polish (no breaking changes)
    • v3.2.0 — devices, password recovery, OpenAPI spec (no breaking changes)
    • v3.0.0breaking changes, config.php stub, driver promotion

Overview

Upgrades are handled by upgrade.sh, included in every release bundle. The script:

  • Creates a timestamped backup of your current install (including the SQLite DB and WAL files)
  • Syncs new application files into the target directory using rsync
  • Preserves config.php and the entire data/ directory
  • Fixes file permissions after the sync
  • Runs database migrations automatically (if the php CLI is available)
  • Removes upgrade artefacts from the webroot

If the migration step fails, upgrade.sh automatically restores from the backup and exits with code 10.


Upgrade steps

# 1. Download and extract the new release bundle
tar -xzf ipam-0.11.tar.gz -C /tmp/

# 2. Run upgrade.sh, pointing it at your current install directory
bash /tmp/Simple-PHP-IPAM/upgrade.sh /var/www/ipam

The script confirms the version transition (e.g. 0.10 → 0.11) before making any changes.

To skip the confirmation prompt (e.g. in a CI/CD pipeline):

bash /tmp/Simple-PHP-IPAM/upgrade.sh --yes /var/www/ipam

upgrade.sh options

FlagDescription
--yesNon-interactive — skip confirmation prompts
--forceAllow reinstalling the same version
--force-downgradeAllow downgrading (not recommended — may break the DB schema)

Environment variables

VariableDefaultDescription
CLEANUP_ARTIFACTS1Remove build/upgrade artefacts from the target webroot after success
REMOVE_UPGRADE_SH_FROM_TARGET1Also remove upgrade.sh from the target webroot

Example — keep upgrade.sh in place after the upgrade:

REMOVE_UPGRADE_SH_FROM_TARGET=0 bash /tmp/Simple-PHP-IPAM/upgrade.sh --yes /var/www/ipam

What the backup looks like

/var/www/ipam.backup.20260326-143000/   ← timestamped copy of entire install
    data/
        ipam.sqlite
        ipam.sqlite-wal   ← if present (WAL mode journal)
        ipam.sqlite-shm   ← if present
    config.php
    ... (all other app files)

The backup is left in place after a successful upgrade. You can remove it manually once you have verified the new version is working correctly.


Version-specific upgrade notes

v3.36.0

  • No breaking changes. No schema migrations. No new config keys. No operator action required. v3.36.0 is "UX foundation — design system" — closes 13 of 17 issues in milestone #59. Upgrade with upgrade.sh as normal.
  • CSS browser-support floor raised. The dark-mode token consolidation uses the CSS Color Module Level 4 light-dark() function, which requires Chrome 123+ (Mar 2024), Safari 17.5 (May 2024), or Firefox 120 (Nov 2023). Users on older browsers will see the light-theme tokens regardless of OS preference until they upgrade. Real-world impact is small: ≈98% of global browser traffic per caniuse, and the project already required :has() and :focus-visible which have similar floors.
  • Mobile body font-size bumped to 16px (@media (max-width: 768px) only; desktop body stays 14px). Prevents iOS Safari auto-zooming on form-input focus and improves readability. Some mobile screenshots will look slightly different — paragraph text grows a hair where it inherits from body without an explicit font-size. Custom CSS overriding body { font-size: ... } continues to win.
  • Type scale collapsed from 14 distinct sizes to 6 (12 / 14 / 16 / 20 / 24 / 32 px). Most pages render visually identically because most call sites already used one of the canonical sizes; the remaining sizes (e.g. 11.9, 14.4, 18.4 px) round to the nearest of the 6. The legacy --font-size-{2xs..8xl} and old --text-* tokens are kept as deprecation aliases so custom CSS continues to resolve.
  • Color tokens unchanged at the value level. IPAM's slightly cool-toned neutrals stay as brand identity (per ADR-007-3 conservative scope). Only the structural layout of the dark-mode overrides changed — the rendered colors are identical.
  • Re-enables vr-dashboard-* Playwright projects on CI. Operators forking the codebase and running CI should not see new failures; baselines for both *-darwin.png and *-linux.png are committed. If you run Playwright locally, see testing/playwright/update-vr-baselines.sh for the regeneration recipe.

v3.35.0

  • One schema migration: 3.35.0-totp-backup-lookup-key. Adds totp_backup_codes.lookup_key VARCHAR(16) NULL + composite index idx_totp_backup_lookup (user_id, lookup_key). Idempotent; no manual action required. Existing rows keep lookup_key = NULL; the verifier falls back to a slow scan for them (one release).
  • New composer lint:at CI gate. Refuses un-justified @-suppressions in PHP. Operators forking the codebase: add inline comments or replace with explicit handling.
  • New lib/bootstrap_*.php modules. init.php is now a thin top-level chain; module boot order is preserved.
  • lib/scan.php consolidation. scan_run.php and cron.php now delegate scan execution to a shared module; audit-log shape unchanged.
  • Browser tag/contact attach/detach now audit-logged. Operators who attached tags via the UI previously left no audit trail; that gap is closed. Audit-log volume on browser POSTs increases moderately.
  • No breaking changes. No new config keys. No deprecated APIs removed.
  • Known limitation: app_secret rotation invalidates totp_backup_codes.lookup_key. After rotating app_secret, run UPDATE totp_backup_codes SET lookup_key = NULL so the verifier falls back to slow-scan until users regenerate codes. Tracked as #1300.

v3.33.0

  • No breaking changes. No schema migrations. No new config keys. No operator action required. v3.33.0 is "Refactor Wave 2" — a pure internal refactor release (milestone #57) that decomposes api.php, rewrites import_csv.php, consolidates migrations.php, completes the ADR-003 global $config sweep, and adds test coverage. Upgrade with upgrade.sh as normal.
  • API list-response flat shape is soft-deprecated (integrations only). List endpoints now emit a Deprecation: true response header (RFC 8594) when returning the legacy flat shape (items under a resource-named key alongside top-level total/page/limit). The {data, meta} envelope — requested with ?envelope=1 — is the canonical shape. The flat shape still works unchanged, but API integrations using list endpoints should move to ?envelope=1. The flat shape will be removed in v4.0.0 (tracked as issue #1252). See API → List response shape.
  • No config-format changes. Existing config.php files keep working unchanged.

v3.32.0

  • No breaking changes. No schema migrations. No operator action required. v3.32.0 is a code-quality, hardening, and dependency-maintenance release that closes milestone #84. Upgrade with upgrade.sh as normal.
  • PHPMailer upgraded to 7.x (transparent). phpmailer/phpmailer was updated from 6.12.0 to 7.1.1. The 7.0 release is functionally equivalent to 6.11.1 for standard SMTP delivery. No configuration changes are required — existing SMTP settings keep working unchanged.
  • Site-hierarchy depth and cycle enforcement is now server-side. The 2-level maximum depth and cycle rejection for site parent assignments are now enforced at the API layer (ipam_site_validate_parent()) in addition to the UI. Operators are not affected; no configuration change is needed.
  • No config-format changes. Existing config.php files keep working unchanged.

v3.31.0

  • No manual action required. v3.31.0 ships two automatic, idempotent migrations that run on first load after upgrade. They re-encrypt existing plaintext settings-secret rows and legacy $2W$ webhook-secret rows in place. Re-running them is harmless — already-encrypted rows are skipped.
  • Encrypt-at-rest for settings secrets (new). The four sensitive settings — oidc.client_secret, smtp.auth_pass, recaptcha_enterprise.api_key, and login_protection.secret_key — plus webhook secrets are now stored encrypted in the settings table as IPAMSEC1 envelopes (libsodium XSalsa20-Poly1305) instead of plaintext. Encryption and decryption are transparent — the settings behave exactly as before in the admin UI.
  • ⚠️ Back up config.php. app_secret (in config.php) is now the encryption root for all settings-table secrets. If config.php is lost or app_secret is regenerated, every encrypted secret becomes unrecoverable — a database backup alone is no longer sufficient. Ensure config.php is backed up off-site. See Backups → Disaster recovery.
  • No breaking changes. No navigation or URL changes. Existing config.php files keep working unchanged.

v3.30.0

  • No operator action required. v3.30.0 ships one schema migration set (three migrations) that runs automatically on first load after upgrade.
  • Per-user theme preference (new). Theme (light/dark) is now stored per user account in a new user_preferences table instead of a single shared column. The 3.30.0-user-preferences migration backfills every existing user's previous theme choice into the new table, so no one loses their setting. Nothing to do — users keep the theme they had.
  • Settings type system moved to PHP (internal). The settings.type database column has been dropped; setting validation is now driven by a PHP logical-type registry. This is an internal refactor with no operator impact — settings behave exactly as before.
  • Large internal refactor (developers only). lib.php is decomposed into 12 lib/*.php modules. No effect on operators; no config-format changes; existing config.php files keep working unchanged.

v3.29.0

  • No schema changes. No new pages. No operator action required. Closes milestone #80 (test infrastructure / CI gate / unit-test coverage cluster from the v3.28.0 code-quality review).
  • CI behaviour change (developers only): GitHub Actions PHPStan / PHPUnit / PHPCS / composer-audit steps now all run with if: always(). A failing PHPStan run no longer short-circuits PHPUnit and PHPCS — expect to see all failures on a single push instead of the first one.
  • Dev gate adds composer-lock-drift detection. If your local vendor/ is behind composer.lock the gate now fails fast (testing/scripts/check-composer-lock-drift.sh). Resolution: composer install. The prior composer status-based check produced empty output and missed real drift.
  • settings.php per-key save handler removed. Operators don't notice — the group-form Save button works as before. Internal: every Playwright fixture that previously POSTed to the per-key path has been migrated to the group form. A regression test (SettingsToggleConsistencyTest::testPerKeyHandlerIsGone) fails the build if the handler is reintroduced.
  • No config-format changes. Existing config.php files keep working unchanged.

v3.28.2

  • Install-key lifecycle: app_secret is now lazily auto-generated on first need (first TOTP enrollment, first restore staging). If you previously left app_secret blank, you'll see a one-time admin banner the first time the value is generated. Back up config.php immediately after. Auto-gen requires config.php to be writable; otherwise the page surfaces an actionable error.
  • Install keys panel: A new "Install keys" card at the top of Settings shows the live state of app_secret, bootstrap_key, and backup_vault_key.
  • Bootstrap key announcement: bootstrap_key auto-generation (already lazy since v3.26.0) now writes an audit-log row and surfaces the same banner. Existing installs whose bootstrap_key was auto-generated silently in an earlier release will NOT retroactively see the banner — it only fires on auto-gen events that happen on v3.28.2 or later.
  • No schema changes. No database migration is required.
  • No config-format changes. Existing config.php files keep working unchanged.

v3.28.1

DR + security overflow point release. No schema changes. No new pages. No operator action required for any of the security or passC fixes.

Engine-native restore now wipes target schema before replay. restore.php's mysql and pgsql branches drop all user tables (mysql, BASE TABLE filtered) / drop the public schema (pgsql) before piping the dump in. This fixes a long-standing bug where restoring a database-type dump onto a populated install duplicated rows. Run as --yes confirmation as before — no new flag.

Postgres extension caveat. DROP SCHEMA public CASCADE removes any extensions installed into the public schema (e.g. pgcrypto, pg_trgm, citext, uuid-ossp). A pg_dump taken with defaults re-emits the CREATE EXTENSION statements and the restore puts them back. A --data-only dump (or any dump that omits extensions) will leave the restored DB without them. If you use Postgres extensions outside what the IPAM schema migrations create, dump with extensions included.

Mid-restore failure semantics. A failed pipe between wipe and replay leaves the target DB empty. Recovery path is "re-run with a known-good dump" — restore.php does not snapshot the pre-wipe state on the mysql/pgsql paths (only the SQLite path makes a safety copy).

restore.php now honours db_dsn. mysql/pgsql restores parse the PDO-style db_dsn connection string (host/port/dbname) the rest of the app already uses. Legacy installs that set discrete db_host/db_port/db_name keys continue to work. Limitation: restore.php cannot pipe mysql/psql through a Unix-domain socket; a db_dsn containing unix_socket=... aborts with a clear error pointing the operator at the discrete TCP keys. (#1177)

psql fail-fast. restore.php invokes psql with -v ON_ERROR_STOP=1 so a mid-restore error aborts cleanly instead of ploughing through subsequent statements onto a partially-populated DB. (#1177)

Other security and bug fixes:

  • Webhook test-fire audit details now record host only (no full URL with query string / fragment / XSS-shaped path). (#1152, S-001)
  • Backup restore upload staging files now chmod 0640 after move_uploaded_file() so other webserver users on shared hosts can't read staged payloads. (#1154, S-004)
  • API sync-scan rejects IPv6 subnets with HTTP 400 (use php scan_run.php for IPv6 — ipam_scan_subnet() is IPv4-only). (#1160, F-S2-01)
  • Scheduled scans (cron) now emit a scan.run audit row per subnet, matching api_scan_run and scan_run.php with a (cron) tag. (#1161, F-S2-04)
  • ipam_mark_stale_addresses threshold defensively clamped to [1, 50] so a mis-edited tenant setting can't produce nonsense LIMIT bounds. (#1162, F-S2-05)
  • Kea JSON and ISC dhcpd reservation hostnames now agree — both routed through ipam_dhcp_normalize_hostname() (single-label, RFC 1123 letter-digit-hyphen, leading alpha, max 63 chars). (#1163, F-S6-01)

Internal cleanup:

  • Removed dead ipam_backup_vault_key_or_init() (139 lines + 4 unit tests). No production caller since v3.28.0/#1164. ipam_config_inject_or_replace_key() is retained — still used by ipam_bootstrap_key() in lib/vault.php. (#1176)
  • Regression test pins that ipam_webhook_retry_pending() does not decrypt secrets (decryption belongs only in ipam_webhook_deliver()). (#1155, S-007)

v3.28.0

DR + security stabilization release. One schema migration (3.28.0-state-tables, runs automatically — adds two small internal tables, no operator action) and one behavior change worth knowing about: the legacy app_secret-based backup-encryption write path was removed.

Migration off app_secret backup encryption (v4.0.0 cold-break preview)

What changed in v3.28.0: the backup orchestrator no longer falls back to encrypting scheduled backups with the app_secret value in config.php. An encrypted backup now requires either:

  • a backup vault key — Stored mode, configured under Admin → Backups → Destinations (this has been the recommended path since v3.26.0; archives are IPAMBKP3 stored-mode), or

  • unencrypted (Local destinations only) — switch the destination's encryption mode under Admin → Backups → Destinations if you don't want destination-side encryption.

    Transitory mode (passphrase-encrypted IPAMBKP3) is not a v3.28.0 alternative for satisfying preflight. The Restore wizard accepts transitory archives produced elsewhere, but the orchestrator has no write path for them — see docs/internal/parked-features.md. Configure a vault key (Stored mode) or switch the destination to unencrypted.

If an install still has only app_secret set and no vault key configured, encrypted backups will fail preflight with a message pointing at this section — they'll show up as status=failed runs in the History tab with a synthetic (preflight-failed-…) name plus a backup.preflight_failed audit row. Fix: configure the backup vault key (Destinations tab → "Encryption key (Stored mode)"). Or, if you don't need encrypted destination backups, set the destination's encryption mode to unencrypted.

If a destination shows "Stored" encryption mode but you only ever used app_secret-style encryption: that's the old-model carry-over. Pre-v3.24.0 a destination just had an encrypt on/off flag, and "on" meant "encrypt with app_secret". The v3.24.0/v3.25.0 redesign replaced that flag with the stored | transitory | unencrypted enum, and the migration had to map an old encrypt=1 destination onto the only "encrypted" choice it has — stored (= backup-vault-key). That mapping silently changes which key is used. Combined with the now-fixed v3.27.x "config.php cleanup needed — remove bootstrap_key" banner (which, if you complied, orphaned any auto-generated vault key), some installs end up with an unreadable backup_vault_key envelope they never deliberately set. What to do: decide whether you actually want vault-key encryption for that destination. If yes — recover the original bootstrap_key (best; see Vault recovery (unreadable envelope)) or, if it's gone, clear the orphaned backup_vault_key setting and set a fresh one. If no — just change the destination's encryption mode to unencrypted on the Destinations tab. Either way, your app_secret-encrypted archives (IPAMBKP1/IPAMBKP2 .enc) are unaffected — they decrypt with app_secret, independent of the vault key. (As of v3.28.0 the orchestrator no longer auto-generates a vault key into config.php on first stored-mode backup — that path was removed; it now fails preflight with the message above instead.)

Reading legacy archives is unaffected in v3.28.0. The in-app Restore wizard still decrypts every legacy format — IPAMBKP1, IPAMBKP2 (the two app_secret-encrypted formats), IPAMBKP3 (stored + transitory), IPAMBKU1, bare .sql.gz, bare .ipambkl1.gz — for the entire v3.x line. You do not lose access to old backups by upgrading to v3.28.0.

What v4.0.0 will do (cold break — plan ahead now): v4.0.0 removes the in-app reader for IPAMBKP1 / IPAMBKP2 / SQLite-binary dumps / bare .sql.gz. After that upgrade, the Restore wizard accepts only IPAMBKL1-inside-IPAMBKP3 (or unencrypted IPAMBKL1). The only in-product way to recover plaintext from a pre-v4 legacy archive becomes the standalone CLI tool tools/decrypt-backup.php (ships in the release tarball; runs without a DB or webserver). So, before you reach v4.0.0:

  1. Inventory your retained archives. Any IPAMBKP1/IPAMBKP2 (.enc) archive you intend to keep restorable in-app needs to be migrated.
  2. Migrate them, one of two ways:
    • Re-encrypt under the vault key (recommended): decrypt the legacy archive (in-app Restore → stage → or tools/decrypt-backup.php --in old.enc --out plain.sqlite --app-secret <hex>), then take a fresh backup to a Stored-mode destination so it's written as IPAMBKP3. Drop the old .enc copy once verified.
    • Or keep the escape hatch handy: retain a copy of app_secret (the value from config.php) stored separately from the archive, plus a copy of tools/decrypt-backup.php. After v4.0.0 you'd run php tools/decrypt-backup.php --in old.enc --out plain.sqlite --app-secret <hex> to recover the plaintext, then re-import it.
  3. Transitory (passphrase) archives — same logic: the in-app reader keeps them through v3.x; for v4.0.0, either re-key them or keep the passphrase + the decrypt tool.

tools/decrypt-backup.php auto-detects the archive format from its magic and takes exactly one credential (--app-secret for IPAMBKP1/2, --vault-key for IPAMBKP3 stored, --passphrase / $IPAM_BACKUP_PASSPHRASE for IPAMBKP3 transitory, none for IPAMBKU1 / bare archives). Run php tools/decrypt-backup.php --help for full usage. Its Pass-1 conformance run is recorded in releases/ipam-3.28.0/decrypt-pass1-results.md.

v3.27.9

Single-bug hotfix plus a small Restore-tab UX change. No schema change, no migration. Drop-in upgrade from v3.27.x — no operator action required for the upgrade itself.

Bug fixed: prior versions (v3.26.0 through v3.27.8) showed a dashboard / settings banner reading "config.php cleanup needed — N non-bootstrap key(s) found … Remove them manually: bootstrap_key". That advice was wrong. bootstrap_key is auto-generated into config.php by the application and is required at runtime to unwrap the backup_vault_key envelope stored in the database; removing it breaks decryption of every IPAMBKP3 stored-mode backup on the install. v3.27.9 stops flagging it. No action needed if you never acted on the banner.

If you already removed bootstrap_key from config.php:

  1. Best — restore the original value. Check config.php.bak / config.php.bak-v3upgrade or any pre-removal backup for the line 'bootstrap_key' => '...' and paste it back into config.php (top-level array key, alongside app_secret). The vault then unwraps cleanly and nothing further is needed.
  2. If the original value is gone — re-key the vault. Visit Admin → Backups → Destinations. The vault-status badge will read unreadable (envelope exists, can't be unwrapped — see Vault recovery (unreadable envelope)). Follow the recovery procedure to generate a fresh backup_vault_key. Backups encrypted under the old key are no longer decryptable — download/restore anything you still need from an older app_secret-keyed archive first, or accept the loss of those stored-mode archives.

Either way, on the next page load the application will silently regenerate a bootstrap_key in config.php if one is missing — option 2 just makes the new key match a freshly-generated envelope instead of the orphaned old one.

One UX change: the Restore tab's destination-browse list now sorts backups newest-first by default (previously the order depended on the destination type).

v3.27.8

Backup/restore stabilization hotfix. No schema change, no migration. Drop-in upgrade from v3.27.x — no operator action required for the upgrade itself.

One operator-facing change worth knowing: the Destinations tab now shows a three-state vault-status badge (absent / present / unreadable). The unreadable state means the encrypted vault-key envelope exists in the database but cannot be unwrapped — typically because the bootstrap_key (in config.php) has changed since the envelope was written, or because the envelope was written by a different installation. If your Destinations tab now shows a red "Vault envelope exists but is unreadable" banner that you didn't see in v3.27.7, see Vault recovery (unreadable envelope) below.

v3.26.0

⚠️ Upgrade-path prerequisite: must pass through v3.23.0–v3.25.x first. v3.26.0 retires the legacy v3.7 single-destination backup runner (run_db_backup_if_due()) and deletes the four legacy settings keys (backup.enabled, backup.frequency, backup.retention, backup.dir) along with the backup.php CLI entry point. The conversion that materialised those legacy keys into the unified backup_destinations + backup_schedules rows lived in ipam_legacy_backup_migrate_if_due(), which ran on every page load between v3.23.0 and v3.25.x. The v3.26.0 migration 3.26.0-retire-legacy-backup enforces this with a hard-fail check: if the backup.legacy_migrated_v3_23_0 sentinel is missing AND any legacy backup.* key still holds a non-default value, the migration aborts with a remediation message. Operators on a pre-v3.23 install must:

  1. Apply v3.23.0, v3.24.0, or any v3.25.x release first; load any web page once so the conversion helper runs.
  2. Confirm the resulting destinations + schedule on Admin → Backups look right.
  3. Then upgrade to v3.26.0.

Removed CLI entry point. php backup.php (or php backup.php --force) is gone. The unified cron.php scheduler iterates every active row in backup_destinations + backup_schedules directly. Operator wrapper scripts that invoked backup.php should be updated to call php cron.php instead — cron.php runs every backup task plus housekeeping in one pass.

db_tools.php scope shrink. The "Automatic Backups" card, "Run Backup Now" button, and the in-page Backup History table moved entirely to Admin → Backups (backup_admin.php). The Database Tools page is now the SQL export / import + status surface only.

v3.25.0

Operator-facing finale of the backup overhaul. One schema migration (3.25.0-backup-destination-evolution) adds new columns to backup_destinations (retention_hourly|daily|weekly|monthly, is_default, default_backup_type, default_encryption_mode) and backup_runs (cancel_requested). Existing per-schedule retention values backfill into the destination on upgrade; per-schedule retention columns are preserved through this release for downgrade safety and will be dropped in a later release. No new runtime dependencies. Standard upgrade: bash upgrade.sh --yes <docroot>.

Behavior changes operators will notice on first login post-upgrade:

  • Backup format defaults to Logical for newly-saved destinations. Existing destinations migrate to default_backup_type='logical' with no operator action required; backups continue to land at the destination. To switch a destination back to engine-native dumps, edit it and pick "Database" under the Backup format radio.
  • Retention is configured on the Destinations tab now, not the Backup tab. The values you had on each schedule are preserved on the matching destination automatically.
  • encrypt=0 destinations stay unencrypted. The legacy encrypt boolean is replaced by a default_encryption_mode enum (stored | transitory | unencrypted); existing rows backfill from the boolean. Unencrypted is gated to Local destinations going forward.
  • History tab now shows an Encryption column with v1 / v2 / v3 / Plaintext / Per-passphrase pills derived from the existing encryption_mode + source_version. No data backfill — older rows pick up the right pill from their existing fields.
  • backup.encryption_change audit row vocabulary changed from old=encrypted|plaintext to old=stored|transitory|unencrypted. Audit-log filters on the previous values keep working for pre-v3.25.0 rows; new rows use the enum vocabulary.

v3.24.0

Encryption-format upgrade: new IPAMBKP3 archive format with three modes (stored / transitory / unencrypted), a new server-side secret (backup_vault_key), and a manual upload-and-restore wizard step. No schema migrations, no new runtime dependencies — the codec uses PHP's existing libsodium build for Argon2id and OpenSSL for AES-256-CTR. Standard upgrade: bash upgrade.sh --yes <docroot>.

Highlights:

  • backup_vault_key auto-generation (#836). A new 32-byte secret in config.php protects scheduled stored-mode backups. Distinct from app_secret (which protects DB-resident data like TOTP secrets). Generated lazily on the first stored-mode backup on v3.24+ — the application rewrites config.php in place to insert the value. Transitory-mode backups derive their key from the operator's passphrase and do not trigger auto-generation. No manual action is required. Operators who prefer to pre-seed manually can run php -r "echo base64_encode(random_bytes(32));" and add 'backup_vault_key' => '...' to config.php before the first backup. See configuration.md → backup_vault_key for rotation, storage, and lifecycle guidance.

  • Existing IPAMBKP1 / IPAMBKP2 archives remain restorable. The dispatcher recognises every format and routes to the matching codec. No migration is required and no operator action is forced. If you have IPAMBKP1 archives (v3.17–v3.18 vintage), consider re-encrypting them as IPAMBKP3 over time — the streaming format is memory-bounded for multi-GB databases and uses constant-time HMAC verification (#838 B-P2-5).

  • Manual upload-and-restore wizard step (#837). The Restore tab gains an Upload a backup file from your computer affordance for one-off restores of an archive that did not originate from a configured destination (e.g. a backup someone emailed you, or one you pulled from another tool). Accepts every supported format. For IPAMBKP3 transitory archives, the wizard prompts for the passphrase between upload and dry-run.

  • backup_max_upload_size_mb setting (#837). New admin-tunable upload cap, default 2048 MiB. Effective cap is the smallest of this setting, PHP's upload_max_filesize, and post_max_size. To accept larger files, raise all three.

  • Standalone decrypt CLI: Simple-PHP-IPAM/tools/decrypt-backup.php (#1043). Decrypts an IPAMBKP1/2/3 or IPAMBKU1 archive to plaintext without a running install. Useful when the originating IPAM install is destroyed but you still hold the credential. php Simple-PHP-IPAM/tools/decrypt-backup.php --help for usage.

  • Encryption-mode vocabulary changed. Before v3.24, internal docs sometimes used "stored / transitory / unencrypted" to describe encryption-at-rest vs. TLS-only vs. plaintext. From v3.24 onward those three labels describe where the key comes from — server-managed backup_vault_key, operator-typed passphrase, or no key at all. The wire path is always TLS-protected for S3 and SSH-protected for SFTP regardless of mode. See docs/backups.md → Encryption for the new model.

v3.23.0

Backup configuration consolidates onto the unified Backup & Restore admin surface. One schema migration, no new runtime dependencies. Standard upgrade: bash upgrade.sh --yes <docroot>.

Highlights:

  • Per-schedule notification overrides (#825). Every backup schedule can now opt to override the global Scheduled-backup failure / Scheduled-backup success defaults and pin its own recipient CSV. Edit on backup_admin.php?tab=notifications — an override section appears under the global toggles. Each override field is tri-state (Inherit / On / Off) so an admin can override a subset (e.g. failure email yes, success no) while inheriting everything else. Manual-run, retention, overdue and connection-test events stay global — the scheduling concept doesn't apply to them.

  • Schema migration: 3.23.0-notify-overrides. Adds four columns to backup_schedules (notify_override, notify_on_failure, notify_on_success, notify_recipients). Idempotent across SQLite / MySQL / PostgreSQL; existing schedules default to notify_override = 0 (use global), preserving the v3.20.0 / v3.22.0 behaviour after upgrade.

  • Settings › Data & Maintenance › Backup is deprecated (#1058). A banner at the top of that section now points to backup_admin.php. The legacy backup.enabled / backup.dir / backup.frequency / backup.retention keys are kept readable for backward compatibility through v3.25.0; they are scheduled for hard-removal in v3.26.0. Edit your destination + schedule on the unified surface from now on.

  • One-shot legacy migration (#1058). On the first v3.23.0 page load after upgrade, the migration helper runs and:

    1. If backup.enabled = true, materialises a Local destination (named “Legacy local backups”) from backup.dir plus a schedule from backup.frequency and backup.retention.
    2. Stamps the sentinel backup.legacy_migrated_v3_23_0 so subsequent page loads skip the helper. (Installs that never enabled legacy backups also get the sentinel stamped on first load — same short-circuit, just no destination created.)
    3. Once stamped, init.php gates run_db_backup_if_due() on the sentinel: the legacy v3.7 runner is suppressed for the rest of the v3.23.x — v3.25.x lifecycle, even though the legacy keys themselves are still readable. This is what prevents the legacy and unified runners from double-firing. The legacy keys are deliberately not cleared — an operator who needs to roll back to a pre-v3.23.0 release retains the original config until the v3.26.0 hard-removal. They no longer drive the runner; they're a backwards-compatibility safety net.
  • Engine-agnostic logical restore (#824). New IPAMBKL1 backup format (gzipped NDJSON with abstract types) lets a backup taken on one engine restore onto another. The dispatcher in ipam_restore_apply() sniffs the magic bytes and routes accordingly — existing IPAMBKP1 / IPAMBKP2 SQL dumps still take the engine-native shell-out path. Operator-facing picker UI lands in v3.25.0; v3.23.0 ships the full backend so the format is producible and consumable on day one.

v3.20.0

Backup destinations UX + reliability polish. No new pages, no schema migrations, no new runtime dependencies. Standard upgrade: bash upgrade.sh --yes <docroot>.

Highlights (all on destinations.php / backup_history.php):

  • Inline Edit drawers for destinations and schedules — edit name, type, config, retention, frequency, and credentials in place without a separate page (#778, #780). Secret-merge is hardened so partially-submitted forms cannot null out an existing key, and destination type changes are rejected on existing rows (#793).
  • Run-now per destination — destination rows have their own "Run backup now" action; previously only schedules could trigger a manual run (#779).
  • Frequency-aware schedule fields — selecting hourly / daily / weekly / monthly / cron hides the rows that don't apply (e.g. day-of-week is hidden for hourly), eliminating the "what value does this field even take for this frequency?" question (#781).
  • Auto-run Test on Save — creating or editing a destination now triggers the connectivity test automatically and renders an inline pass/fail badge with the latency and any error message (#787).
  • Timestamps render in the user's configured timezone instead of UTC across backup_history.php and the destination tables (#782). A semgrep guard rule prevents future drift back to raw UTC rendering.
  • Notification dispatch wired into both orchestratorsipam_backup_notify() was previously dead code on the schedule path; success and failure email now fires reliably from cron and from manual Run-now (#791).
  • backup_schedules.updated_at is now bumped on every cron transition (success and failure), so the column reflects actual last-touched time (#792).
  • S3 error-body redaction is scoped to signature/credential XML elements — long base64-ish runs in unrelated XML node text are no longer stripped, restoring full error context for non-auth S3 failures (#795).
  • BackupClientInterface::list() renamed to listObjects() to avoid collision with PHP's list() language construct (#796). Site-local code that called the method directly (unlikely — internal interface) needs the rename.
  • backup.php CLI uses getopt() for flag parsing and documents its exit codes (#794).

End-to-end coverage now includes a MinIO-backed integration test in CI (#789) — the backup pipeline (dump → encrypt → upload → list → download → decrypt → verify) runs against a real S3-compatible server on every push across all three database engines.

v3.19.1

Hotfix release. No new pages, no schema migrations, no new runtime dependencies. Standard upgrade: bash upgrade.sh --yes <docroot>.

If you are on v3.17.0 / v3.18.0 / v3.19.0 and tried to use a remote backup destination (S3 / SFTP / Local), READ THIS:

  • S3 destinations were broken on every v3.17–v3.19 release. A SigV4 canonical-request canonicalization bug caused every S3-compatible server (AWS, Wasabi, MinIO, Ceph) to reject the signature with HTTP 403 SignatureDoesNotMatch. The bug is fixed in v3.19.1 — re-test your destination after upgrading. Existing destination rows and credentials work unchanged; no re-entry required.
  • MySQL and PostgreSQL operators were locked out of remote destinations entirely. ipam_backup_dump_to_tmp() hard-threw on non-SQLite drivers in v3.17–v3.19 — the dump step never ran, so no remote-destination backup ever completed for non-SQLite operators. v3.19.1 dispatches to mysqldump / pg_dump natively (passwords via env, never on cmdline) and gzips the output, so all three engines now produce a .sql.gz that flows through the existing encryption + upload pipeline. Local-disk backups via the legacy backup.php CLI runner — which DID support all three engines — continued to work the whole time, so no data was at risk during the gap; only off-box / cloud-replicated backups were unavailable.

After upgrading, if you set up a destination during the broken window and gave up: it should work now. If you set up a destination and never tested it: test it now.

v3.18.0

Polish release. No new pages, no schema migrations, no new runtime dependencies. Standard upgrade: bash upgrade.sh --yes <docroot>.

  • Settings page now saves boolean toggles individually — flipping one MFA switch no longer silently disables siblings. Each boolean is its own auto-submitting form. The "Save group" button still works for non-bool fields and continues to apply the legacy group-cascade behaviour for bools when used.
  • Backup retention is now clock-aligned to the cron tick (was previously sensitive to PHP request timing across long ticks). Behaviour is unchanged for normal cron-driven schedules; the change only affects edge cases where a long-running tick straddled a retention slot boundary.
  • BackupEngine and RestoreEngine classes are gone. Replaced by top-level functions (ipam_backup_run_for_destination(), ipam_restore_prepare_for_restore(), ipam_restore_apply(), etc.) in a new lib/backup.php. All the same orchestration, just procedural instead of class-wrapped. If you have site-local code that referenced these classes (unlikely — they were not part of any public API surface), update to the function names.
  • Unified MFA card has a small visual polish pass — pill no longer italicises the "unavailable" state, preserved-enrollment hints use role="note", the tab rail handles 768–900px viewports without label wrapping.
  • New doc: Contacts — guide for linking contacts to IP addresses via the Owner typeahead.

v3.17.0

New tables: backup_destinations, backup_schedules, backup_log. Migration 3.17.0-backup is automatic and idempotent.

New pages:

  • destinations.php — Backup destinations admin (admin only)
  • backup_history.php — Backup history log with Schedules tab (admin only)
  • remote_backups.php — Remote backup browser (admin only)
  • restore_web.php — Web-based restore wizard with dry-run and confirmation typing gate (admin only)
  • download_remote_backup.php — AJAX endpoint: download a file from a destination to the admin's browser
  • test_destination.php — AJAX endpoint: test a destination connection
  • run_backup_now.php — AJAX endpoint: trigger a schedule immediately

New runtime dependency: phpseclib/phpseclib ^3.0 (SFTP transport, MIT license, zero transitive runtime deps). Bundled in the release tarball; no composer install needed at install time. Source installs must run composer install --no-dev after upgrade.

New settings registry keys:

  • backup.notify_on_failure (bool, default true) — send an email to alert_email when a backup run fails.
  • backup.notify_on_success (bool, default false) — send an email to alert_email when a backup run succeeds.

New audit actions: destination.create, destination.update, destination.delete, destination.test, backup.run, backup.failed, backup.retention_pruned, db.restore_stage, db.restore_dryrun, db.restore, remote_backup.delete, remote_backup.verify, remote_backup.download, remote_backup.download_failed.

Cron: if you already have cron.php scheduled, backup schedules run automatically the next time it fires. No additional cron entry is needed.

SQLite-only for backup dumps: v3.17.0 uses the existing SQLite-only ipam_db_dump_stream() helper for the backup dump. MySQL and PostgreSQL backup dumps are planned for a follow-up release. The legacy v3.7.0 backup.php CLI is unchanged and continues to write to backup_history.

app_secret is required for encrypted backups. The key must be set in config.php (not the settings table — it must be available before the database is opened). If you have not yet set app_secret, either leave the Encrypt option unchecked on each destination or set app_secret first:

php -r "echo bin2hex(random_bytes(32));"
# Add the output as 'app_secret' => '<value>' in config.php

v3.16.0

  • No manual upgrade steps required.
  • The 3.16.0-preferred-mfa-method migration adds a nullable preferred_mfa_method column to users. Non-destructive and idempotent. Existing users dispatch as before until they pick a preference from the Account page.
  • New setting mfa.totp_enabled (default true). Existing installs continue to allow TOTP exactly as before. Admins who want to phase TOTP out in favour of Email OTP / passkeys can disable it from Admin → Settings → Multi-Factor Auth without revoking individual user enrollments — re-enabling restores each user's previous TOTP without re-enrollment.
  • app_secret warning banner. The Settings page now flags the case where mfa.totp_enabled = true but app_secret is unset in config.php. This was previously a silent failure mode at TOTP enrollment time. If you see the banner, either generate and add app_secret to config.php (php -r "echo bin2hex(random_bytes(32));") or disable mfa.totp_enabled.
  • Settings page reorganised into 5 tabs. General, Authentication, Notifications, Data & Maintenance, Integrations. The per-subsection POST flow is unchanged — bookmarks to settings.php still work and the legacy #group-<key> anchor format auto-redirects to the correct tab via a JS shim. URL state is now carried via ?tab=.
  • Account page MFA section consolidated. TOTP, Email OTP, and Passkeys now live in a single "Two-Factor Authentication" card. No data migration; the layout change is purely UI. The "Two-Factor Authentication" heading replaces the prior three separate sections.
  • Login MFA switch graph. Each verify page (TOTP, Email OTP, Passkey) now offers switch buttons to either of the other two methods when they are enrolled and globally enabled. Extends v3.15.2's TOTP-page-only buttons.
  • Search is now case-insensitive on every supported engine. PostgreSQL was previously case-sensitive — operators who built habits around exact-case search on Postgres should know it now matches SQLite and MySQL behaviour. SQLite remains ASCII-only by design (no ICU bundled with the standard SQLite build); MySQL and PostgreSQL perform full Unicode case folding via their default collations.

v3.15.2

  • No manual upgrade steps required. No new migrations, no new settings, no new dependencies.
  • Bug fixes:
    • Passkey registration with password-manager providers (LastPass, 1Password, Bitwarden) now succeeds: requireResidentKey: 'preferred' makes credentials discoverable so they save to the vault, and attestation: 'none' avoids the "no signature found" verification failure on packed self-attestation.
    • The WebAuthn rp.name now defaults to the configured branding.site_name setting (was hardcoded to "Simple PHP IPAM"). Most password managers and OS credential dialogs display this. LastPass labels entries by rpId regardless and is unaffected — that is a LastPass UX choice, not something this app can override.
    • The default credential name shown in IPAM's passkey list is the site name (was an interactive window.prompt).
    • When TOTP and Email OTP — or TOTP and a passkey — are both enrolled, the TOTP verify page now offers "Send a code to my email instead" and "Use a passkey instead" buttons. Previously TOTP was always dispatched first with no way to choose another method.
    • The post-login redirect URL now survives idle-timeout and absolute-lifetime expiry. v3.15.1 only stashed in the cold-start branch of require_login(), so users hitting an authenticated link with a stale or expired cookie still landed on the dashboard.
    • JS / CSS cache-buster query string now includes file mtime, so edits within a release invalidate browser caches without an IPAM_VERSION bump.

v3.15.1

  • No manual upgrade steps required. No new migrations, no new settings, no new dependencies.
  • Bug fixes:
    • Post-login redirect now preserves the requested URL across all eight final-success paths (local login, demo, recovery, OIDC, TOTP, TOTP-bypass, Email OTP, passkey). Previously, clicking a deep link (e.g. an email-verification link) while logged out dropped you on dashboard.php after sign-in.
    • Email body charset is now declared as UTF-8. Previously, em-dashes and accented characters arrived as mojibake (â—) in clients that respected the declared charset.
    • The dashboard config.php cleanup banner no longer flags app_secret, session, auth, or api as stale keys. Following the previous bad advice would have broken every TOTP enrollment.
    • Email verification failures now surface the real cause (e.g. missing base_url) instead of a generic guess, with the underlying reason also logged via error_log() for server-side diagnostics.

v3.15.0

  • No manual upgrade steps required.
  • The 3.15.0-passkeys migration creates the webauthn_credentials table (passkey storage). This is non-destructive and idempotent.
  • New pages: passkey_verify.php (mid-login passkey challenge; direct navigation redirects to login.php), passkey_register.php (AJAX registration endpoint, POST only; requires an active session).
  • New settings key: mfa.passkeys_enabled (default false). Passkeys are opt-in — existing installs are unaffected. Enable via Admin → Settings → Multi-Factor Auth.
  • mfa.require now includes passkeys: when enforcement is enabled, a registered passkey satisfies the requirement alongside TOTP and Email OTP.
  • New Composer dependency: lbuchs/webauthn ^2.1 (WebAuthn server-side library, MIT, zero transitive deps). Included in the release tarball under vendor/. No action required for tarball-based installs. Source installs must run composer install --no-dev after upgrade.

v3.14.0

  • No manual upgrade steps required.
  • The 3.14.0-email-otp migration adds Email OTP columns to users (email_otp_enabled, email_otp_hash, email_otp_expires_at, email_otp_attempts). Non-destructive and idempotent.
  • New page: email_otp_verify.php (mid-login Email OTP challenge; direct navigation redirects to login.php).
  • New settings keys: mfa.email_otp_enabled (default false) and mfa.require (default false). When mfa.require is enabled, users without any enrolled 2FA method are redirected to the Account page to enroll before accessing the application.
  • SMTP and users.email are required for Email OTP enrollment. Without SMTP configured (smtp.enabled true, host/port valid) and a user-level email address, enrollment fails closed.
  • Admins can reset a user's Email OTP enrollment from users.php.

v3.13.0

  • No manual upgrade steps required.
  • The 3.13.0-settings-cascade migration adds a tenant_id column to the settings table. This is non-destructive — all existing settings rows remain and gain tenant_id = NULL (global scope). The migration is idempotent.
  • Existing config.php files are unaffected.

v3.12.0

No breaking changes, new configuration keys, or migrations.

UI changes: The dashboard two-column widget grid now requires a wider viewport before splitting (fixes overlap at ~900–1100px). Health page scanning card has two new rows: Warn alerts and Crit alerts.


v3.11.0

No database migrations or configuration changes. Pure UI polish release.

  • No manual steps required on upgrade.
  • The asset cache-buster has been bumped to ?v=3.11.0 — browsers will fetch fresh CSS/JS on first load after upgrade.

v3.10.0

No breaking changes. No manual migration steps required — migrate.php handles all schema changes automatically.

If upgrading from ≤v3.9.0: The IPAM_VERSION load-order fix (#662) resolves webhook test_fire returning HTTP 500. No config changes needed.


v3.9.0

  • backups.php removed from nav; redirects 301 to db_tools.php. Update any bookmarks or internal links.
  • No schema changes. No config changes. No new runtime dependencies.
  • Asset cache-busters bumped to ?v=3.9.0 — hard-refresh may be needed after upgrade if CSS/JS is cached.

v3.8.1

No breaking changes. Run upgrade.sh as normal — no manual steps required.

Bug fixes:

  • Dashboard KPI cards now have proper spacing below action pills.
  • Dashboard two-column grid collapses at the correct sidebar breakpoint; uPlot growth chart resizes when the sidebar is toggled.
  • Address growth chart shows a friendly empty state when no new addresses were recorded in the last 30 days.

Documentation:

  • All three database drivers (MySQL, MariaDB, PostgreSQL) correctly documented as production-ready since v3.0.0.
  • New guides: Sidebar & Command Palette, Two-Factor Authentication (moved to security.md).
  • v3.8.0 upgrade notes corrected (mobile breakpoint was listed as <768px; correct value is <1024px).

v3.8.0

New pages: None.

UI changes: The top navigation bar has been replaced with a sidebar navigation pattern. On desktop (≥1024px) the sidebar is always visible. On mobile (<1024px) a hamburger button opens the sidebar as an overlay. The previous emoji-icon nav links are now SVG Heroicons. The dashboard has been rewritten with KPI cards and a uPlot time-series chart.

New keyboard shortcuts: ⌘K / Ctrl+K opens the command palette for navigation, creating records, and toggling the theme.

No config changes, no migration, no schema changes.

v3.7.0

No breaking changes. All new features are opt-in. Upgrading from v3.6.0 requires no manual action — run upgrade.sh as normal.

New pages:

  • backups.php — Admin → Backups: lists backup history with status badges, download, SHA-256 verify, and delete
  • health.php — Admin → Health: real-time operational metrics dashboard (DB size, backup status, scan health, webhook delivery, auth/security, system info)

New CLI scripts (403 if accessed via web):

  • backup.php — Run a database backup on-demand (SQLite, MySQL, PostgreSQL)
  • restore.php — Restore from a backup file with dry-run mode and SHA-256 verification

Database migration (applied automatically on first boot after upgrade):

  • New table backup_history — tracks every backup run with status, size, and SHA-256

New database settings (Admin → Settings):

KeyDefaultPurpose
backup.enabledfalseEnable scheduled database backups via cron
backup.local_pathdata/backups/Directory for backup files (relative to app root)
backup.retention_count7Number of backup files to retain; older files are deleted
backup.schedule_cron0 2 * * *Cron expression controlling backup frequency
audit.retention_days365Days to retain audit log entries; 0 = never prune
audit.prune_batch_size1000Rows deleted per prune pass

Action required to enable backups: Set backup.enabled = true at Admin → Settings and ensure cron.php is scheduled (see docs/backups.md for cron setup). SQLite installs: no additional configuration. MySQL/PostgreSQL installs: verify the mysqldump/pg_dump binary is on the server's PATH.

See docs/backups.md for the full backup, restore, and disaster-recovery reference.


v3.6.0

No breaking changes. All new features are opt-in. Upgrading from v3.5.0 requires no manual action — run upgrade.sh as normal.

New pages:

  • totp_enroll.php — TOTP enrollment wizard (Account → Enable 2FA)
  • totp_verify.php — Mid-login 2FA challenge (shown automatically after password check if 2FA is enabled)

Database migration (applied automatically on first boot after upgrade):

  • New columns on users: totp_secret_enc, totp_enabled, failed_auth_count, locked_until, lock_reason
  • New table totp_backup_codes — stores hashed single-use backup codes per user
  • New table rate_limit_buckets — stores per-API-key sliding window counters

New config.php keys (all optional — have safe defaults):

KeyDefaultPurpose
app_secret''Encryption key for stored TOTP secrets. Required before any user can enable 2FA. Generate with: php -r "echo bin2hex(random_bytes(32));"
session.absolute_lifetime_minutes480Absolute session lifetime in minutes. 0 = no limit.
auth.lockout_after_failures10Consecutive 2FA failures before persistent account lockout.
auth.lockout_duration_minutes30Duration of a 2FA-triggered persistent lockout in minutes.

New database settings (configurable at Admin → Settings, no config.php change required):

KeyDefaultPurpose
api.rate_limit_window_seconds60Sliding window size for per-API-key rate limiting.
api.rate_limit_requests300Max requests per window per API key.

Action required to enable 2FA: Set app_secret in config.php before users attempt enrollment. Users who try to enroll without this key set will see an error. Existing installs with app_secret left empty have 2FA disabled; all other functionality is unaffected.

Warning — app_secret is a wrapping key. app_secret encrypts every user's stored TOTP secret. If you change its value after users have enrolled, their stored secrets become undecryptable and those users will be unable to pass the 2FA challenge. They will be locked out of 2FA until an admin resets their TOTP (users.php → Reset 2FA) and they re-enroll. Never rotate app_secret on a live instance without first resetting all enrolled users' TOTP. If you need to rotate the key, the safe procedure is: (1) admin-reset every enrolled user's TOTP, (2) update app_secret, (3) have all users re-enroll.

See the Security guide for full enrollment and admin-reset instructions.


v3.5.0

No breaking changes. Run upgrade.sh as normal — no manual steps required.

New admin page:

  • custom_fields.php — Admin-only. Accessible from Admin → Custom Fields. Manage per-entity custom field definitions (create, edit, delete, reorder).

New schema (applied automatically by migration 3.5.0-custom-fields):

  • New table custom_field_defs — stores field definitions (key, label, entity type, type, options, sort order, required flag).
  • subnets.custom_fields TEXT NOT NULL DEFAULT '{}' — JSON object of custom field values.
  • addresses.custom_fields TEXT NOT NULL DEFAULT '{}' — same for addresses.

API change (additive): Subnet and address responses now include a custom_fields object. Existing clients that do not inspect this key are unaffected. PUT requests now accept custom_fields; unknown keys or type mismatches return HTTP 422.

CSV change (additive): A custom_fields column is appended to export_addresses.php output. The import wizard accepts an optional custom_fields mapping column. Existing import files without this column continue to work.

See the Custom Fields guide for full documentation.


v3.4.0

No breaking changes. Run upgrade.sh as normal — no manual steps required.

New schema columns on subnets: dhcp_routers, dhcp_dns_servers, dhcp_domain_name, dhcp_lease_default, dhcp_lease_max, dhcp_next_server, dhcp_boot_filename. All nullable. Added automatically by migration 3.4.0-dhcp-options.

New page added:

  • export_dhcp.php — DHCP config export endpoint (write-role required). Accessible from Admin → DHCP Pools.

v3.3.0

No breaking changes. Run upgrade.sh as normal — no manual steps required.

New pages added:

  • webhooks.php — outbound webhook management (admin-only)

New features:

  • Outbound webhooks: configure HTTP callbacks fired on address/subnet mutations. HMAC-SHA256 signed. Admin UI at Admin → Webhooks with test-fire, delivery log, and retry. Cron retries failed deliveries (up to 3 attempts). See Webhooks guide.
  • Login history: Account page now shows last 20 login events. Users table gains a "Login history" link per user. Audit log accepts ?user_id=N and ?action=auth.login deep-link filters.
  • Brand polish: nav and footer use text logo (SimplePHPIPAM) in Fira Code monospace; dark mode buttons match marketing-site brand green.

New settings (Settings → Webhooks):

  • webhook.retention_days — delivery log retention in days (default 30; set to 0 to disable pruning)
  • webhook.allow_private_ips — allow private-IP webhook targets (default false; enable for lab environments)

v3.2.0

No breaking changes. Run upgrade.sh as normal — no manual steps required.

New pages added:

  • devices.php — device and interface management (admin-only)
  • forgot_password.php / reset_password.php — email-based password recovery (unauthenticated)

New features:

  • Device & interface records: link IP addresses to named equipment. Includes admin CRUD, REST API resources (devices, device_interfaces), device search, and CSV import columns device_name / interface_name.
  • Email password recovery with SHA-256 token, 1-hour expiry, and rate limiting (max 3 requests per hour per user). Requires SMTP configured under Admin → Settings.
  • Email change verification: users can update their email on the Account page and must verify via a link sent to the new address.
  • Utilization CSV export now supports ?include_trend=1&trend_days=N to include delta columns from snapshot history.
  • All API responses now include X-IPAM-API-Version: 1 header.
  • OpenAPI 3.1 spec available at api.php?resource=spec and committed to docs/api-spec.yaml.

Nav change: The "Password" link in the user dropdown is now labelled "Account". The URL (change_password.php) is unchanged.


v3.0.0

This is a breaking release. Read these notes carefully before upgrading.

What changed

  • config.php is now a bootstrap stub. All non-bootstrap settings (OIDC, alerting, passwords, housekeeping, backup, etc.) live in the settings database table and are managed through Admin → Settings. The upgrade migration automatically imports your customised values from the old config.php into the database and rewrites the file to stub format.
  • ?api_key= query-parameter authentication removed. Use the Authorization: Bearer <key> header instead. The deprecation headers have shipped since v2.x.
  • MySQL and PostgreSQL drivers are now stable. The experimental/beta banners are removed. Minimum versions: MySQL 8.0, PostgreSQL 14.
  • migrate_db.php — new CLI tool for migrating between database engines (all 6 direction pairs).
  • Multi-contact assignments on sites and subnets (new site_contacts and subnet_contacts tables).

Pre-upgrade checklist

  1. Back up your databaseupgrade.sh does this automatically, but make a manual copy too
  2. Back up config.php — the migration rewrites it; a .bak-v3upgrade copy is created automatically
  3. Check PHP version — PHP 8.2+ required (unchanged from v2.x)
  4. Check API clients — any client using ?api_key= must switch to Authorization: Bearer header

Upgrade paths

Path 1: Stay on SQLite (default)

Run upgrade.sh as usual. The migration:

  1. Imports customised config.php values into the settings table
  2. Backs up old config.php as config.php.bak-v3upgrade
  3. Rewrites config.php to stub format
  4. Creates new tables (site_contacts, subnet_contacts)

No manual action required.

Path 2: Migrate to MySQL or PostgreSQL during the upgrade

After running upgrade.sh:

# 1. Provision the target schema
mysql -u ipam -p ipam_db < Simple-PHP-IPAM/schema.mysql.sql
# or: psql -U ipam ipam_db < Simple-PHP-IPAM/schema.pgsql.sql

# 2. Run the migration tool
php Simple-PHP-IPAM/migrate_db.php \
  --from=sqlite --from-dsn="sqlite:Simple-PHP-IPAM/data/ipam.sqlite" \
  --to=mysql --to-dsn="mysql:host=127.0.0.1;dbname=ipam_db" \
  --to-user=ipam --to-pass=secret

# 3. Update config.php with the new driver
# (migrate_db.php prints the exact lines to change)

# 4. Restart Apache / PHP-FPM

Post-upgrade verification

  1. Admin login works
  2. Settings page loads — verify your imported settings are correct
  3. Create a test subnet and address
  4. Run php migrate.php — should be a no-op
  5. If using OIDC, verify SSO login still works

Rollback

If something goes wrong:

  1. Restore config.php.bak-v3upgradeconfig.php
  2. Restore the database backup created by upgrade.sh
  3. Redeploy the v2.x release bundle

v2.9.0

New vendor/ directory in the release tarball. Starting in v2.9.0, the release bundle includes Simple-PHP-IPAM/vendor/ with a pre-built Composer autoloader. You do not need Composer on the target server — upgrade.sh handles vendor/ like every other file in the tree (rsync overwrites it on each upgrade).

If you have customised anything inside vendor/ on the server, those changes will be lost on upgrade. Don't do that — keep all customisation outside vendor/.

The bundled vendor/.htaccess denies direct HTTP access to library source. Verify after upgrade with:

curl -sI https://your-host/path/to/ipam/vendor/autoload.php | head -1
# Expected: HTTP/1.1 403 Forbidden (or 404 if your server hides denied paths)

No schema changes that require manual action, provided php is in PATH. v2.9.0 includes a one-shot internal migration that normalises ip_bin / network_bin storage to BLOB affinity on SQLite. upgrade.sh runs migrate.php automatically when the php CLI is available, so no operator action is required. The migration is idempotent — re-running it is a no-op.

⚠️ If php is not in PATH, upgrade.sh skips the migration step (see upgrade.sh lines 75-77 and 239-244). On minimalist containers or systems where php is only reachable via a full path, either add php to PATH before running upgrade.sh or run php /var/www/ipam/migrate.php manually after the upgrade completes. Without this step, your install retains TEXT-affinity ip_bin / network_bin storage, and ORDER BY ip_bin will produce incorrect results once new rows start arriving via v2.9.0's PDO::PARAM_LOB binding.


v2.7.0

No schema changes. v2.7.0 is entirely a runtime rewire — every registered setting was already seeded into the settings table by v2.6.0's migration. upgrade.sh just syncs files, runs php migrate.php (no-op), and you are done.

What changes at runtime:

  • Every subsystem now reads through ipam_setting() instead of $config[...]. Edits in ⚙ Admin → Settings take effect on the next request without a config.php change or a restart.
  • config.php still works as a fallback for back-compat. Nothing in your existing config.php stops working on upgrade.
  • Three new registered OIDC keys (oidc.disable_local_login, oidc.disable_emergency_bypass, oidc.hide_emergency_link) are now visible in the admin UI. If you already have these in config.php they keep working via the fallback chain.

Dealing with the deprecation banner. After upgrading, if you have customised any non-bootstrap value in config.php (i.e. the value differs from the registry default), the admin Settings page will show a config.php settings to migrate banner listing each such key. You have two options:

  1. Click Import to database on each row in the banner. This copies the current config.php value into the settings table in one atomic write (audited), and the row disappears. You can then delete the key from config.php at your convenience.
  2. Leave it. The fallback keeps working through v2.7.x. You can batch the migration later — the banner stays visible until every customised key is imported or matches the registry default.

The dashboard shows a matching admin warning card linking to the banner so you do not forget.

Server log warning. Once per hour (rate-limited via data/tmp/deprecation_warning.txt), init.php writes a single consolidated line to the PHP error log listing every registered key still being served from config.php. This is informational — nothing is broken. It is there so your log aggregator surfaces the migration work before v3.0.0 removes the fallback.

Sensitive values in the banner. Sensitive keys (oidc.client_secret, login_protection.secret_key, recaptcha_enterprise.api_key) are masked as *** in the banner — the secret itself is never rendered into the HTML source. Import to database still imports the real value from config.php; the mask only affects the display.

What stays in config.php forever: the bootstrap keys — db_path, session_name, proxy_trust, force_https / base_url, and bootstrap_admin. These are loaded before the database is open and will never be in the settings table.


v2.1.0

New database tables: vrfs, contacts

Modified tables:

  • subnets: the UNIQUE(cidr) constraint is replaced with UNIQUE(cidr, vrf_id) to support the same CIDR in different VRFs. Existing subnets get vrf_id = NULL (global VRF) — no data is lost.
  • addresses: new nullable column owner_contact_id (FK → contacts.id).

These changes are applied automatically by upgrade.sh via php migrate.php.

New admin pages: vrfs.php (VRF management), contacts.php (Contacts management) — both accessible from the Admin dropdown.

New config keys added automatically by config_auto_populate on first boot after upgrade:

  • api_max_attempts (default 20)
  • api_lockout_seconds (default 300)
  • api_bulk_limit (default 500)
  • recaptcha_enterprise block (disabled by default)

API additions: resource=vrfs (full CRUD), resource=contacts (CRUD + ?q= search), ?vrf_id= filter on subnets, ?contact_id= filter on addresses, owner_contact_id/owner_contact_name fields in address responses, vrf_id/vrf_name fields in subnet responses, search.php?format=json for the ⌘K overlay.


v2.0.0

New database tables: vlans, tags, subnet_tags, address_tags, alert_state

Modified tables:

  • subnets: new columns vlan_fk (FK → vlans.id), tags (via join table subnet_tags).
  • addresses: new columns mac, expires_at, tags (via join table address_tags).
  • sites: new column parent_id (nullable FK → sites.id, self-referential for region/site hierarchy).
  • users: new columns name, email, last_login_at, password_changed_at, theme.

These changes are applied automatically by upgrade.sh via php migrate.php.

New admin pages: vlans.php (VLAN management), tags.php (Tag management) — both accessible from the Admin dropdown.

New config keys added automatically on first boot after upgrade:

  • utilization_warn, utilization_critical (subnet utilization thresholds)
  • auto_reserve_network_broadcast (default true)
  • alert_email, alert_util_warn_pct, alert_util_crit_pct (email alerts, disabled by default)
  • password_policy block
  • login_protection block (bot protection, disabled by default)
  • demo_mode block (disabled by default)
  • oidc block (disabled by default)

API additions: vlan_name and tags[] on subnet responses, tags[] on address responses, resource=vlans (full CRUD), ?tag= / ?vlan_id= / ?parent_id= / ?site_id= / ?ip_version= / ?expired=1 filters, bulk write (POST ?resource=addresses&bulk=1 / POST ?resource=subnets&bulk=1).


CLI utilities

These scripts are run from the application directory using the PHP CLI.

Run database migrations manually

cd /var/www/ipam
php migrate.php

Applies any pending schema migrations. Migrations are also applied automatically on each web request (ipam_db_init()) and during upgrades via upgrade.sh, so this is only needed for scripted or manual deployments.

Clean up stale temp files

cd /var/www/ipam
php tmp_cleanup.php

Deletes uploaded CSV files and import plan files in data/tmp/ that are older than tmp_cleanup_ttl_seconds (default: 24 hours). This also runs automatically as part of lazy housekeeping on normal site traffic — a cron job is not required.


Vault recovery

If the Destinations tab shows a red "Vault envelope exists but is unreadable" banner, the backup_vault_key envelope stored in the database cannot be decrypted by the running install's bootstrap_key. This is a one-way situation — by design, the envelope cannot be recovered without the original bootstrap key. New backups will fail (or in v3.27.x+, hard-fail with a (preflight-failed-…) row and a backup.preflight_failed audit) until the envelope is replaced.

Common causes:

  • bootstrap_key in config.php was rotated, regenerated, or different between the host that wrote the envelope and the host now reading it.
  • The DB was restored from another install's backup that carries that install's envelope.
  • A misconfigured load-balanced setup where two app instances run with different bootstrap_key values.

Recovery options:

  1. Restore the original bootstrap_key (preferred — keeps existing encrypted archives readable).

    1. Locate the original value (offline backup of config.php, deploy artefact, password manager).
    2. Paste it into config.php$config['bootstrap_key'] = '...';.
    3. Reload the Destinations tab. The banner clears and the badge returns to present.
  2. Discard the envelope and write a new one (only when the original key is truly lost). This makes every existing backup_vault_key-encrypted archive (.ipambkp3) unrecoverable — they remain encrypted under the old key. Only proceed if you have other recovery paths for the data those archives protected.

    1. Go to Admin → Backups → DestinationsSet new vault key. The new key is generated server-side and wrapped under the current bootstrap_key.
    2. Future backups encrypt under the new key. Past .ipambkp3 archives remain unreadable.
    3. Consider keeping the old archives offline for forensic completeness even though they cannot be decrypted by the running install.

Why this can happen silently after a DB restore: restoring a database dump from one install on top of another install copies the encrypted envelope but does not copy bootstrap_key (which lives in config.php, not the DB — by design). If the destination install's bootstrap_key differs from the source's, the envelope is structurally valid but cryptographically unreadable. This is the case the v3.27.8 banner exists to surface.

Why the orchestrator is now hard-failing instead of writing plaintext: as of v3.27.1, ipam_backup_resolve_encrypt_to_tmp() throws when encryption is requested but no usable key is available. The orchestrator's preflight catch records a backup_runs row with status='failed', a synthetic (preflight-failed-<8hex>) filename, the truncated exception in error_message, and a backup.preflight_failed audit entry — then re-throws so the calling cron task fails visibly. No plaintext fallback exists. If you need backups to continue running before recovery, switch the destination's encryption_mode to unencrypted (only allowed for local destination type per the v3.25.0 server-side guard) or restore the original bootstrap_key.