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>
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>
Stored credential secrets are encrypted with a key derived from
ORCHESTRAD_SECRET_KEY. When that value changes, the secrets are intact but
unreadable, and the only symptom was an opaque "decryption failed" surfacing
deep inside an unrelated operation:
"preview failed: building LDAP client: failed to decrypt credential:
decryption failed"
Nothing pointed at the real cause, so the error is now self-diagnosing:
- ErrDecryptionFailed states that the data was encrypted under a different
ORCHESTRAD_SECRET_KEY (or is corrupted). GCM auth failure on a well-formed
ciphertext is overwhelmingly a wrong-key case.
- The three credential decrypt sites name the credential, so the operator
knows which password to restore or re-enter.
- New services.CheckSecretKey verifies every stored secret against the
current key. It runs at startup (LogSecretKeyCheck) and in `doctor`, so a
mismatched key is reported once, loudly, at the moment it is first used
rather than during the next rule run. A correctly-sized but *different*
key passed doctor's existing length check and still broke every bind.
Not fatal: the server still starts, since an operator may be mid-migration
or may intend to re-enter the secrets.
Verified on the demo instance: starting with a wrong key logs
"1 of 1 stored credential secret(s) CANNOT be decrypted ... [OrchestrAD]",
and the rule preview error now names both the credential and the key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Toggling LDAPS in the connection form now auto-sets the port (636 on / 389
off when it's still the other default) and enables "Allow invalid certs",
because a non-domain-joined host usually can't validate the DC's LDAPS
certificate. This fixes the common "existing connection was forcibly
closed" seen when LDAPS was enabled while the port stayed 389 (dialing TLS
to the plaintext port). Helper text explains the traffic is still encrypted.
- LDAP client sets tls ServerName to the host so verification succeeds when a
DC cert IS trusted (ignored under InsecureSkipVerify).
- Backups retention reduced from 10 to 3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Firewall:
- On service initialize/install, create an idempotent inbound allow rule
("OrchestrAD") for the configured listen port, scoped to RFC 1918 private
ranges plus CGNAT (10/8, 172.16/12, 192.168/16, 100.64/10). The rule is
deleted-then-added so it always reflects the current port, and removed on
service uninstall. Best-effort (needs admin; the MSI custom action and
service run elevated); no-op off Windows. Verified the netsh rule lands
with the expected port and remote-address scoping.
MSI:
- Skip the license/EULA page (Welcome now goes straight to the install
directory), since it was blank. A standard short notice is kept in
license.rtf only so the stock license control resolves at build time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Database maintenance:
- New MaintenanceService prunes rule_runs (+ their action detail) and
audit_events older than their retention windows, then VACUUMs to reclaim
space. Runs once at startup and then on an interval, bound to the run
context. Configurable via ORCHESTRAD_RUN_RETENTION_DAYS (90),
ORCHESTRAD_AUDIT_RETENTION_DAYS (180),
ORCHESTRAD_MAINTENANCE_INTERVAL_HOURS (24), ORCHESTRAD_MAINTENANCE_VACUUM.
This stops the database growing forever. (Log rotation already existed via
lumberjack: ORCHESTRAD_LOG_MAX_SIZE_MB/_MAX_BACKUPS/_MAX_AGE_DAYS.)
Config import:
- Import now accepts either the wrapped {payload,dryRun} shape or a bare
exported config object, so a file downloaded from Export re-imports
directly (dryRun via ?dryRun=true) — useful for automation.
Test covers retention pruning with FK-cascaded action rows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Proxy / base-URL:
- ORCHESTRAD_TRUSTED_PROXIES now defaults to "local", trusting reverse
proxies in loopback + RFC1918 + link-local/ULA ranges out of the box, so
X-Forwarded-* (client IP, scheme, host) is honored behind an edge proxy
without extra config. New keywords: local/private, all/any, none.
- OIDC redirect URI derivation now uses the trust-gated request base URL
instead of reading X-Forwarded-Proto directly, and audit client IP now
trusts the middleware-rewritten RemoteAddr rather than the raw (spoofable)
X-Forwarded-For header. Both honor forwarded values only from trusted
peers.
Schedules:
- Seed eight built-in schedules on startup (every 5/15/30 min, hourly,
every 6/12h, daily, weekly), idempotent by name, so operators have
ready-made cadences in the Schedules page and the rule editor's schedule
dropdown without hand-building one.
Test covers the trusted-proxy keyword expansion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turn the rule_run_actions ledger into operator intelligence. A new
ActivityService rolls the ledger up by time window (24h / 7d / all-time)
into syncs, operations and removals with success/failure counts and the
number of distinct objects affected, plus all-time totals per action type
and a most-active-rules ranking. A companion filtered, paginated feed
answers the operational questions directly: what action ran, what
happened, to which object, triggered by whom, and when.
Backend:
- ActivityService.Summary() and ListActions(filter) over the joined
rule_run_actions / rule_runs / rules tables, categorising each action
type as Sync (AddToGroup/AddGroupToGroup), Removal
(RemoveFromGroupIfNoLongerMatched) or Operation (everything else).
- GET /api/v1/activity/summary and GET /api/v1/activity (category,
actionType, status, ruleId, search, pagination).
- Table-driven test seeding a run with mixed action types/statuses and
asserting window roll-ups, distinct-object counts, top rules, and the
category/status feed filters.
Frontend:
- New Activity page: window summary cards, by-action-type and
most-active-rules tables, and a drill-in feed with category/result/
object filters and a detail dialog that pretty-prints the action's
details JSON and error.
- Wired into the vertical sidebar and horizontal navbar under Automation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add runtime TLS modes resolved via settings (UI wins over env): auto
(self-managed), provided (bring-your-own upload), and windows-store (Windows
cert store, enumeration/serving scaffolded). New /api/v1/tls endpoints: status,
mode, certificate upload, and windows-store list; changes reload the live
manager without a restart, falling back to the self-managed cert if a configured
source is unavailable. Auto SANs now cover the hostname (CN), localhost, every
non-loopback IP, and host.<suffix> + the suffix for each detected DNS suffix
(Windows registry / resolv.conf). Bring-your-own is validated and unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add internal/tlsmgr: on startup, load-or-generate the CA, issue/reuse the leaf,
export the chain under <data>/tls, and hot-swap the cert on renewal via
GetCertificate. The server listens HTTPS when a manager is present. TLS is on by
default for the standalone binary/service (tls.enabled setting or
ORCHESTRAD_TLS_ENABLED, UI wins over env); the container image defaults it off so
it runs plain HTTP behind a TLS-terminating proxy. Startup logs a clickable URL.
Verified end to end: HTTPS serves, plain HTTP rejected, disable serves HTTP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A service launched by the SCM inherits the manager's working directory
(C:\Windows\System32 on Windows), which put the default ./data there. When
running non-interactively, chdir to the executable's directory so the database
lands beside the installed binary (Program Files\OrchestrAD\data). Interactive
and container runs are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add initialize (install + start) and remove (stop + uninstall) that are safe to
re-run: initialize skips reinstall/restart when already up, remove treats a
not-installed service as done. install/uninstall become aliases. Uses the
service status to branch. Verified idempotent on Windows (repeat runs exit 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement install/uninstall/start/stop via kardianos/service (Windows SCM,
systemd/upstart/sysv, launchd). The run command now goes through service.Run so
foreground/container and service execution share one startup path; install also
starts the service and uninstall stops then removes it. Route CLI and main
command output/errors through the centralized logger. Verified end to end as a
real Windows service (install -> RUNNING -> uninstall).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the users table is empty on startup (typical first boot into a
fresh data directory), create a single Admin-role user called admin
so the operator has something to log in with before an IdP is wired.
Password source precedence:
1. ORCHESTRAD_BOOTSTRAP_PASSWORD (or _FILE) if set
2. A 24-byte hex string generated at random and printed once to
stdout and to the structured log
The generated-password path logs a bright one-time banner with the
username and password, intended to be captured from the console on the
very first run and then forgotten. No password is ever written to a
file on disk by this flow.
Wired from cmd/run via cli.go so that every run invocation checks the
seed state after migrations have been applied.
Introduce ConfigService and /api/v1/config endpoints for system portability.
- Export: emits a versioned JSON document covering credentials (encrypted secrets preserved), connections, schedules, rules (with nested condition groups, conditions, and actions), and non-sensitive app settings.
- Import: validates the format version and upserts each entity by ID. Supports a dryRun mode that plans the import without writing, and emits per-entity warnings for secrets that need re-entry.
- Audit: Export and Import actions emit ConfigChange audit events with entity counts.
- Wiring: add ConfigService to server.Dependencies and instantiate it in cli.RunForeground.
New DashboardService aggregates entity counts (rules, connections, credentials, schedules, users, api keys), rolling rule-run statistics (last 24h and 7d), the 10 most recent rule runs, and per-connection health based on last_tested_utc/last_test_result. Exposed via GET /api/v1/dashboard/summary and wired through server.Dependencies + cli.RunForeground.
New SettingsService provides List/Get/Upsert/Delete over the app_settings table. SettingsHandler exposes GET /api/v1/settings, GET/PUT/DELETE /api/v1/settings/{key} and redacts sensitive values in responses. Changes are recorded as ConfigChange audit events. The service is wired into server.Dependencies and constructed in cli.RunForeground.
Previously the audit service was never constructed, so every emitAudit call across the handlers was a silent no-op. Build an audit.Service in RunForeground, add it to the Dependencies struct, and log a ServiceStart event on startup so the audit trail is bootstrapped.
- New BackupsHandler exposing list, create, and restore over
/api/v1/backups, backed by services.BackupService.
- Create records who triggered the backup (username from context or
'api') and defaults the backup type to 'manual'.
- Restore resolves the {id} path parameter against the list of available
backups so clients can reference backups by filename, or pass an
explicit filePath in the body.
- Inject BackupService via server.Dependencies and initialize it in
cli.RunForeground under <DataPath>/backups with retention 10.
- New APIKeysHandler exposing list, create, revoke, and delete over
/api/v1/api-keys, backed by services.APIKeyService.
- Create returns the full plaintext key once; list/get responses only
surface the prefix and metadata.
- When userId is omitted, the handler falls back to the authenticated
principal pulled from context so callers can self-service tokens.
- Inject APIKeyService via server.Dependencies and initialize it in
cli.RunForeground.
- Add UserRepository.List with pagination and non-deleted filtering.
- New UsersHandler with List/Get/Create/Update/Delete; Create hashes
passwords via crypto.HashPassword and Update re-hashes on rotation.
- Responses omit password material; username conflict returns 409.
- Inject UserRepo via server.Dependencies and wire /api/v1/users routes.
- Extend server.Dependencies with AuthService and CredService, and
initialize them in cli.RunForeground using the derived AES key.
- Replace the handleNotImplemented stubs on /auth and /credentials with
the existing AuthHandler and CredentialsHandler routes.
- Guard /auth/me behind api.AuthMiddleware so it only resolves when a
valid session token is presented.
- New SchedulesHandler backed by the existing ScheduleRepository, with
request/response DTOs matching the schedules model (kind, easy/cron,
timezone mode).
- Register List/Get/Create/Update/Delete routes on /api/v1/schedules.
- Extend server.Dependencies with ScheduleRepo and wire it from
cli.RunForeground.
- Extend RuleRepository with List/Update/SoftDelete/UpdateEnabled and
upsert helpers for condition groups, conditions, and actions.
- Extend RuleService with Create/GetByID/List/Update/Delete/Enable/Disable.
- Add List/Get/Create/Update/Delete/Enable/Disable handlers on RulesHandler
with request/response DTOs that decouple the API from storage models.
- Register the full CRUD route set on /api/v1/rules.
- Inject RuleService into server.Dependencies from cli.RunForeground.
Derives a 32-byte AES key from the configured secret via SHA-256, constructs
the ConnectionService, Runner, and Engine, and starts the Scheduler so
enabled rules fire automatically. Injects the shared services into the HTTP
server via a new Dependencies struct so API handlers can reuse them.
Phase 1 foundations:
- Go backend with Chi router framework
- SQLite database with WAL mode and foreign keys
- Database migrations for users, roles, credentials, AD connections, schedules, rules, and audit
- CLI commands: init, run, install, uninstall, start, stop, migrate, backup, restore, doctor
- Configuration loading from environment variables
- Centralized logging with file rotation (lumberjack)
- Crypto package for Argon2id password hashing and AES-GCM encryption
- Auth service with session management
- Audit service for event logging
- Scheduler with 6-field cron support
- REST API routes scaffolded for all major resources
- CORS support with localhost defaults for development
- Docker support with Dockerfile and docker-compose.yml
- Multi-platform build script (PowerShell)
- Project structure per design specification
Version format: yyyy.MM.dd.HHmm
All PKs are UUIDv4, all timestamps UTC