Release: backup restore, portable secret key, multi-arch image, CSRF, About dialog #14

Merged
gsadmin merged 4 commits from development into main 2026-09-04 00:56:17 +00:00
Owner

Summary

Four commits since #13.

Backup restore — was a stub, now real

Every repository shares one connection pool, so the database cannot be swapped underneath a live server. Restore is two-phase: RestoreBackup validates and stages the file beside the database; db.New applies it before the pool is opened, the only safe moment.

  • The replaced database is preserved as <db>.replaced-<timestamp>.
  • Stale -wal/-shm are removed so SQLite cannot replay the old journal over the restored file.
  • Validation is strict (integrity_check + schema probe) — applying an unrelated file would destroy the install.
  • GET/DELETE /api/v1/backups/restore inspect and cancel a staged restore.
  • The CLI does both phases at once. orchestrad backup was also a stub and now works.

Portable secret key

With nothing configured the key is generated once and persisted to <data>/secret.key (owner-only; ACL-restricted on Windows), so restarts reuse it and moving the stack to another server is a matter of copying the data directory.

Upgrades are handled deliberately: the MSI never set ORCHESTRAD_SECRET_KEY, so existing installs have been running on the legacy built-in default. If a database already exists, that value is adopted and written out rather than replaced — generating a fresh key there would make every stored credential undecryptable. Startup logs where the key came from and warns to rotate the legacy one.

Multi-arch container image

buildx now emits linux/amd64 + linux/arm64, matching the architectures the release binaries already covered. The Dockerfile cross-compiles via TARGETARCH instead of emulating, so arm64 costs little. This is the one change that can only prove itself on a real CI run — it restructured build+push into a single buildx --push, so watch that job.

Real CSRF

The middleware previously checked only that a header was present, and was never wired up; /auth/csrf returned csrf-token-placeholder. Tokens are now nonce + HMAC-SHA256 signed with the application secret, properly validated, and the middleware is mounted on /api/v1. Bearer and API-key requests are not CSRF-reachable and pass through untouched, so this is transparent to the SPA and to API clients.

TLS

  • The serving leaf is published to LocalMachine\My with its private key, and renewal replaces it rather than accumulating one cert per renewal. Only certificates issued by our own CA to our own subject are ever pruned.
  • Fixed a real handle leak: ensureWindowsStore returned mid-enumeration without freeing the matched CertContext, which also leaked the store handle (CertCloseStore(store, 0) defers until outstanding contexts are released).
  • Ensure/Reload are now serialized — reachable concurrently from the renewal loop and the settings handler, where a racing pair could prune the leaf the other just installed.
  • Store import drops CRYPT_EXPORTABLE (the store copy is not the source of truth), and the PFX password is written beside the bundle.

UI

The notifications bell rendered hard-coded template data ("Roman Joined the Team!") with no backing feature. Replaced with an About dialog showing the running server's version, build time, and commit, with a copy button. Three other unreferenced template files went with it.

Verification

Full backend suite green; Linux cross-build clean. Verified live against the demo instance and the lab DC: backup → stage → restart applies and preserves the previous database; secret key generated, adopted, and read back across restarts with the credential check confirming decryptability; leaf published to the store and replaced on renewal; CSRF issues real signed tokens; version endpoint renders release values.

Merging this cuts a release (binaries, container image, MSI).

## Summary Four commits since #13. ### Backup restore — was a stub, now real Every repository shares one connection pool, so the database cannot be swapped underneath a live server. Restore is two-phase: `RestoreBackup` validates and stages the file beside the database; `db.New` applies it before the pool is opened, the only safe moment. - The replaced database is preserved as `<db>.replaced-<timestamp>`. - Stale `-wal`/`-shm` are removed so SQLite cannot replay the old journal over the restored file. - Validation is strict (`integrity_check` + schema probe) — applying an unrelated file would destroy the install. - `GET`/`DELETE /api/v1/backups/restore` inspect and cancel a staged restore. - The CLI does both phases at once. `orchestrad backup` was also a stub and now works. ### Portable secret key With nothing configured the key is generated once and persisted to `<data>/secret.key` (owner-only; ACL-restricted on Windows), so restarts reuse it and **moving the stack to another server is a matter of copying the data directory**. Upgrades are handled deliberately: the MSI never set `ORCHESTRAD_SECRET_KEY`, so existing installs have been running on the legacy built-in default. If a database already exists, that value is **adopted and written out** rather than replaced — generating a fresh key there would make every stored credential undecryptable. Startup logs where the key came from and warns to rotate the legacy one. ### Multi-arch container image `buildx` now emits `linux/amd64` + `linux/arm64`, matching the architectures the release binaries already covered. The Dockerfile cross-compiles via `TARGETARCH` instead of emulating, so arm64 costs little. **This is the one change that can only prove itself on a real CI run** — it restructured build+push into a single `buildx --push`, so watch that job. ### Real CSRF The middleware previously checked only that a header was *present*, and was never wired up; `/auth/csrf` returned `csrf-token-placeholder`. Tokens are now nonce + HMAC-SHA256 signed with the application secret, properly validated, and the middleware is mounted on `/api/v1`. Bearer and API-key requests are not CSRF-reachable and pass through untouched, so this is transparent to the SPA and to API clients. ### TLS - The serving leaf is published to `LocalMachine\My` with its private key, and **renewal replaces it** rather than accumulating one cert per renewal. Only certificates issued by our own CA to our own subject are ever pruned. - Fixed a real handle leak: `ensureWindowsStore` returned mid-enumeration without freeing the matched `CertContext`, which also leaked the store handle (`CertCloseStore(store, 0)` defers until outstanding contexts are released). - `Ensure`/`Reload` are now serialized — reachable concurrently from the renewal loop and the settings handler, where a racing pair could prune the leaf the other just installed. - Store import drops `CRYPT_EXPORTABLE` (the store copy is not the source of truth), and the PFX password is written beside the bundle. ### UI The notifications bell rendered hard-coded template data ("Roman Joined the Team!") with no backing feature. Replaced with an **About dialog** showing the running server's version, build time, and commit, with a copy button. Three other unreferenced template files went with it. ### Verification Full backend suite green; Linux cross-build clean. Verified live against the demo instance and the lab DC: backup → stage → restart applies and preserves the previous database; secret key generated, adopted, and read back across restarts with the credential check confirming decryptability; leaf published to the store and replaced on renewal; CSRF issues real signed tokens; version endpoint renders release values. ### Merging this cuts a release (binaries, container image, MSI).
gsadmin added 4 commits 2026-09-04 00:56:06 +00:00
Clients that resolve a certificate by store lookup rather than by reading our
PEM files had nothing to find: in auto mode the leaf lived only under
<data>/tls, and the My store was opened strictly read-only.

InstallLeafToMyStore imports the exported PKCS#12 into LocalMachine\My with
the private key persisted to the machine keyset (CNG KSP, matching the
ncryptSigner path used when serving *from* the store). Renewal is accounted
for: the new leaf is added with REPLACE_EXISTING, then pruneSupersededLeaves
removes any certificate sharing its subject *and* issuer, so the store holds
exactly one current leaf instead of one per renewal. Only certificates issued
by our own CA to our own subject are ever deleted - anything from another
issuer is left strictly alone. Best-effort: it needs admin rights and TLS
serving does not depend on it.

RemoveLeafFromMyStore runs on service removal, alongside the firewall rule,
so uninstalling leaves no orphaned certificate.

Also fixes a genuine leak found while auditing this code, in response to a
question about whether listing the store could damage it (it cannot - the
listing handle is read-only and stores are not exclusively locked):
ensureWindowsStore returned from inside the enumeration without freeing the
matched CertContext. CertEnumCertificatesInStore frees the previous context
each call and the last on completion, so only the early-return paths leaked -
and because CertCloseStore(store, 0) defers until outstanding contexts are
released, the store handle leaked with it, once per certificate load.

Verified on Windows 11: leaf appears in LocalMachine\My with a usable private
key and full SANs; forcing a re-issue replaces it (one cert, new thumbprint,
old one pruned) and leaves unrelated certificates untouched; HTTPS keeps
serving throughout. Cross-compiles for linux via the no-op stubs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ensure is reachable concurrently from the renewal loop (Manager.Start) and
the TLS settings handler (Reload on a config change). Nothing serialized
them, so an overlapping pair could issue leaves, rewrite <dir>, and mutate
the host certificate store underneath each other — and since the store prune
removes any superseded leaf, a racing pair could delete the very certificate
the other just installed. Guard the whole operation with a mutex.

The file-rewrite half of this race predates the store work; publishing to the
certificate store is what made the consequence bad enough to matter.

Also drops an unused parameter from the freeEnum helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses the gaps identified in the last audit.

Restore (was a stub returning "not yet implemented"). Every repository shares
one connection pool, so the database cannot be swapped underneath a live
server. Restore is therefore two-phase: RestoreBackup validates the file and
stages it beside the database; db.New applies it before the pool is opened,
which is the only safe moment. The database being replaced is preserved as
<db>.replaced-<timestamp>, and stale -wal/-shm are removed so SQLite cannot
replay the old journal over the restored file. Validation is strict — SQLite
integrity_check plus a schema probe — because applying an unrelated file
would destroy the install. GET/DELETE /api/v1/backups/restore inspect and
cancel a staged restore. The CLI does both phases at once, since it runs
standalone; `orchestrad backup` was also a stub and now works.

Secret key. With nothing configured the key is generated once and persisted
to <data>/secret.key, so restarts reuse it and moving the stack to another
server is a matter of copying the data directory. Upgrades are handled: if a
database already exists the install was silently running on the legacy
built-in default, so that value is adopted and written out rather than
replaced — generating a fresh key there would make every stored credential
undecryptable. The file is owner-only (ACL-restricted on Windows).

Multi-arch image: buildx now emits linux/amd64 + linux/arm64, matching the
architectures the release binaries already covered. The Dockerfile
cross-compiles via TARGETARCH rather than emulating, so arm64 costs little.

CSRF: the middleware previously checked only that a header was *present* and
was never wired up, and /auth/csrf returned "csrf-token-placeholder". Tokens
are now nonce + HMAC-SHA256 signed with the application secret, validated
properly, and the middleware is mounted on /api/v1. Bearer and API-key
requests are not CSRF-reachable and pass through untouched, so this is
transparent to the SPA and to API clients.

Also: the Windows store import drops CRYPT_EXPORTABLE (the store copy is not
the source of truth — <data>/tls holds the key, so portability is unaffected
and a non-exportable server key is the better posture), the PFX password is
written to server.pfx.password beside the bundle so an operator importing it
by hand does not have to hunt for a password they never chose, and the
"renewed" log line now reflects whether a leaf was actually issued instead of
guessing from its age.

Verified live: backup -> stage -> restart applies and preserves the previous
database; secret key generated, adopted, and read back across restarts with
the credential check confirming decryptability; CSRF endpoint issues real
signed tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bell rendered hard-coded template data ("Roman Joined the Team!",
"New Payment received") — a demo artifact that had nothing to do with the
product and no backing feature.

In its place, an About dialog reports the running server's build: version,
build time, and commit, read from GET /api/v1/version, with a copy button so
the exact build can be quoted in a bug report without shelling onto the host.
That probe predates the {success,data} envelope and returns bare snake_case
JSON, so it is fetched directly rather than through the api client, accepting
either shape in case it is ever normalised. Dev builds ("dev"/"unknown")
degrade to a readable "—" rather than an invalid date.

Removing the bell also stranded three other unreferenced template files, so
Notification.tsx, AppLinks.tsx, QuickLinks.tsx and their shared data.ts (fake
users, chat/ecommerce app links) are deleted with it.

Also fixes the account button announcing itself to screen readers as
"show 11 new notifications".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gsadmin merged commit 901692ead7 into main 2026-09-04 00:56:17 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gsadmin/OrchestrAD#14