Repository navigation
v3.16.0
Egress can follow the IP ranges a provider publishes, the dashboard session
moves to an HttpOnly cookie that no script in the page can read, and with
GDPR mode on client addresses are masked everywhere they leave the backend.
Two API changes are BREAKING under X-API-Version: 1: read the Upgrade
notes and Removed first.
Upgrade notes
-
Take a backup first. On the first start the backend runs schema
versions 7 (createsegress_providersandegress_provider_entries) and 8
(archives three retired settings into the Audit log and deletes them, see
below), each once;PRAGMA user_versionis 8 afterwards. -
The dashboard signs in with an HttpOnly session cookie (
spm_session)
instead of keeping the token in the browser'slocalStorage. Everyone
signed in to the dashboard signs in once more after the upgrade; a token an
earlier version left inlocalStorageis revoked and removed on the first
load. API clients are unchanged:POST /api/auth/loginwithout
X-SPM-Session: cookiestill returnsaccess_tokenandrefresh_token, and
Basic and bearer authentication work as before. An external reverse proxy
in front of the dashboard must pass theCookieandX-Requested-With
headers through (nginx and Caddy do by default). -
The backend service joins the proxy group:
group_add: ["13"]in
docker-compose.ymlanddeploy/docker-compose.prod.yml. A deployment that
keeps its own compose file should add it to the backend service; without it
the backend logs a warning on each proxy password change and new
credentials get407until the proxy reloads, as before. -
Schema version 8 archives and deletes
allowed_networks,waf_custom_rules
andwaf_block_thresholdfromsettings(#444). If any of the three keys
had a row insettings, the migration copies its previous value into the
Audit log (action = retire_setting_value,username = system,targetset
to the key name,detailsholding{"value": "..."}, truncated at 64 KiB
with"truncated": truewhen longer) before deleting the row.retire_setting_value
rows follow the Audit log retention of 365 days (AuditRetentionDays), so
values can be recovered from the Audit log for a year or from a database
export at any time.waf_custom_rulesin the database was never read by the
WAF: active custom rules live in/config/waf_custom_rules.txt.
Removed
- BREAKING (
X-API-Version: 1): the settingsallowed_networks,
waf_custom_rulesandwaf_block_threshold, which nothing in the backend,
proxy or WAF read (#444). Schema version 8 archives their values into
audit_log(retire_setting_value) and deletes their rows;POST /api/settingsskips them,PUT /api/settings/{name}answers 400, and
POST /api/maintenance/restore-configlists them inskipped_settings.
Added
-
scripts/local-ci.shruns the 26 required CI gates locally, including the
Docker stack suites, Playwright and Trivy, and writesresults.txtwith
one line per gate. It runs only in a linked git worktree, since the suites
wipeconfig/,data/andlogs/and rewrite.env
(SPM_LOCAL_CI_ALLOW_RESET=1accepts a disposable clone), and holds a lock
so two runs on one Docker host never collide; a lock left by a process that
no longer runs is taken over. Seetests/README.md. -
The global search (Cmd+K / Ctrl+K) also searches Allowed IPs, Allowed
domains and the egress allowlist, and a result opens its own tab of
Blacklists, or the Egress page, with the searched term in the filter. The
"Domain blacklist" line of Analytics opens the Blocked domains tab. -
Managed egress destination provider lists (
GET /api/egress-providers,
PUT /api/egress-providers/{name},POST /api/egress-providers/url,
DELETE /api/egress-providers/url/{id},
POST /api/egress-providers/{name}/refresh,
POST /api/egress-providers/{name}/apply-held) and the Provider lists
section on the Egress allowlist page: operators can enable IPv4 and IPv6 CIDR
ranges from a fixed 12-provider catalog (amazon,cloudflare,
digitalocean,facebook,github,google,linode,microsoft,
oracle,telegram,twitter,vultr, plusall) fetched at a pinned
commit oflord-alfred/ipranges, or add custom CIDR list URLs.
Shared-network providers require explicit confirmation (confirm_shared: true, with a confirmation dialog in the UI and a danger dialog showing the
IPv4 space percentage forall). Updates are validated as a whole before
replacing the previous list (rejecting malformed lines, prefixes wider than
/8in IPv4 or/16in IPv6, any non-public range, or lists over 50,000
entries), held for manual approval when covered addresses grow by more than
25% or when a previously empty address family appears from zero while the
other family already had entries, refreshed every
egress_provider_refresh_hourshours (default 24, from 6 to 168, configurable
under Settings, Filtering), recorded in the Audit log with commit SHA and
counts, and exported intodst_allow_ip.txtas a deduplicated union without
touching the manual egress allowlist. -
The
observabilityprofile starts:infra/holds the Prometheus
configuration (scrapingbackend:5000/metricsandwaf:8080/metrics), six
alert rules ininfra/prometheus/rules/spm.rules.ymland the Grafana
Prometheus datasource. A CI job starts the profile and checks both. -
GET /api/statusandGET /api/security/cvereportcves_checked, false
for a Squid major version the CVE list does not cover (the Squid 7 the image
ships); the dashboard shows "Not checked for Squid 7.x" instead of a zero
count.
Changed
-
Dashboard sessions moved from
localStorageto an HttpOnly cookie.
Script in the page, an injected one included, can no longer read the
session token. The cookie isSameSite=Strict,Path=/api,Secureover
https, and lastsJWT_EXPIRE_DURATION. A state-changing request
authenticated by the cookie alone needsX-Requested-With: spmand a
same-siteOrigin, or answers403.GET /api/sessionreports the session
(username,via,expires_at), and logout and password changes clear or
renew the cookie. CORS no longer names the dashboard's host over plain
http://. -
The notification queue reads and decrypts the notification settings once
for a burst of alerts instead of once per alert (measured: about 28 µs and
119 allocations per read on an Apple M4, 31 ns for a cached one). A save
through the API, a config restore or a database reset applies to the next
alert; a change made outside the API reaches the queue within 2 seconds. -
The UI no longer shows the global "Failed to load data" toast on top of the
error each page already reports in place. Signing in reads
GET /api/settingsonce instead of twice, and the active Blacklists tab and
the Egress page take the list count from the page they already download
instead of a second?limit=1request. The sign-in screen, the setup wizard
and the global search load as separate chunks: the entry chunk goes from
76.3 kB to 35.7 kB (23.1 kB to 11.0 kB gzip). Unused CSS, components and the
zoddependency are gone. -
The setup wizard writes only the settings it shows: it no longer sets
enable_proxy_auth,ssl_bump_enabled,enable_time_restrictionsand
enable_bandwidth_limitstofalse. -
BREAKING (
X-API-Version: 1): withgdpr_modeon, client endpoints
and log filters operate on/24(IPv4) and/64(IPv6) networks instead of
single addresses (#459).GET /api/clients/statisticsandtop_clientsin
GET /api/dashboard/summaryaggregate rows by network (ip_address/ip
masked as192.168.1.xor2001:db8:1:2::x,networkCIDR, distinct
addressescount, and"mixed"classification when a network mixes internal
and external addresses);GET /api/clients/{ip}/detailsand thesource_ip
filter onGET /api/logsandGET /api/logs/exportaccept/24and/64
CIDRs (both URL-encoded%2Fand/api/clients/{ip}/{prefix}/details) and
refuse single addresses with400whilegdpr_modeis on;GET /api/database/exportadds"contains_unmasked_addresses": truewhen
gdpr_modeis on. -
The sign-in page, the setup wizard and the device setup panel use the design
system the other pages use (tokens, shared buttons and notices, both
themes); a test fails if a legacy colour or shadcn class returns under
ui/src. The unusedIpBadgeandCardcomponents are removed. -
WAF_FAIL_OPENreaches the WAF as well as the proxy in both compose files,
as their comment said; the config contract check fails if a shared variable
is missing from either service. -
A new install stores a 2000 MB disk cache, the size Squid already used until
the first save; an install that saved 1000 keeps 1000.
Fixed
-
Signing out with unsaved Settings changes asks first ("Sign out and lose 3
unsaved changes?", naming the sections); before, the changes were dropped
without a word. -
A database reset now clears the cached
gdpr_mode, which kept its old
value for up to 30 seconds after the reset emptied the settings. -
Proxy login: a changed proxy password works at once. The backend writes
proxy_users.htpasswdwith the proxy group (gid 13) itself, also on the
file's first write, as a member of that group throughgroup_addand with
no capabilities added. Before, the file kept the backend's group until the
proxy's generator reset it on the next reload, and Squid answered407to
the new credentials in between.scripts/check-config-contract.shfails if
a compose file losesgroup_add, and the ACL matrix fails if the proxy
image'sproxyuser moves off gid 13. -
Accessibility: the password reveal button sits inside the field and keeps
one name ("Show password") with a pressed state; the expanded Search button's
name matches its visible text; the sidebar is anasidelandmark; the global
search is a combobox with a listbox,aria-activedescendantand a spoken
result count; tables keep a keyboard-focusable scroll box without repeating
the name of the section around them as a second region. -
A settings refetch that fails after the first load (after a save, say) no
longer replaces the whole UI with the "backend did not answer" screen and
its unsaved drafts; a sign-in screen or wizard chunk that fails to load
shows an error instead of a blank page. -
With
gdpr_modeon,GET /api/dashboard/summary(last_24h_clientsand
last_24h_internal_sources) counts distinct/24and/64networks via
privacy.NetworkPrefixinstead of distinct unmasked IP addresses, as
GET /api/clients/statisticsalready does (#459);Clients
labels the search box (Filter clients by network or IP address) and lifetime
detail note (for this network) for network rows, andLogsconverts masked
192.168.1.xand2001:db8:1:2::xaddresses to/24and/64CIDRs (Show this network's requests) instead of hiding the filter action. -
POST /api/maintenance/restore-configno longer addsrestored_egress_providers
intorestored_entries;Maintenancestates in the Backup description that
enabled egress providers and custom URLs are backed up without their downloaded
entries, lists egress providers in the pre-restore confirmation and post-restore
result (8 list entries and 1 egress provider), notes when provider lists are
downloading in the background (egress_providers_refreshing), and invalidates
the query cache after a restore;egress_provider_refresh_hoursuses its own
provider_cycleapply mode (StartEgressProviderRefreshsleeps for the full
interval even whenegress_default_denyis off), andChangePassworduses
sentence case on its field labels and submit button. -
First-run sign-in, setup wizard and client setup report what actually
happened:Logindistinguishes 401 (Invalid username or password), 429
rate-limit responses (showing the backend'sdetail), 5xx or unreachable
backend errors, and missing tokens, and adds a password reveal toggle (also
added to Confirm New Password in Settings);Appshows a retry notice instead
of skipping the setup wizard whenGET /api/settingsfails on initial load;
SetupWizardremoves the misleading devices step, describes every setup and
strictness option by the exact settings it enables without turning on
bandwidth limits oraggressive_caching_enabled, invalidates the settings
query cache after saving, and reports the realapplyToProxy()outcome
(applied,pending,refused,failed) referencingSettings, Admin and backup, Reload the proxy;POST /api/maintenance/reload-configanswers 500
(code: trigger_write_failed) instead of 200successwhen.reload-squid
cannot be written;ClientSetupawaitsnavigator.clipboard.writeTextbefore
reporting a copy, showsAuthentication: Username and passwordon iOS when
enable_proxy_authis on, and removes the duplicateConnect Your Devices
heading. -
gdpr_mode(Settings > Logs and privacy > Mask client IPs) masks client
addresses everywhere they leave the backend:GET /api/clients/statistics,
GET /api/clients/{ip}/details,GET /api/dashboard/summary(top_clients
andrecent_blocks),GET /api/logs,GET /api/logs/export,/api/ws/logs,
the MCP server and outbound WAF alert notifications (formatted messages, raw
webhooks and digests), instead of leaving full addresses in client statistics,
the dashboard and alerts; IPv6 addresses are masked to their/64prefix
(2001:db8:1:2::x) instead of only the last 16-bit group (#459). -
Squid serves distinct branded error pages for DNS lookup failures
(ERR_DNS_FAIL), refused or unreachable destinations (ERR_CONNECT_FAIL),
interrupted reads (ERR_READ_ERROR), socket creation failures
(ERR_SOCKET_FAILURE), unreachable ICAP security inspection when
WAF_FAIL_OPEN=0(ERR_ICAP_FAILURE), and every other Squid 7 error
template, instead of showing "Access Denied" for network errors or stock
Squid pages withERROR: loading filelines incache.log; the Access
Denied page shows the client IP address instead of"-"(#443). -
POST /api/database/resetclears the tables in one transaction and answers
500 naming the table that failed, instead of reporting success after errors;
its own audit row is written after the wipe, so it is the first row of the
new log. -
GET /api/docsis listed as requiring authentication, as it does. -
The installer's port check and its hint read
compose.prod.yml, the file
the production stack runs.