Changelog
Notable changes in each Rackd release
Release notes for each version of Rackd. Downloads are on the GitHub releases page.
0.3.1 - 2026-10-16
Changed
- CI’s race-detector job is several times faster: tests copy a database
migrated once (
internal/storage/storagetest) instead of migrating a new one each time, and the mDNS test no longer waits on every network interface.task releasenow waits for CI to pass on the release commit instead of rerunning the suite locally.
Fixed
- A discovery scan could make the whole server stop responding on slow or
network storage: every scan worker wrote the scan’s progress after each
host, and saved what it found, at the same time, which took every database
connection, so even
/api/v1/configtimed out with a 503. A scan now saves results from one goroutine and writes progress every two seconds, so it holds at most two connections.
0.3.0 - 2026-10-15
Changed
- The web UI’s menu is grouped into sections: Organization, Inventory, IPAM, DNS, Discovery, Integrations and Administration, with Dashboard above them. Sections collapse, and the browser remembers which. On a phone, only the current page’s section starts open, and a collapsed section shows its conflict count on the heading. Inside a section, labels are shorter: DNS Zones and DNS Providers are Zones and Providers, and Discovery is Scans.
- Extension menu items can name a
Section: a core section to join, or their own. Without one they go in an Extensions section.Ordernow sorts items within their section. An extension item is hidden when its path is a core page’s, or its label is a core item’s in the same section.
0.2.1 - 2026-10-15
Fixed
- Pages could take a minute to load on slow or network storage. Every API
request wrote the session’s new expiry, so a page load’s parallel requests
queued for SQLite’s write lock while holding database connections, and
reads waited behind them. The expiry is now written at most once a minute,
and SQLite commits with
synchronous=NORMAL(the recommended setting with WAL), so commits no longer wait for the disk to sync. A power cut can lose the last few transactions, but not corrupt the database. - On Linux, Rackd warns at start when the data directory is on a network filesystem (NFS, SMB/CIFS), where SQLite is slow and its locking unreliable.
0.2.0 - 2026-10-15
Upgrading from v0.1.0
- Global search (
/api/v1/search, the search box) now requiressearch:read. The built-in roles have it; add it to custom roles that should search. DISCOVERY_MAX_CONCURRENTandDISCOVERY_TIMEOUTare no longer read (they never did anything); remove them from your configuration.- Audit retention now applies. On the first start, audit entries older than
AUDIT_RETENTION_DAYS(default 90) are deleted. To keep your full history, setAUDIT_RETENTION_DAYS=0before upgrading, or export it first (rackd audit export). - Rackd now refuses to start if
ENCRYPTION_KEYis set but malformed. Before, it ran without encryption. The docs’RACKD_ENCRYPTION_KEYwas never read; useENCRYPTION_KEY. - On start, the database files are made readable only by Rackd’s user. If
another user (a backup agent, say) reads them directly, use
rackd backupinstead. rackd credentials rotate-key --new-keyis replaced byNEW_ENCRYPTION_KEYor--new-key-file, plus--yes.- MCP clients see error codes -32001 (unauthenticated), -32003 (forbidden), -32004 (not found) and -32009 (conflict), as well as -32602 and -32603.
- Login (
POST /api/v1/auth/login) now requiresContent-Type: application/json. Scripts that post JSON without that header get415. - Sessions now end 7 days after sign-in even if used every day; set
SESSION_MAX_LIFETIMEto change this, or0for the old behaviour. is_adminnow always matches the admin role. A migration sets it for users who hold the role and clears it for users who don’t, so users created withis_adminbut never given the role (and who never had admin rights) no longer show as admins. Grant them the admin role if they should be admins.- An
idsent when creating something, including theidcolumn of a CSV import, must now be a UUID. Imports of Rackd’s own exports are unaffected; clear hand-written ids such asdev-1and let Rackd assign them. - Upgrade the server before the CLI. The CLI now calls
/api/v1/..., which v0.1.0 servers do not serve; against one it fails with “the server did not answer like a rackd /api/v1 server”. - Scripts and integrations that call
/api/...keep working until the removal date, but should move to/api/v1/.... - This release adds a database migration, and v0.1.0 refuses to start on a database
migrated by a newer Rackd. To go back to v0.1.0, first run
rackd migrate rollback --to 20261013120000 --yeswith this release, or restore the pre-migration snapshot from<data-dir>/backups/. - Webhook requests use a new format; see Webhooks.
Only the test ping was ever sent before, so this matters only for receivers built
against it:
- Headers are
X-Rackd-Event,X-Rackd-Delivery,X-Rackd-TimestampandX-Rackd-Signatureinstead ofX-Event-Type,X-Delivery-IDandX-Signature-256. - The body is
{"event", "delivery_id", "timestamp", "data"}instead of{"id", "type", "timestamp", "payload"}. - The signature covers
<X-Rackd-Timestamp>.<body>instead of the body alone, so receivers can reject replayed requests.
- Headers are
- The
routelabel onhttp_request_duration_millisecondsis now the route pattern, such asGET /api/v1/devices/{id}, instead of the raw method and path. Update dashboards and alerts that filter on old label values. The duration metrics are now typed as gauges, since each value is a mean. /metricsnow needs the newmetrics:readpermission rather than just a login. The built-in roles have it. Give it to the role of the user your Prometheus scraper’s API key belongs to if that is a custom role.
Added
Every list endpoint accepts
limit(at most 1000) andoffset. Device lists, audit logs, logs and webhook deliveries return 100 results unlesslimitsays otherwise; other lists return everything when no page is asked for, as the API reference says.An extension API,
pkg/extension, and a public entry point,pkg/app, for building Rackd binaries with extra features compiled in. A feature declares its permissions, event types and UI entries in a manifest, and can add its own migrations (in their own namespace), REST routes under/api/v1, MCP tools, CLI commands and background work. It reads core data through interfaces that apply the caller’s permissions, and records its changes in the audit log. The API is unstable until 1.0. See Extensions.Extensions can add to the web UI. A feature embeds a JavaScript module and stylesheets, which the UI loads at startup. The module draws the feature’s pages and adds content to slots on the dashboard and on device, network and datacenter pages, through
window.rackd. Pages get their titles and permission checks from the manifest. A binary can also replace the built-in UI with its own (app.Options.UI).examples/hellois a complete example.rackd migrate statusshows each extension’s migrations, including extensions recorded in the database but not in the binary;migrate runapplies them; andmigrate rollback --namespace <name>reverts them. The status output now groups migrations under a heading per namespace and ends withTotal: N pendinginstead ofTotal: X migrations, N pending; update scripts that parse it.Event types are a registry instead of a fixed list, so extensions can add types that webhooks subscribe to.
/api/v1/configlists the extensions in the binary, with their pages and nav items, and reports the edition the binary was built with.tools/loadtest, a load tool that seeds a server (by default 10,000 devices with 100,000 addresses) and reports per-operation latency percentiles and errors under concurrent clients. See Performance Testing.The API is versioned under
/api/v1. Within v1, changes are backward compatible; breaking changes go into a new version. The web UI, the CLI and the OpenAPI spec use the new paths. See Versioning and Deprecation.
Changed
- The README no longer warns against production use. It states what this release
promises instead:
/api/v1stays backward compatible, upgrades back up the database first, and only the latest release gets security fixes. - The server’s SQLite database uses a pool of 8 connections instead of one, so
reads no longer queue behind each other or behind writes. Writes still run one at
a time: transactions start with
BEGIN IMMEDIATE, and a connection waits up to 5 seconds for the write lock. With the load tool’s defaults (10,000 devices, 50 clients), throughput doubled. - Device lists, searches and related-device lookups load addresses, tags and domains for all devices in three queries per 500 devices, instead of three per device. A page of 100 devices took about 300 queries. Under the load tool’s defaults the median page time fell from 3.1 s to 0.29 s.
- An API key’s
last_used_atis updated at most once a minute, instead of on every request, so it is accurate to about a minute. Each API-key request used to add a database write. - The dashboard computes network utilization with one grouped query instead of two
queries per network. A new migration (
20261014120000) replaces thenetwork_idindex onaddresseswith a(network_id, ip)index, so utilization counts read only the index. This also speeds upGET /api/v1/networks/{id}/utilization. - The domain types moved from
internal/modelto the publicpkg/model, so extensions can use the same types as core. They are unstable until 1.0.
Deprecated
- The unversioned
/api/...paths. They still work as aliases of/api/v1/...and are removed in the first release on or after 14 April 2027. Their responses carryDeprecation,SunsetandLink: rel="successor-version"headers. To migrate, insert/v1after/api.
Fixed
Reservations (found by a Strix scan):
- Auto-assigning a reservation (
POST /api/v1/reservationswithoutip_address) failed with “high contention” once the pool had any reservation, because the next free address ignored reservations. It now skips reserved addresses, and so doesGET /api/v1/pools/{id}/next-ip. - Expired reservations never expired. Their address is now free as soon as
the expiry passes, and the status changes to
expiredwithin a minute. Expiry times given with a UTC offset other than zero are compared correctly. - A device taking a reserved address left the reservation
active. It becomesclaimedwhen the device is created or updated with that address in the pool. - The reservations guide described a configuration block, webhook events and a conflict type that don’t exist; it now matches what Rackd does.
- Auto-assigning a reservation (
Concurrent writes could record the same thing twice (found by a Strix scan):
- Device writes for one address at the same moment each recorded an active
duplicate-IP conflict, so resolving one left the others. There can now be
only one active duplicate-IP conflict per address. On upgrade, extra
active records are resolved by
systemas duplicates, keeping the first detected. - A duplicate IP that came back after its conflict was resolved was never recorded again. It now gets a new conflict; ignored conflicts still stay ignored.
- Overlapping scans of a network each recorded the same host. A network now has one discovered-device record per address. On upgrade, duplicates are removed, keeping the promoted record or else the one seen last.
- Device writes for one address at the same moment each recorded an active
duplicate-IP conflict, so resolving one left the others. There can now be
only one active duplicate-IP conflict per address. On upgrade, extra
active records are resolved by
Bad input and references to missing records got a 500 instead of a 4xx, on the REST API and over MCP. Credentials, scan profiles and scheduled scans that fail validation, a missing
user_idorrole_idwhen granting a role, and a relationship to a device that doesn’t exist now answer 400; scanning or testing something that doesn’t exist answers 404; a zone import the DNS provider fails answers 502, like the provider test. Deleting a user that doesn’t exist answers 404 like every other resource, instead of 204.A scan profile created without
timeout_secormax_workerswas rejected; they default to 30 seconds and 10 workers. A scan profile now needs a name.A NUL byte in a search query caused a 500.
The web UI showed OAuth Clients in the menu when MCP OAuth was off, and the page failed with a 404.
/api/v1/confignow reportsmcp_oauth, and the page and its menu entry appear only when it’s on.The web UI hid the DNS zone Sync and Import buttons from operators, and the provider Test button from viewers and operators. They checked
dns-zone:updateanddns-provider:updateinstead of thesync,importandtestpermissions the server uses. The Circuits and NAT nav entries now show only to users who can list them.pool.utilization_highcould be subscribed to but never fired. It now fires when a pool’s utilization reachesPOOL_UTILIZATION_ALERT_PERCENT(default 90;0turns it off), checked at each snapshot, once per crossing.search:readwas granted but never checked; global search now requires it, as well as each kind’s list permission, and reports errors instead of returning empty results.dns:import, which nothing checked, is removed.The pool form’s Gateway and DNS Servers fields were silently discarded; they are removed.
Users, roles and API keys are timestamped in UTC like everything else.
DISCOVERY_MAX_CONCURRENTandDISCOVERY_TIMEOUTwere read but never used; they are removed. Discovery concurrency and timeouts are set in scan profiles.The CLI documentation listed flags and commands that don’t exist. Every command and flag is now in
docs/cli-reference.md, generated from the code (go run ./tools/clidoc), with a test that fails when it falls behind. The docs no longer mention settings Rackd doesn’t read (RACKD_*server variables,--api-auth-token,rackd mcp,rackd completion). The MCP guide describes connecting over HTTP, and exit codes are documented as0or1.rackd conflict resolvealways failed: it posted to/api/v1/conflicts/resolve, which doesn’t exist. It now uses/api/v1/conflicts/{id}/resolve. That route ignored its{id}and resolved whateverconflict_idthe body named; it now resolves the conflict in the path, and refuses a body naming another one.Reservations: create took only
expires_in_daysand update onlyexpires_at, sorackd reservation update --expiresdid nothing. Both now take either form (and refuse both at once).The CLI printed only “HTTP 400” for validation errors, because it couldn’t decode the
detailslist. It now shows the code, the message and each field.rackd device listreturned at most 100 devices, and--querywas ignored. It now fetches every page unless--limitis given, adds--offset, and searches with--query.Duplicate names and other unique fields now return
409 ALREADY_EXISTSeverywhere. Before, roles, circuits and user emails gave500, and scan profiles and scheduled scans gave400with the database’s message. Deleting an unknown OAuth client or conflict now returns404instead of500or204.Scheduled scans with an invalid cron expression, no name or no network are refused when saved, instead of being stored and failing silently in the scheduler. The documented 5-minute minimum interval is now enforced.
A reservation’s expiration can be removed (
clear_expiry, orrackd reservation update --expires 0).The API reference’s error section, and the OpenAPI conflict and reservation schemas, described fields and codes Rackd doesn’t use. They now match, as do
docs/conflicts.mdanddocs/reservations.md.The web UI showed only the first 100 devices: the device list, the relationship graph and the device pickers in NAT, DNS and pools. It now fetches every page.
MCP
device_save,network_saveanddatacenter_savereplaced the whole record on update, so{id, description}wiped a device’s addresses, tags and hostname. An update now changes only the fields passed. Addresses can carrynetwork_id,pool_idandport, and custom field errors are reported instead of ignored.Field validation (CIDRs, IP formats, VLAN range, hostnames, pool ranges, lengths) ran only in the REST handlers. MCP and bulk operations skipped it, and bulk device creation also skipped IP conflict detection and DNS sync. The services now validate every write, and bulk writes get the same side effects as single ones.
Security: writes to devices through other features were checked only against that feature’s permission. Promoting a discovered device now needs
devices:create. Resolving a duplicate-IP conflict, which takes the IP off other devices, needsdevices:update, and so does linking a DNS record that adds a domain to a device. Promoting now also runs device validation, conflict detection and DNS sync.Device list filters accept
tagsboth repeated (tags=a&tags=b) and comma-separated (tags=a,b). The web UI and CLI sent the second form, which matched nothing. New tags can’t contain a comma.Security: much of what an attacker would do left no audit trail. Sign-ins (successful, failed and refused), sign-outs, user changes, password changes and resets, role and permission changes, role assignments, API keys, credentials, webhooks, OAuth client registration, approval and deletion, circuits, NAT, custom fields, conflict resolution, scan profiles and scheduled scans are now audited. Entries for credentials, keys, webhooks and OAuth clients hold names and settings, never secrets or full webhook URLs.
A burst of changes bigger than the audit queue (1,000 entries) silently dropped audit entries while the changes themselves succeeded (found by a Strix scan). Entries are now written in batches, and when the queue is full the request waits for room instead of dropping its entry.
AUDIT_RETENTION_DAYSwas never applied, so audit entries were kept forever. They are now deleted once they are older than it, at start and daily.AUDIT_ENABLEDdid nothing, since changes were always audited; it is now documented as unused and logs a warning if set.Security: secrets and internal details were easier to reach than they should have been:
- The install guide and CLI reference told operators to set
RACKD_ENCRYPTION_KEYor--encryption-key, which Rackd never read, so following them ran without encryption. A malformedENCRYPTION_KEYalso only printed a warning. The docs now nameENCRYPTION_KEY, and Rackd refuses to start if it is set but isn’t 64 hex characters. - The database and its WAL files were created world-readable (
0644) in a0755directory, and audit exports were written0644. They are now0600, in a0700directory for new installs, and existing database files are tightened on start. - Webhook URLs, which can themselves be credentials (Slack and Teams incoming
webhooks,
?token=), were shown to every role that can read webhooks. Only those who can update webhooks now see them in full; others see the scheme and host. Delivery errors no longer repeat the URL, and a migration removes it from errors already stored. - The in-app log kept debug and trace entries whatever
LOG_LEVELwas set to. It now keeps what the level shows, and hides sensitive values in logged query strings. - MCP tools, the OAuth token endpoint and
/readyzreturned raw internal errors, including database messages. MCP errors now say forbidden, not found, invalid or conflict, with the service’s message, and anything else is logged and reported as an internal error. - HSTS was only sent when Rackd itself served TLS, which it never does. It is
now sent whenever
COOKIE_SECUREis on (the default), withoutincludeSubDomains, so it doesn’t affect other sites on the same domain. rackd credentials rotate-keytook the new key as a command-line argument, visible in the process list, and updated rows one at a time. It now reads the key fromNEW_ENCRYPTION_KEYor--new-key-file, needs--yes, and rotates in one transaction that changes nothing if the current key is wrong.
- The install guide and CLI reference told operators to set
Security: sessions and OAuth tokens could outlive revocation, and sign-in could be abused:
- Sessions never re-checked their user, and the refresh on each request was an upsert, so a request in flight could bring back a session that a password reset or deactivation had just deleted. Refresh now only extends existing sessions, and every request rejects a session whose user is missing or inactive.
- Sessions had no absolute lifetime. They now end 7 days after sign-in
(
SESSION_MAX_LIFETIME). - When a reused OAuth refresh token was detected, only the tokens issued directly from it were revoked, so a thief who refreshed first kept a working refresh token. Revocation now covers every descendant. Two refreshes of the same token at once no longer both succeed.
- A password change or reset now revokes the user’s OAuth tokens.
- A cross-site form could sign a visitor into another account (login CSRF).
Login now requires
Content-Type: application/json. - The login page’s
?redirect=could send users to another site with a path like/.//evil.example. - Guessing from many addresses got around the per-address login limit. Failed
sign-ins are now also limited per account (
LOGIN_ACCOUNT_MAX_FAILURES,LOGIN_ACCOUNT_WINDOW). - Login, OAuth, API key, user and bulk routes shared one rate-limit bucket per address, so failed logins could block token refreshes. Each now has its own.
- The OAuth consent page now shows where an approval sends access, and warns that the application’s name can’t be verified.
Security: several ways to overload the server or reach places it shouldn’t:
- Overlapping-subnet detection compared every pair of networks and listed every recorded conflict once per new one. An operator could create thousands of identical networks and exhaust the server’s memory. It now sweeps sorted networks, stops when the request is cancelled, records at most 1,000 pairs per run, and reads the recorded conflicts once.
/mcpread request bodies of any size; it now has the same 1 MB limit as the API.- Discovery scans could target loopback, link-local (including the cloud
metadata address) and other non-host ranges, which probed the server itself.
Those subnets are now refused, and at most 4 scans run at once
(
429 TOO_MANY_SCANS). A subnet that is too large is now a400instead of a500. - Changing a DNS provider’s endpoint kept its stored token and sent it to the new host on the next sync or test. The token must now be entered again.
- Outbound requests (webhooks, DNS providers) now apply the IPv4 rules to NAT64, 6to4 and Teredo addresses. They block reserved IPv4 space, and check the target before handing a request to an environment proxy. DNS provider responses are capped at 32 MB.
- Anonymous OAuth client registration limits field lengths and the number of redirect URIs.
Security: delegated user and role management could be escalated to full admin:
- The “only admins can manage admins” checks read the
is_admincolumn, which stayed false for admins promoted by granting the admin role. So a user withusers:updatecould reset such an admin’s password and sign in as them. roles:updatecould grant any permission, including to the caller’s own role.users:createcould give a new user any role except the one namedadmin.
Callers who aren’t admins can now only grant, revoke or assign permissions they hold. They can only act on users whose permissions they all hold. The last active admin can’t be removed. See RBAC.
- The “only admins can manage admins” checks read the
Security: an operator could make an admin’s browser delete or rename users, API keys, roles and other records. Creating a device, network, datacenter, pool, scan profile or scheduled scan kept any
idthe client sent, and the web UI put IDs into request paths unescaped. So a device with the id../users/<admin-id>sentDELETE /api/v1/users/<admin-id>when an admin deleted it from the list.- Client-supplied IDs on create must now be UUIDs.
- The web UI escapes every value it puts in a request path, and refuses
requests whose path contains
.or..segments, so IDs already stored are harmless too. - CSV exports neutralise formulas in ID columns.
Bulk network creation (
POST /networks/bulk) now validates each network like single creation does, instead of storing empty names and invalid subnets.The web UI’s notifications (such as “Permissions refreshed successfully” and permission-denied messages) never appeared: the notification area showed a separate, always-empty list.
Lists were cut off at 100 records with no way to page, although the API reference said they returned everything. Among the effects:
- The API and web UI showed only the first 100 networks, datacenters, users, webhooks, circuits, NAT mappings, reservations, DNS records and others.
- Rackd has 118 permissions, so the last 18 could not be assigned to a role (“Unknown permission ID”), were missing from the role editor, and could not be requested as OAuth scopes.
- Audit log exports held at most 100 entries.
- The Prometheus
devices_total,networks_totalanddatacenters_totalmetrics, dashboard utilization and utilization snapshots covered at most 100 records.
Without an encryption key (so without the DNS service), the MCP server still offered the DNS tools, and calling one crashed the request with a nil pointer. The DNS tools, and their mention in the server instructions, now appear only when DNS is available.
The utilization snapshot worker was never started, so the dashboard’s trend chart had no data. It now runs with the server: a snapshot of every network and pool on start, then every
SNAPSHOT_INTERVAL(default 1h;0disables it), with snapshots older thanSNAPSHOT_RETENTION_DAYS(default 90) removed.Creating or updating a device never checked for duplicate IPs, although the conflicts guide said it did: the check was never connected. It now runs, and looks up the devices holding each IP with one indexed query. The old check, had it run, compared only against the first 100 devices by name. The conflicts guide now says which detection is automatic and which needs an explicit run.
Webhooks never delivered events: nothing published them, and the delivery worker was never started. Device, network, discovery and conflict changes now publish the documented events (bulk operations publish one per item), and the worker runs with the server. Deliveries retry with the same delivery ID, and those interrupted by a shutdown are retried after the next start.
pool.utilization_highis not published yet.Webhook delivery records stored a response code of 200 for every success; they now store the actual code. The test ping sent an empty event ID and timestamp.
Bulk tag add and remove counted a device whose tags failed as both failed and succeeded.
The webhooks guide described fields, per-webhook retry settings and a retry schedule that don’t exist, said private networks were allowed by default, and suggested sending events straight to Slack, which rejects them.
The server never closed its storage on shutdown, so audit entries still queued were lost. It now finishes in-flight requests, stops background work and closes the storage, flushing the queue.
Request metrics were labelled with the raw path and method, so every device ID, and every random path or method from any client, added a series that was kept for the life of the process. Memory and
/metricsoutput grew without bound. Theroutelabel is now the matched route pattern (such asGET /api/v1/devices/{id}). Requests that match no route shareroute="unmatched", and requests answered before routing (for example by the rate limiter) shareroute="unrouted". Label values in/metricsare escaped.Duration metrics were stored in whole milliseconds, so sub-millisecond requests counted as 0 and averages read low. They are now kept in nanoseconds and exported as fractional milliseconds.
The monitoring guide’s p95 panel used
histogram_quantileon a metric that is an average, not a histogram. It now shows the average per route.Requests to an unknown API path fell through to the web UI: a GET returned
index.htmlwith status 200, and other methods a plain-text 405. They now return a JSON 404, or a JSON 405 with anAllowheader when the path exists but the method is wrong.The Nomad job’s health check probed
/api/datacenters, which requires authentication, so the service never became healthy. It now uses/healthz.Any signed-in user, even one with no roles, could read
/metrics, including inventory counts. It now needsmetrics:read. The monitoring guide said the endpoint needed no authentication; it shows the scraper an API key now.A webhook ping that didn’t reach the receiver answered 200 “Webhook ping sent successfully”. It now answers 502
WEBHOOK_UNREACHABLEwith the cause, and the web UI and CLI report it as a failure.An unparsable
start_timeorend_timeon the audit and log list and export endpoints was ignored, so the filter quietly returned everything. It is now a 400, as is anend_timebeforestart_time.
0.1.0 - 2026-10-14
Upgrading from v0.0.21
- The first start writes a verified snapshot of the database to
<data-dir>/backups/before migrating, and up to five are kept. If that directory cannot be written (a full disk or read-only storage), the server does not start until you setPRE_MIGRATION_BACKUP=false. rackd backup --outputno longer overwrites an existing file. Scripts that write to a fixed path need--overwrite.rackd backupnow reads the database through SQLite instead of copying the file. While the server is stopped, the user running it needs write access to the data directory, because SQLite creates a-shmfile there. Read access torackd.dbalone is no longer enough.- Migration history is now recorded in
xdal_migrations.schema_migrationsis kept but no longer updated, so anything that reads the schema version from it sees v0.0.21’s history. - Creating a DNS provider of type
powerdnsorbindnow returns 400. These types never worked; existing ones can still be edited and deleted. - v0.0.21 can still open an upgraded database. To go back completely, restore the
pre-migration snapshot with
rackd backup restorebefore reinstalling v0.0.21. - Releases now also publish
*.sbom.jsonand*.sigstore.jsonfiles next to each archive. Download scripts that match assets by platform name alone may need a tighter pattern, such as*_linux_amd64.tar.gz.
Added
Signed releases.
checksums.txtand each SBOM get a keyless cosign signature bundle (*.sigstore.json), using the release workflow’s GitHub identity.- Each archive gets an SPDX SBOM (
*.sbom.json) generated by syft. - The release carries a GitHub build provenance attestation.
- The signing permissions are scoped to the release job, and every action in it is pinned to a commit SHA.
- Verifying a release shows the
cosign verify-blobandgh attestation verifycommands.
SECURITY.md: supported versions, private reporting, and disclosure.CI runs
govulncheck. Dependabot proposes weekly updates for Go modules, the web UI (Bun), GitHub Actions and the Docker base images, with minor and patch updates grouped.The
rackd dnsCLI command group (providers, zones, sync, import, records) is now available. It existed but was never registered.CONTRIBUTING.md, and a Contributor License Agreement (CLA.md) that a GitHub workflow asks first-time contributors to sign on their pull request.Automatic backup before migrations. When the server starts or
rackd migrate runruns with pending migrations on an existing database, a verified snapshot is written to<data-dir>/backups/first. The five newest are kept. If the snapshot fails, no migration runs. SetPRE_MIGRATION_BACKUP=falseto skip the snapshot, for example on a full disk.rackd migrate rollback --steps N/--to VERSIONreverts migrations after taking the same snapshot. It requires--yes, because some Down steps drop data. The target is validated first, and nothing is written when there is nothing to revert.rackd backup verify --file Fchecks a backup’s integrity and migration history, and warns when it was made by a newer Rackd.rackd backup restore --file F --yesreplaces the database with a verified backup. The backup is first consolidated into a single file, so a backup copied together with its-walfile keeps all its data. The restored database is left in WAL mode. The current database is kept asrackd.db.pre-restore-<timestamp>. The command refuses to run while the server has the database open, and refuses backups from a newer Rackd.rackd migrate run --no-backupskips the pre-migration snapshot.rackd backup --overwritereplaces an existing output file, for scripts that write to a fixed path. The new backup is written and verified next to it first, so a failed backup leaves the previous file in place.A CI workflow for pull requests and pushes to
main:- Go: formatting,
go mod tidy, vet, golangci-lint and the full test suite. - A separate job runs the tests with the race detector.
- Web UI: typecheck, the OpenAPI type check and unit tests.
- Go: formatting,
Upgrade tests against a real v0.0.21 database. A reversibility test applies each migration, reverts it, and checks that the schema and row counts match the state before it ran.
Changed
- Licence: Rackd is now licensed under the Apache License 2.0, which adds an
explicit patent grant; see
LICENSEandNOTICE. Releases up to and including v0.0.21 remain available under the MIT License. - Creating or updating a DNS provider now accepts only
technitium. Thepowerdnsandbindtypes were accepted before but failed with “not yet implemented” whenever the provider was used. They are planned, and the API now says so. Providers already stored with those types can still be edited and deleted. - Migrations run on xdal
pkg/migrate(v0.1.0). This is groundwork for PostgreSQL and MySQL support.- Applied migrations are recorded in
xdal_migrationsunder thecorenamespace. - On first start, the existing history is copied from
schema_migrations, once: a marker row stops a later full rollback from triggering the copy again. The old table is left in place so an older binary can still open the database.
- Applied migrations are recorded in
rackd migrate statusalso reports dirty migrations, and migrations applied by a newer Rackd. It never writes to the database; before the first upgrade it reads the oldschema_migrationstable directly.- A database migrated by a newer Rackd is refused at startup with a clear error, instead of being run against an older schema definition.
- Per-migration log lines are now at debug level. One summary line is logged at info.
- The
scan_profiles,scheduled_scansandcredentialstables are now created by a versioned migration (20261013120000) instead of when their stores start.- Existing tables and data are kept.
- The default scan profiles are seeded once. A deleted default profile no longer comes back on the next restart.
- Opening one of these stores before migrations have run now returns a clear error.
Removed
- The stale
webui/package-lock.json.bun.lockis the web UI’s only lockfile.
Security
- Build with Go 1.27.2, which fixes nine standard library vulnerabilities that
rackd’s code reaches. These include HTTP/2 server crash, CPU and memory
exhaustion issues in
net/http, HTTP/1 connection desynchronisation after CONNECT, MIME header memory limits, and malformed ECH handling incrypto/tls(GO-2026-6603, 6605, 6607, 6608, 6610, 6611, 6612, 6613 and 6617). v0.0.21 was built with Go 1.27.1 and is affected.
Fixed
rackd backupcopied the live database file and its WAL file separately, so a backup taken while the server was writing could be inconsistent. It now uses SQLite’sVACUUM INTO, which produces one self-contained file. The backup is verified, written with mode0600, and does not overwrite an existing file unless--overwriteis given.- Rolling back a migration now fully reverses it. Previously these Down steps left
changes behind:
- Device
hostname, the device status columns, relationshipnotesand DNS recordaddress_idstayed in place. - The
api_keysrebuild lost its primary key, NOT NULL and UNIQUE constraints. - The DNS rollback left seven permissions behind.
- The audit/logs rollback left the operator’s
audit:listgrant behind.
- Device
- Discovery’s ARP table was read and replaced by concurrent scans and the startup load without locking, a data race on Linux. It is now swapped in and read under a lock.
- Logging before the logger was initialised (for example from CLI commands and tests) panicked. Messages are now discarded until initialisation.
- The README listed Cloudflare, Route53 and PowerDNS as DNS providers, and custom fields on networks. Only Technitium and custom fields on devices exist. The DNS guide and the architecture page no longer describe PowerDNS, BIND or a feature interface as available.
docker buildfailed because the Dockerfile installed Bun from Alpine’s edge repository, which no longer packages it. The web UI is now built in anoven/bunstage, a.dockerignorekeeps local data and build output out of the image, and CI builds the image on every pull request.- The runtime image is pinned to
alpine:3.24. - Pass
--build-arg VERSION=... --build-arg COMMIT=...(or setRACKD_VERSIONandRACKD_COMMITfordocker compose) sorackd versionreports the build.
- The runtime image is pinned to
- The Docker image and
docker-compose.ymlhealth checks probed/api/datacenters, which requires authentication, so the container never became healthy. They now use/healthz. - The README and the installation guide downloaded bare binaries such as
rackd-linux-amd64, which releases do not publish, so every download link returned 404. They now download the versioned archives. - The installation guide also referred to a Docker Hub image that does not exist,
RACKD_-prefixed environment variables, Go 1.25 and Make. It now builds the image locally, uses the real variable names, and requires Go 1.27 and Task. - The backup guide referred to
rackd server --dry-run,--init-dband/api/health, which do not exist. It now uses the real commands and/healthz//readyz.
0.0.21 - 2026-10-13
Added
- This changelog. Each release’s section is published as its GitHub release notes and on the documentation site.
Changed
- RBAC permission checks now use the go-rbac engine (v0.1.0). Role and permission definitions are kept in memory and reloaded whenever a role or permission changes; permissions still match exactly (wildcard grants are disabled).
- The permissions reported for a user (UI configuration, OAuth scope checks) are now derived from the same source as enforcement, so they always agree.
- DNS provider (Technitium) calls and webhook test pings now time out after 20s instead of 30s, so failures are reported before the request timeout.
- The network topology graph uses Cytoscape’s default scroll-zoom sensitivity (removes the “custom wheel sensitivity” console warning).
- Updated Go dependencies (including
paularlott/mcpv0.32.4,paularlott/cliv0.10.1,modernc.org/sqlitev1.60.1,golang.org/x/cryptov0.58.0) and web UI dependencies (Alpine.js 3.17.4, Cytoscape 3.34.3, Playwright 1.64.0). Runbun run test:e2e:installto fetch the matching Playwright browser.
Fixed
- Requests running longer than 15s received an empty reply because the HTTP server
write timeout was shorter than
REQUEST_TIMEOUT. The write timeout now outlasts the request timeout (REQUEST_TIMEOUT+ 5s). - DNS provider “Test Connection” failures against unreachable servers are shown
again (502
PROVIDER_UNREACHABLEwith the cause) instead of hanging. - OAuth client registration without a
scopereturns 400invalid_client_metadatainstead of a 500 server error. - Approving an OAuth authorization whose requested scopes are not permitted for the
user returns
invalid_scopeinstead ofserver_error. - Deleting a permission now revokes it immediately rather than at the next role change or restart.
Removed
- Internal
HasPermission,HasAnyPermissionandHasAllPermissionshelpers, replaced by go-rbac.
0.0.20 - 2026-09-19
Changed
- Build tooling moved from a Makefile to a Taskfile.
- Go bumped to 1.27.1 and dependencies updated.
- Tests can lower the bcrypt cost via
RACKD_BCRYPT_COSTfor faster runs.
Fixed
- Resolved all golangci-lint findings.