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.phpdecomposition, scan consolidation (no breaking changes) - v3.33.0 — refactor wave 2 (
api.php/import_csv/migrations), ADR-003global $configsweep 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 (
IPAMSEC1envelopes), webhook-secret consolidation,lib.phprefactor wave 1 finish (no breaking changes) - v3.30.0 — per-user theme preference table with backfill, settings type system moved to PHP,
lib.phpdecomposed 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.1 — Hotfix: 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.0 — breaking changes, config.php stub, driver promotion
- v3.35.0 — refactor wave 4 (lib polish + admin refactor), TOTP backup-code O(1) lookup,
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.phpand the entiredata/directory - Fixes file permissions after the sync
- Runs database migrations automatically (if the
phpCLI 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
| Flag | Description |
|---|---|
--yes | Non-interactive — skip confirmation prompts |
--force | Allow reinstalling the same version |
--force-downgrade | Allow downgrading (not recommended — may break the DB schema) |
Environment variables
| Variable | Default | Description |
|---|---|---|
CLEANUP_ARTIFACTS | 1 | Remove build/upgrade artefacts from the target webroot after success |
REMOVE_UPGRADE_SH_FROM_TARGET | 1 | Also 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.shas 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-visiblewhich 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 explicitfont-size. Custom CSS overridingbody { 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.pngand*-linux.pngare committed. If you run Playwright locally, seetesting/playwright/update-vr-baselines.shfor the regeneration recipe.
v3.35.0
- One schema migration:
3.35.0-totp-backup-lookup-key. Addstotp_backup_codes.lookup_key VARCHAR(16) NULL+ composite indexidx_totp_backup_lookup (user_id, lookup_key). Idempotent; no manual action required. Existing rows keeplookup_key = NULL; the verifier falls back to a slow scan for them (one release). - New
composer lint:atCI gate. Refuses un-justified@-suppressions in PHP. Operators forking the codebase: add inline comments or replace with explicit handling. - New
lib/bootstrap_*.phpmodules.init.phpis now a thin top-level chain; module boot order is preserved. lib/scan.phpconsolidation.scan_run.phpandcron.phpnow 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_secretrotation invalidatestotp_backup_codes.lookup_key. After rotatingapp_secret, runUPDATE totp_backup_codes SET lookup_key = NULLso 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, rewritesimport_csv.php, consolidatesmigrations.php, completes the ADR-003global $configsweep, and adds test coverage. Upgrade withupgrade.shas normal. - API list-response flat shape is soft-deprecated (integrations only). List endpoints now emit a
Deprecation: trueresponse header (RFC 8594) when returning the legacy flat shape (items under a resource-named key alongside top-leveltotal/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.phpfiles 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.shas normal. - PHPMailer upgraded to 7.x (transparent).
phpmailer/phpmailerwas 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.phpfiles 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, andlogin_protection.secret_key— plus webhook secrets are now stored encrypted in thesettingstable asIPAMSEC1envelopes (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(inconfig.php) is now the encryption root for all settings-table secrets. Ifconfig.phpis lost orapp_secretis regenerated, every encrypted secret becomes unrecoverable — a database backup alone is no longer sufficient. Ensureconfig.phpis backed up off-site. See Backups → Disaster recovery. - No breaking changes. No navigation or URL changes. Existing
config.phpfiles 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_preferencestable instead of a single shared column. The3.30.0-user-preferencesmigration 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.typedatabase 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.phpis decomposed into 12lib/*.phpmodules. No effect on operators; no config-format changes; existingconfig.phpfiles 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 behindcomposer.lockthe gate now fails fast (testing/scripts/check-composer-lock-drift.sh). Resolution:composer install. The priorcomposer status-based check produced empty output and missed real drift. settings.phpper-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.phpfiles keep working unchanged.
v3.28.2
- Install-key lifecycle:
app_secretis now lazily auto-generated on first need (first TOTP enrollment, first restore staging). If you previously leftapp_secretblank, you'll see a one-time admin banner the first time the value is generated. Back upconfig.phpimmediately after. Auto-gen requiresconfig.phpto 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, andbackup_vault_key. - Bootstrap key announcement:
bootstrap_keyauto-generation (already lazy since v3.26.0) now writes an audit-log row and surfaces the same banner. Existing installs whosebootstrap_keywas 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.phpfiles 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 CASCADEremoves any extensions installed into thepublicschema (e.g.pgcrypto,pg_trgm,citext,uuid-ossp). Apg_dumptaken with defaults re-emits theCREATE EXTENSIONstatements and the restore puts them back. A--data-onlydump (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 0640aftermove_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.phpfor IPv6 —ipam_scan_subnet()is IPv4-only). (#1160, F-S2-01) - Scheduled scans (cron) now emit a
scan.runaudit row per subnet, matchingapi_scan_runandscan_run.phpwith a(cron)tag. (#1161, F-S2-04) ipam_mark_stale_addressesthreshold defensively clamped to[1, 50]so a mis-edited tenant setting can't produce nonsenseLIMITbounds. (#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 byipam_bootstrap_key()inlib/vault.php. (#1176) - Regression test pins that
ipam_webhook_retry_pending()does not decrypt secrets (decryption belongs only inipam_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 tounencrypted.
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 anencrypton/off flag, and "on" meant "encrypt withapp_secret". The v3.24.0/v3.25.0 redesign replaced that flag with thestored | transitory | unencryptedenum, and the migration had to map an oldencrypt=1destination 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 — removebootstrap_key" banner (which, if you complied, orphaned any auto-generated vault key), some installs end up with an unreadablebackup_vault_keyenvelope they never deliberately set. What to do: decide whether you actually want vault-key encryption for that destination. If yes — recover the originalbootstrap_key(best; see Vault recovery (unreadable envelope)) or, if it's gone, clear the orphanedbackup_vault_keysetting and set a fresh one. If no — just change the destination's encryption mode tounencryptedon the Destinations tab. Either way, yourapp_secret-encrypted archives (IPAMBKP1/IPAMBKP2.enc) are unaffected — they decrypt withapp_secret, independent of the vault key. (As of v3.28.0 the orchestrator no longer auto-generates a vault key intoconfig.phpon 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:
- Inventory your retained archives. Any IPAMBKP1/IPAMBKP2 (
.enc) archive you intend to keep restorable in-app needs to be migrated. - 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.enccopy once verified. - Or keep the escape hatch handy: retain a copy of
app_secret(the value fromconfig.php) stored separately from the archive, plus a copy oftools/decrypt-backup.php. After v4.0.0 you'd runphp tools/decrypt-backup.php --in old.enc --out plain.sqlite --app-secret <hex>to recover the plaintext, then re-import it.
- Re-encrypt under the vault key (recommended): decrypt the legacy archive (in-app Restore → stage → or
- 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:
- Best — restore the original value. Check
config.php.bak/config.php.bak-v3upgradeor any pre-removal backup for the line'bootstrap_key' => '...'and paste it back intoconfig.php(top-level array key, alongsideapp_secret). The vault then unwraps cleanly and nothing further is needed. - 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 olderapp_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:
- Apply v3.23.0, v3.24.0, or any v3.25.x release first; load any web page once so the conversion helper runs.
- Confirm the resulting destinations + schedule on Admin → Backups look right.
- 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=0destinations stay unencrypted. The legacyencryptboolean is replaced by adefault_encryption_modeenum (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-passphrasepills derived from the existingencryption_mode+source_version. No data backfill — older rows pick up the right pill from their existing fields. backup.encryption_changeaudit row vocabulary changed fromold=encrypted|plaintexttoold=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_keyauto-generation (#836). A new 32-byte secret inconfig.phpprotects scheduled stored-mode backups. Distinct fromapp_secret(which protects DB-resident data like TOTP secrets). Generated lazily on the first stored-mode backup on v3.24+ — the application rewritesconfig.phpin 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 runphp -r "echo base64_encode(random_bytes(32));"and add'backup_vault_key' => '...'toconfig.phpbefore the first backup. Seeconfiguration.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_mbsetting (#837). New admin-tunable upload cap, default 2048 MiB. Effective cap is the smallest of this setting, PHP'supload_max_filesize, andpost_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 --helpfor 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. Seedocs/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 tobackup_schedules(notify_override,notify_on_failure,notify_on_success,notify_recipients). Idempotent across SQLite / MySQL / PostgreSQL; existing schedules default tonotify_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 legacybackup.enabled/backup.dir/backup.frequency/backup.retentionkeys 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:
- If
backup.enabled = true, materialises a Local destination (named “Legacy local backups”) frombackup.dirplus a schedule frombackup.frequencyandbackup.retention. - Stamps the sentinel
backup.legacy_migrated_v3_23_0so 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.) - Once stamped,
init.phpgatesrun_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.
- If
Engine-agnostic logical restore (#824). New
IPAMBKL1backup format (gzipped NDJSON with abstract types) lets a backup taken on one engine restore onto another. The dispatcher inipam_restore_apply()sniffs the magic bytes and routes accordingly — existingIPAMBKP1/IPAMBKP2SQL 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/cronhides 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.phpand the destination tables (#782). A semgrep guard rule prevents future drift back to raw UTC rendering. - Notification dispatch wired into both orchestrators —
ipam_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_atis 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 tolistObjects()to avoid collision with PHP'slist()language construct (#796). Site-local code that called the method directly (unlikely — internal interface) needs the rename.backup.phpCLI usesgetopt()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 tomysqldump/pg_dumpnatively (passwords via env, never on cmdline) and gzips the output, so all three engines now produce a.sql.gzthat flows through the existing encryption + upload pipeline. Local-disk backups via the legacybackup.phpCLI 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.
BackupEngineandRestoreEngineclasses are gone. Replaced by top-level functions (ipam_backup_run_for_destination(),ipam_restore_prepare_for_restore(),ipam_restore_apply(), etc.) in a newlib/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 browsertest_destination.php— AJAX endpoint: test a destination connectionrun_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, defaulttrue) — send an email toalert_emailwhen a backup run fails.backup.notify_on_success(bool, defaultfalse) — send an email toalert_emailwhen 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-methodmigration adds a nullablepreferred_mfa_methodcolumn tousers. Non-destructive and idempotent. Existing users dispatch as before until they pick a preference from the Account page. - New setting
mfa.totp_enabled(defaulttrue). 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_secretwarning banner. The Settings page now flags the case wheremfa.totp_enabled = truebutapp_secretis unset inconfig.php. This was previously a silent failure mode at TOTP enrollment time. If you see the banner, either generate and addapp_secrettoconfig.php(php -r "echo bin2hex(random_bytes(32));") or disablemfa.totp_enabled.- Settings page reorganised into 5 tabs. General, Authentication, Notifications, Data & Maintenance, Integrations. The per-subsection POST flow is unchanged — bookmarks to
settings.phpstill 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, andattestation: 'none'avoids the "no signature found" verification failure on packed self-attestation. - The WebAuthn
rp.namenow defaults to the configuredbranding.site_namesetting (was hardcoded to "Simple PHP IPAM"). Most password managers and OS credential dialogs display this. LastPass labels entries byrpIdregardless 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_VERSIONbump.
- Passkey registration with password-manager providers (LastPass, 1Password, Bitwarden) now succeeds:
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.phpafter 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.phpcleanup banner no longer flagsapp_secret,session,auth, orapias 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 viaerror_log()for server-side diagnostics.
- 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
v3.15.0
- No manual upgrade steps required.
- The
3.15.0-passkeysmigration creates thewebauthn_credentialstable (passkey storage). This is non-destructive and idempotent. - New pages:
passkey_verify.php(mid-login passkey challenge; direct navigation redirects tologin.php),passkey_register.php(AJAX registration endpoint, POST only; requires an active session). - New settings key:
mfa.passkeys_enabled(defaultfalse). Passkeys are opt-in — existing installs are unaffected. Enable via Admin → Settings → Multi-Factor Auth. mfa.requirenow 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 undervendor/. No action required for tarball-based installs. Source installs must runcomposer install --no-devafter upgrade.
v3.14.0
- No manual upgrade steps required.
- The
3.14.0-email-otpmigration adds Email OTP columns tousers(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 tologin.php). - New settings keys:
mfa.email_otp_enabled(defaultfalse) andmfa.require(defaultfalse). Whenmfa.requireis enabled, users without any enrolled 2FA method are redirected to the Account page to enroll before accessing the application. - SMTP and
users.emailare required for Email OTP enrollment. Without SMTP configured (smtp.enabledtrue, 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-cascademigration adds atenant_idcolumn to thesettingstable. This is non-destructive — all existing settings rows remain and gaintenant_id = NULL(global scope). The migration is idempotent. - Existing
config.phpfiles 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.phpremoved from nav; redirects 301 todb_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 deletehealth.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):
| Key | Default | Purpose |
|---|---|---|
backup.enabled | false | Enable scheduled database backups via cron |
backup.local_path | data/backups/ | Directory for backup files (relative to app root) |
backup.retention_count | 7 | Number of backup files to retain; older files are deleted |
backup.schedule_cron | 0 2 * * * | Cron expression controlling backup frequency |
audit.retention_days | 365 | Days to retain audit log entries; 0 = never prune |
audit.prune_batch_size | 1000 | Rows 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):
| Key | Default | Purpose |
|---|---|---|
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_minutes | 480 | Absolute session lifetime in minutes. 0 = no limit. |
auth.lockout_after_failures | 10 | Consecutive 2FA failures before persistent account lockout. |
auth.lockout_duration_minutes | 30 | Duration of a 2FA-triggered persistent lockout in minutes. |
New database settings (configurable at Admin → Settings, no config.php change required):
| Key | Default | Purpose |
|---|---|---|
api.rate_limit_window_seconds | 60 | Sliding window size for per-API-key rate limiting. |
api.rate_limit_requests | 300 | Max 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_secretis a wrapping key.app_secretencrypts 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 rotateapp_secreton 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) updateapp_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=Nand?action=auth.logindeep-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 columnsdevice_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=Nto include delta columns from snapshot history. - All API responses now include
X-IPAM-API-Version: 1header. - OpenAPI 3.1 spec available at
api.php?resource=specand committed todocs/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.phpis now a bootstrap stub. All non-bootstrap settings (OIDC, alerting, passwords, housekeeping, backup, etc.) live in thesettingsdatabase table and are managed through Admin → Settings. The upgrade migration automatically imports your customised values from the oldconfig.phpinto the database and rewrites the file to stub format.?api_key=query-parameter authentication removed. Use theAuthorization: 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_contactsandsubnet_contactstables).
Pre-upgrade checklist
- Back up your database —
upgrade.shdoes this automatically, but make a manual copy too - Back up
config.php— the migration rewrites it; a.bak-v3upgradecopy is created automatically - Check PHP version — PHP 8.2+ required (unchanged from v2.x)
- Check API clients — any client using
?api_key=must switch toAuthorization: Bearerheader
Upgrade paths
Path 1: Stay on SQLite (default)
Run upgrade.sh as usual. The migration:
- Imports customised
config.phpvalues into the settings table - Backs up old
config.phpasconfig.php.bak-v3upgrade - Rewrites
config.phpto stub format - 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
- Admin login works
- Settings page loads — verify your imported settings are correct
- Create a test subnet and address
- Run
php migrate.php— should be a no-op - If using OIDC, verify SSO login still works
Rollback
If something goes wrong:
- Restore
config.php.bak-v3upgrade→config.php - Restore the database backup created by
upgrade.sh - 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
phpis not inPATH,upgrade.shskips the migration step (seeupgrade.shlines 75-77 and 239-244). On minimalist containers or systems wherephpis only reachable via a full path, either addphptoPATHbefore runningupgrade.shor runphp /var/www/ipam/migrate.phpmanually after the upgrade completes. Without this step, your install retains TEXT-affinityip_bin/network_binstorage, andORDER BY ip_binwill produce incorrect results once new rows start arriving via v2.9.0'sPDO::PARAM_LOBbinding.
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 aconfig.phpchange or a restart. config.phpstill works as a fallback for back-compat. Nothing in your existingconfig.phpstops 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 inconfig.phpthey 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:
- Click Import to database on each row in the banner. This copies the current
config.phpvalue into thesettingstable in one atomic write (audited), and the row disappears. You can then delete the key fromconfig.phpat your convenience. - 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: theUNIQUE(cidr)constraint is replaced withUNIQUE(cidr, vrf_id)to support the same CIDR in different VRFs. Existing subnets getvrf_id = NULL(global VRF) — no data is lost.addresses: new nullable columnowner_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_enterpriseblock (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 columnsvlan_fk(FK →vlans.id),tags(via join tablesubnet_tags).addresses: new columnsmac,expires_at,tags(via join tableaddress_tags).sites: new columnparent_id(nullable FK →sites.id, self-referential for region/site hierarchy).users: new columnsname,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(defaulttrue)alert_email,alert_util_warn_pct,alert_util_crit_pct(email alerts, disabled by default)password_policyblocklogin_protectionblock (bot protection, disabled by default)demo_modeblock (disabled by default)oidcblock (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_keyinconfig.phpwas 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_keyvalues.
Recovery options:
Restore the original
bootstrap_key(preferred — keeps existing encrypted archives readable).- Locate the original value (offline backup of
config.php, deploy artefact, password manager). - Paste it into
config.php→$config['bootstrap_key'] = '...';. - Reload the Destinations tab. The banner clears and the badge returns to present.
- Locate the original value (offline backup of
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.- Go to Admin → Backups → Destinations → Set new vault key. The new key is generated server-side and wrapped under the current
bootstrap_key. - Future backups encrypt under the new key. Past
.ipambkp3archives remain unreadable. - Consider keeping the old archives offline for forensic completeness even though they cannot be decrypted by the running install.
- Go to Admin → Backups → Destinations → Set new vault key. The new key is generated server-side and wrapped under the current
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.