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>
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>
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>
The OpenAPI spec and Swagger UI were public. Put them behind the same
authentication as the rest of the API, and add a compact route list so a
client can ask "what can I call?" without opening dev tools.
Access:
- /api/openapi.json and /api/routes require a bearer token or API key.
- /api/docs additionally accepts a session cookie set at login, so a
signed-in operator can open the docs in a new tab; an anonymous browser
is redirected to /login?redirect=... and returned afterwards.
- The cookie is HttpOnly and path-scoped to /api/docs, so it is never sent
to /api/v1/* and cannot authenticate an API call (no CSRF surface).
Verified: cookie-only request to /api/v1/rules returns 401.
Discovery: both the spec and GET /api/routes accept ?method=get,post and
?path=<substring> (comma-separated, case-insensitive). The route list
returns method, path, summary, tag, public, and `allowed` — false when a
read-scoped API key cannot invoke that route. /api/docs passes the same
query through to the spec it loads.
UI: a </> icon in the header (both layouts) and an Administration → API
Docs menu entry, opened in a new tab via a new `external` nav-item flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
APIKeyService.List scanned created_utc straight into time.Time, which the
modernc.org/sqlite driver returns as a string — surfacing as a 500 "Failed
to list API keys". Scan it into a string and parse. Regression test covers
the create+list round-trip (created_utc parsed, scope preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The object viewer showed raw bytes for binary attributes. Format them for
display: objectGUID as a canonical GUID, objectSid/sIDHistory as S-1-… SID
strings, and any other non-printable value as base64. Applied in the
query-preview path that the object viewer uses; printable values pass through.
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>
When TLS is in auto (self-managed) mode, install the generated root into the
LocalMachine "Root" (Trusted Root CAs) store and the intermediate into the
"CA" (Intermediate CAs) store, so clients on this host trust the served
chain without manual import. Runs on every ensure/renewal with a replace
disposition, so it is idempotent and a renewed CA supersedes the previous
one. Best-effort (needs admin; the Windows service runs as LocalSystem);
a no-op on non-Windows.
Verified: OrchestrAD Root CA appears in LocalMachine\Root and the
Intermediate in LocalMachine\CA after startup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Roles:
- Add GET /api/v1/roles (built-in SuperAdmin/Admin/Operator/Viewer) and
wire role assignment into user create/update (UserRepository.SetRoles /
GetRoleNames / ListRoles). User responses now include roles.
- User dialog gains a roles multi-select; the users list shows role chips.
OIDC / SSO config UI:
- Default role is now a dropdown populated from /roles.
- Issuer URL shows real provider examples (Entra/Okta/Google).
- Replace the confusing manual "Redirect URL" field with a read-only,
auto-derived callback URL (from the browser origin / public URL) plus a
copy button — the exact value to register at the IdP. The backend still
auto-derives the callback and honors an env override.
Verified: /roles lists the four roles; creating/updating a user with roles
round-trips through GET.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the API browsable/usable from Swagger UI without dev tools:
- Normalize chi's trailing slash on collection roots (Post("/")) so registry
detail and pagination attach — previously POST /ad-connections etc. showed
no request body.
- Document request bodies for the main create/update operations (connections,
credentials, schedules, api-keys, users, settings, tls mode/certificate,
query-preview, config import) with component schemas.
- Add standard page/pageSize query params to collection GETs.
- Add the X-API-Key security scheme alongside bearer so both auth methods
show in the Authorize dialog.
Test covers the trailing-slash normalization + body attachment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SQLite database now lives at data/db/orchestrad.db instead of the data
root. db.New creates the db/ directory (SQLite will not) and, on first run,
moves a legacy data/orchestrad.db plus its -wal/-shm sidecars into db/ so
existing installations keep their data. Verified live: an existing demo
database migrated into data/db and all data (connections, rules, schedules)
was retained. Test covers the move + idempotency.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lower ORCHESTRAD_LOG_MAX_SIZE_MB default from 100 to 5 so log files roll
sooner by default; README updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
API keys were never validated — the middleware only checked session tokens,
so an X-API-Key request always 401'd. Add auth.Service.ValidateAPIKey (looks
up the key hash, enforces enabled/revoked/expiry, loads the user + roles,
stamps last_used_utc) and route X-API-Key / bearer auth through it.
Add per-key scopes (migration 006): "read" (GET/HEAD only) or "readwrite"
(full access, default). The middleware rejects mutating requests from a
read-scoped key with 403. Create accepts a scope; list and create responses
include it; the UI create dialog has a scope selector and the list shows a
scope chip. Disable/re-enable and revoke (permanent) were already correct.
Verified live: RW key GET/POST ok; read key GET ok, POST 403; disable→401,
re-enable→200; revoke→401 and re-enable blocked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- GET /api/docs serves a Swagger UI and GET /api/openapi.json serves an
OpenAPI 3 spec built by walking the live chi router, so documented paths
always match what the build serves. A small registry adds rich detail
(request bodies, params, schemas) for the automation-critical operations
(auth login, rule create/update/preview, connection introspection);
RuleInput and friends are defined as reusable component schemas.
- Add docs/examples/Create-OrchestrADRule.ps1: a no-alias PowerShell sample
that builds headers/body as typed dictionaries, serializes with
ConvertTo-Json, logs in, creates a SyncGroupMembership rule, and runs it.
- README: new "API & Automation" section (Swagger + PowerShell), and a
"Logging & Retention" section documenting log rotation and the new
database maintenance/retention knobs.
Tests cover spec generation from a router and registry well-formedness.
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>
The runner updated the rule_runs history but never wrote the rules table's
last_run_utc / last_run_result columns, so the rules list showed blank Last
Run / Last Result. Add RuleRepository.UpdateLastRun and call it from the
runner's finalize and fail paths with the run's completion time and status.
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>
Add two directory introspection endpoints the rule filter builder uses:
- GET /ad-connections/{id}/attributes?objectType=&q= returns the schema
attributes that APPLY to the given object type (User/Computer/Group). The
applicable set is derived by walking the classSchema hierarchy from the
object's class up through subClassOf to top, plus auxiliary classes,
unioning each class's may/must-contain attributes; results are filtered by
substring and returned with adminDescription. The per-(connection,object
type) set is cached for 10 minutes so the walk is not repeated per
keystroke. (Computer inherits user attributes, since AD's computer class
subclasses user — reflected correctly.)
- GET /ad-connections/{id}/attribute-values?attribute=&objectType=&q= samples
objects and returns the distinct values present for one attribute, so the
value field can suggest real directory values.
Backed by a new bounded ldap Client.SearchWithLimit that tolerates the
server's size-limit response. Attribute names are validated against the LDAP
descriptor charset before use in a filter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getConditionGroups held an open rows cursor while calling getConditions
per group, so each GetByID needed two pooled connections at once and could
deadlock under a small/exhausted pool. Read all groups first, close the
cursor, then load every group's conditions in a single bulk IN-query keyed
by condition_group_id. The repository round-trip test now runs against a
single-connection pool, which guards the no-nested-cursor invariant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns rules into Adaxes/Active-Roles style dynamic groups. The core new
capability is a set-level SyncGroupMembership action that reconciles a
target group's membership against the matched object set in one pass
instead of the old add-only, per-object behaviour.
Engine / reconciliation:
- New ActionSyncGroupMembership runs once per target group after the
match: resolve (and optionally create) the group, read its current
members, diff against the matched set, and apply the adds/removes.
- Three per-rule sync modes (types.SyncMode): FullSync (membership ==
matched set; removes stale members incl. manual adds), ManagedAdd (adds
matches, removes only members this rule added), AddOnly (never removes).
- Managed ownership tracked in a new managed_group_members table
(migration 005) + repository, wired into the runner's engine so
ManagedAdd removes only what it added.
- Adds/removes are recorded as AddToGroup / RemoveFromGroupIfNoLongerMatched
run-actions so the activity feed categorises them as syncs/removals.
- Preview now computes an accurate, non-mutating diff for sync actions
(+add / -remove / already-in-sync counts and per-member entries).
- memberOf and memberOf-recursive (LDAP_MATCHING_RULE_IN_CHAIN) operators;
Regex no longer silently degrades to equals.
- Canonical group targets: CanonicalToLeafDN / NormalizeGroupTarget so a
target group can be given as domain.com/OU/Group as well as a DN.
Editor-facing APIs (backend-first; UI comes next):
- Rule create/update now accept conditionGroups + actions and persist them
via RuleRepository.ReplaceLogic (soft-delete + insert, preserving the
rule_run_actions FK). Omitting them leaves existing logic untouched.
- POST /api/v1/rules/preview evaluates an unsaved draft (live match panel).
- GET /api/v1/rules/metadata serves the operator vocabulary (object types,
operators, action types, sync modes, common attributes) so UI dropdowns
stay in lock-step with the backend.
- GET /api/v1/ad-connections/{id}/directory searches groups/OUs for the
target pickers.
Tests: reconciliation across all three modes + create-if-missing and the
missing-group error path (fake directory client); canonical leaf-DN
conversion; ReplaceLogic round-trip. Full suite green.
Note: RuleRepository.GetByID nests a query (getConditionGroups holds a
cursor while calling getConditions); safe under the production pool (25)
but a follow-up should flatten it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ad_connections List query selected only a subset of columns, so
use_start_tls, allow_invalid_certs, timeout_seconds, paging_enabled and
page_size came back as zero-values in the list response. Because the edit
form pre-populates directly from the list row, opening and saving a
connection silently overwrote those stored values with their zero values
— e.g. "Allow invalid certs" would never stick, and timeout/page size
reset to 0.
Select the full column set in List (matching GetByID) and scan the
previously dropped fields. Extend the repository round-trip test to set
these fields on create and assert List surfaces them.
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>
The modernc.org/sqlite driver returns TEXT timestamp columns as strings
rather than time.Time (unlike the CGO mattn driver), so scanning directly
into *time.Time failed with "unsupported Scan" and surfaced as 500 errors
on the credentials and connections list endpoints.
Scan created/updated/started timestamps into strings and convert with the
existing parseTimeOrZero/parseNullTime helpers across credentials,
connections, schedules, rules, and rule_runs repositories. Add a
regression test that round-trips a credential and connection through a
migrated DB and asserts the timestamps come back parsed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add CanonicalToOUDN/NormalizeOUTarget so an OU target can be given as a canonical
path (domain.com/OU/OU) or a DN, and Client.EnsureOUPath which idempotently
creates every OU down the path (parents before children). MoveToOu and group
creation now normalize the target and ensure the full OU path instead of only
the leaf. Unit-tested (conversion, escaped split) and verified against the test
AD: a canonical nested target creates each OU and moves the object; re-running is
a no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ldap.CanonicalName: prefer the directory's canonicalName attribute (now
requested in searches) and otherwise build domain.com/OU/OU/CN from the DN.
Populate MatchedObject.CanonicalName and return it in the rule preview API so
operators see canonical names instead of raw DNs. Unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add build-tagged (adtest) + env-gated integration tests that run the rule engine
against a live Active Directory over plain LDAP: create a unique test OU with
users/groups, execute a rule, and assert the AD outcome, then tree-delete the OU
on cleanup. Covers add-to-group-by-condition (matching users added, others not),
create-missing-group, and move-to-OU. Excluded from normal builds and CI; run
with -tags adtest and ORCHESTRAD_AD_TEST_* env. Verified green against the test
domain; no objects left behind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add backend OIDC: /api/v1/auth/oidc/status|login|callback (authorization code +
PKCE, state+nonce cookies, ID-token verification via go-oidc) and admin
/auth/oidc/config (GET/PUT, validates the issuer via discovery on enable).
Config resolves app_settings (UI) > env > default, so SSO can be configured from
the UI while env still seeds it. LoginOIDC provisions/links federated users by
the stable (issuer, subject) pair, refusing to reuse a username held by a
different account. Verified: discovery + real authorize redirect + PKCE cookies;
config API with DB-over-env precedence; provisioning unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Windows, resolve the listen address/port with precedence env > registry
(HKLM\Software\Grace Solutions\OrchestrAD ListenAddress/ListenPort, written by
the MSI wizard) > default. No-op off Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement ListWindowsCerts (LocalMachine + CurrentUser My stores: thumbprint,
subject, issuer, validity, SANs, has-private-key) and windows-store serving:
find the cert by thumbprint, acquire its CNG key, and serve TLS via a
crypto.Signer that signs through NCryptSignHash (RSA PKCS#1 v1.5 and PSS) so the
private key is never exported. Legacy CSP / non-RSA keys report a clear error.
Verified end to end: a CNG cert in LocalMachine\My serves a completed HTTPS
handshake (openssl confirms the served subject).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Connection-specific DNS suffixes are set per adapter (and via DHCP) without a
domain join, so read Domain/DhcpDomain from each Tcpip Interfaces subkey in
addition to the global Domain/NV Domain/SearchList. Verified on a workgroup host:
the cert now covers host.<adapter-suffix> and the suffix for every adapter.
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>
Add internal/pki: generate a Root CA -> Intermediate CA -> server leaf, re-issue
leaves under a persisted CA for renewal, and export the chain as PEM
(cert/key/fullchain), DER (.cer), and PKCS#12 (.pfx). Unit-tested: the chain
verifies, the PFX decodes with the CA chain, and a reloaded CA re-issues a
valid leaf.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add SettingsService.ResolveString/Bool/Int resolving runtime config as
app_settings (UI) > environment variable > default. Env seeds bootstrap; any
value set in the UI persists to app_settings and wins. Foundation for OIDC and
TLS UI configuration. Unit-tested for the precedence order.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change the default HTTP port from 8080 to 18090 and propagate it through the
Dockerfile (EXPOSE + healthcheck), docker-compose, and the README. Host default
stays 0.0.0.0.
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 a purpose-drawn icon (indigo/violet badge with an orchestration hub-and-spoke
glyph) as .svg, a 256px .png, and a multi-resolution .ico (16-256). versioninfo.json
drives goversioninfo to embed the icon + metadata into the Windows binaries; the
generated .syso objects are build artifacts and gitignored.
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>
Replace mattn/go-sqlite3 (CGO) with modernc.org/sqlite and the modernc-based
golang-migrate driver, translating the DSN pragmas. This lets every target
(win/mac/linux, amd64/arm64) cross-compile from one runner with no C toolchain,
so the Docker build now sets CGO_ENABLED=0 and drops gcc/musl-dev. Verified:
DB opens (WAL + foreign keys), migrations run, bootstrap seeds admin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce logging.Init/Default plus package-level Info/Warn/Error/Debug so
code without an injected *Logger (main, the service wrapper) emits the same
[UTC] - [Component] - [Level] - Message format on the one shared writer and
rotation policy. Existing injected usage is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Docker HEALTHCHECK uses 'wget --spider' (a HEAD request), but the health
routes were GET-only and chi returned 405, which would mark the container
unhealthy. Register HEAD alongside GET on /health and /api/health.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cover the environment-driven config loader (defaults, overrides, secret
from file) and the unauthenticated /health and /api/v1/version handlers
the Docker HEALTHCHECK and bootstrap flow depend on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add password_reset_required column (migration 004) + repository support
- auth.Service.ChangePassword verifies current, hashes new, clears flag,
emits PasswordChange audit events for success and failure
- Bootstrap: when no ORCHESTRAD_BOOTSTRAP_PASSWORD[_FILE] is set, seed
admin/admin with password_reset_required=true and log a one-time warn
banner; env/file-supplied passwords keep the flag clear
- Expose passwordResetRequired in UserInfo / /auth/me / login response
- POST /api/v1/auth/change-password behind the authenticated group
- Frontend: /change-password page + ChangePasswordForm, AuthLogin and
RequireAuth bounce any other route to it while the flag is set
- Docs: DesignSpecification 8.6/8.8 and Template 7.6/7.7 rewritten,
Trusted Proxy renumbered to 7.8 in the template, acceptance items
updated to match the new default-credential behavior
- Add sanitizeRedirect/safeRedirectTarget helper; reject anything that isn't
a safe local path (must start with a single /, no scheme, no whitespace,
not /login itself). Apply in RequireAuth when encoding the current pathname
and in AuthLogin when consuming ?redirect=.
- Make configureApi's getToken read localStorage directly via readCurrentToken
so the first request after login cannot race the AuthContext re-render that
previously owned the token via a React closure.
- Redirect legacy /auth/* (including the pre-flatten /auth/auth1/login) with
HTTP 301 to /login in the webui handler so stale bookmarks can't seed the
SPA router with a malformed URL.
- Regression test TestHandler_LegacyAuthPathRedirectsToLogin covering four
legacy path shapes.
Two server-layer changes that were developed together because they
share the same routing rewrite:
- proxy.go: TrustedProxy middleware inspects RemoteAddr against the
operator-configured CIDR list (ORCHESTRAD_TRUSTED_PROXIES) and, only
when the peer matches, rewrites r.RemoteAddr and r.URL.Scheme from
X-Forwarded-For / X-Forwarded-Proto. Untrusted peers are ignored so
downstream handlers and the audit log see the direct connection
address, preventing header spoofing.
- server.go: /api/v1 is split into a public surface (version, health,
auth login/logout/csrf) and an authenticated group that now wraps
/users, /credentials, /ad-connections, /schedules, /rules, /rule-runs,
/backups, /api-keys, /audit, /settings, /dashboard, and /config with
AuthMiddleware. Previously only /auth/me and two /api-keys routes
were protected; every other administrative endpoint was reachable
without a token, which was a real security bug.
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.
Adds four related pieces of first-run configuration surface so the
single binary can be dropped behind a reverse proxy or into a container
with sensible defaults:
- getEnvOrFile: any ORCHESTRAD_* value may be supplied either directly
via environment variable or indirectly via ORCHESTRAD_*_FILE pointing
at a file on disk, matching the Docker / Kubernetes secret idiom.
- ORCHESTRAD_BOOTSTRAP_USERNAME and ORCHESTRAD_BOOTSTRAP_PASSWORD drive
the first-run admin seed (consumed by auth.Bootstrap in a later commit).
- ORCHESTRAD_TRUSTED_PROXIES accepts a comma-separated list of CIDRs
whose X-Forwarded-* headers will be honored by the proxy middleware.
- ORCHESTRAD_ALLOWED_ORIGINS replaces the hardcoded localhost:3000 CORS
allowlist; empty means no cross-origin access, which is the correct
default for the same-origin embedded-UI deployment.