Merge development into main (release)
OrchestrAD
OrchestrAD is a modern, rule-based automation platform for Active Directory. It enables dynamic group membership, object lifecycle orchestration, and policy-driven directory operations through a clean UI, REST API, and powerful scheduling engine.
🚀 Overview
OrchestrAD replaces static scripts and JSON-based tooling with a centralized, database-backed system for managing directory automation.
It allows administrators to define rules that evaluate Users, Computers, and Groups, then automatically perform actions such as:
- Adding/removing group memberships
- Moving objects to Organizational Units (OUs)
- Creating groups dynamically
- Nesting groups
- Enforcing directory structure and policy consistency
All configuration is managed through the UI or API — no manual file editing required.
📸 Screenshots
| Dashboard | Rules |
|---|---|
![]() |
![]() |
| Credentials | Sign in |
|---|---|
![]() |
![]() |
The entire UI is served by the single backend binary (embedded Next.js static export) — no separate web server to run.
✨ Key Features
🔁 Rule-Based Automation
- Visual rule builder with condition groups and logical operators
- Supports multiple object types (Users, Computers, Groups)
- Rich condition support (equals, regex, LDAP, comparisons, etc.)
- Multiple actions per rule with ordered execution
🧠 Dynamic Directory Orchestration
- Add/remove group membership with diff-based execution
- Move objects to OUs with dynamic path generation
- Auto-create groups and OUs when missing
- Support for nested group relationships
⏱️ Scheduling Engine
- Easy schedules (e.g., every 5 minutes)
- Advanced 6-field cron expressions
- Per-rule execution control and concurrency handling
🔐 Secure Authentication & Access
- Local authentication (Argon2-secured)
- OIDC support (via NextAuth/Auth.js)
- API key system (separate from user auth)
- Role-based access control (RBAC)
🔑 Credential Management
- Reusable, encrypted credential objects
- Secure storage using AEAD encryption
- Credential testing and validation
📊 Observability & Auditing
- Centralized logging with rotation and retention
- Human-readable logs with structured context
- Full audit trail for all actions and changes
- Rule execution history and summaries
💾 Backup & Restore
- Automatic database backups with retention
- Manual backup and restore support
- Safe restore with validation and rollback protection
📦 Cross-Platform Runtime
-
Single Go binary
-
Runs as:
- foreground process
- system service
- Docker container
-
Supports Windows, macOS, and Linux (amd64 + arm64)
🧱 Architecture
- Backend: Go (Chi, sqlc, SQLite, robfig/cron)
- Frontend: Next.js + Material UI + Tailwind (Spike Template)
- Database: SQLite (WAL mode, migrations enabled)
- Auth: NextAuth/Auth.js (UI sessions) + backend RBAC/API keys
- Directory Integration: LDAP/LDAPS via go-ldap
🔐 Authentication Model
OrchestrAD separates authentication concerns:
-
Browser Sessions
- Managed via NextAuth/Auth.js
- Supports OIDC and local login
-
API Access
- Managed via API keys
- Keys can expire or persist indefinitely
- Shown only once at creation
-
Backend Authorization
- All security decisions enforced server-side
- RBAC controls access to resources and actions
🔌 API & Automation
Everything the UI does is available over the REST API, so rules can be created and run programmatically.
- Interactive docs (Swagger UI):
https://<host>:18090/api/docs - OpenAPI 3 spec:
https://<host>:18090/api/openapi.json - Compact route list:
https://<host>:18090/api/routes
The spec is generated from the live router, so it always reflects the endpoints the running build actually serves. Authenticate at POST /api/v1/auth/login, then send the returned token as Authorization: Bearer <token>.
Access to the docs
The documentation endpoints are not public — they require the same authentication as the rest of the API:
- From the dashboard: sign in, then use the
</>icon in the header or Administration → API Docs in the menu. Login sets a session cookie scoped to/api/docs, so the Swagger UI opens straight away in a new tab. Opening/api/docswhile signed out redirects to the login page and returns you there afterwards. - From a client: send
Authorization: Bearer <token>orX-API-Key: <key>to/api/openapi.jsonor/api/routes.
The docs cookie is HttpOnly and path-scoped to /api/docs, so it is never sent to /api/v1/* and cannot be used to make API calls. Swagger's Try it out still needs a token or API key.
Discovering routes
Both /api/routes and /api/openapi.json accept the same filters, so a client can ask for just the part of the API it cares about:
| Query | Meaning |
|---|---|
?method=post |
only POST operations |
?method=get,post |
GET or POST |
?path=rules |
paths containing rules |
?method=post&path=connections |
POST operations on connection routes |
/api/routes returns a flat list — method, path, summary, tag, whether the route is public, and allowed, which is false when a read-scoped API key cannot invoke it:
$headers = New-Object 'System.Collections.Generic.Dictionary[String,String]'
$headers.Add('X-API-Key', $apiKey)
$routes = Invoke-RestMethod -Method 'Get' `
-Uri 'https://localhost:18090/api/routes?method=post&path=rules' `
-Headers $headers -SkipCertificateCheck
$routes.data.routes | Format-Table -Property 'method', 'path', 'allowed', 'summary'
The same filters work on the Swagger UI itself (/api/docs?path=rules), which loads a spec narrowed to those operations.
Create a rule from PowerShell
A ready-to-run example lives at docs/examples/Create-OrchestrADRule.ps1. It builds the headers and body as strongly-typed dictionaries, serializes the body with ConvertTo-Json, and uses full cmdlet names (no aliases):
# Authenticate
$loginBody = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
$loginBody.Add('username', 'admin')
$loginBody.Add('password', 'admin')
$login = Invoke-RestMethod -Method 'Post' -Uri 'https://localhost:18090/api/v1/auth/login' `
-ContentType 'application/json' -Body ($loginBody | ConvertTo-Json) -SkipCertificateCheck
$headers = New-Object 'System.Collections.Generic.Dictionary[String,String]'
$headers.Add('Authorization', "Bearer $($login.data.token)")
# Build the rule: users where department = Sales -> synced into a target group
$condition = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
$condition.Add('attributeName', 'department'); $condition.Add('operator', 'Equals'); $condition.Add('comparisonValue', 'Sales')
$conditions = New-Object 'System.Collections.Generic.List[Object]'; $conditions.Add($condition)
$group = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
$group.Add('joinOperator', 'AND'); $group.Add('conditions', $conditions)
$conditionGroups = New-Object 'System.Collections.Generic.List[Object]'; $conditionGroups.Add($group)
$actionConfig = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
$actionConfig.Add('targetGroupDn', 'CN=Sales,OU=Groups,DC=corp,DC=com')
$actionConfig.Add('syncMode', 'FullSync'); $actionConfig.Add('createIfMissing', $true)
$action = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
$action.Add('actionType', 'SyncGroupMembership'); $action.Add('configurationJson', ($actionConfig | ConvertTo-Json -Compress))
$actions = New-Object 'System.Collections.Generic.List[Object]'; $actions.Add($action)
$ruleBody = New-Object 'System.Collections.Generic.Dictionary[String,Object]'
$ruleBody.Add('name', 'Sales Team'); $ruleBody.Add('adConnectionId', '<connection-id>')
$ruleBody.Add('objectType', 'User'); $ruleBody.Add('executionMode', 'Apply'); $ruleBody.Add('groupJoinOperator', 'AND')
$ruleBody.Add('conditionGroups', $conditionGroups); $ruleBody.Add('actions', $actions)
Invoke-RestMethod -Method 'Post' -Uri 'https://localhost:18090/api/v1/rules' `
-Headers $headers -ContentType 'application/json' -Body ($ruleBody | ConvertTo-Json -Depth 10) -SkipCertificateCheck
Tip: GET /api/v1/rules/metadata returns the valid object types, operators, action types, and sync modes for building rule bodies.
🧾 Logging & Retention
Standard log format:
[TimestampUTC] - [Component] - [Level] - Message
Log rotation is automatic (max size, backups, and age; compressed):
| Setting | Env var | Default |
|---|---|---|
| Max file size (MB) | ORCHESTRAD_LOG_MAX_SIZE_MB |
5 |
| Rotated copies kept | ORCHESTRAD_LOG_MAX_BACKUPS |
3 |
| Max age (days) | ORCHESTRAD_LOG_MAX_AGE_DAYS |
30 |
Database maintenance keeps history from growing forever — old rule runs and audit events are pruned and the database is compacted (VACUUM) on a schedule:
| Setting | Env var | Default |
|---|---|---|
| Rule-run history retained (days) | ORCHESTRAD_RUN_RETENTION_DAYS |
90 |
| Audit history retained (days) | ORCHESTRAD_AUDIT_RETENTION_DAYS |
180 |
| Maintenance interval (hours) | ORCHESTRAD_MAINTENANCE_INTERVAL_HOURS |
24 |
| Run VACUUM | ORCHESTRAD_MAINTENANCE_VACUUM |
true |
Error logs:
[TimestampUTC] - [Component] - [Level] - [File:Line:Column] - Message
Logs are designed to be:
- Clear and human-readable
- Operationally useful
- Free of unnecessary noise
⚙️ Configuration
Configuration is managed via:
- Environment variables
- Secure secrets
- Database-backed runtime configuration
Supports:
- Export and import of configuration (JSON)
- Schema versioning
- Credential portability with key validation
📥 Installation
Windows (MSI)
Download OrchestrAD-<version>-x64.msi from the latest release and run it. The installer:
- Installs to
C:\Program Files\OrchestrAD - Registers and starts the OrchestrAD Windows service (auto-start)
- Records the install directory and version under
HKLM\Software\Grace Solutions\OrchestrAD - On upgrade, updates only the binaries — the database (
…\OrchestrAD\data) is preserved - On uninstall, stops and removes the service and deletes the program files (the data directory is left in place)
Standalone binary (Windows / macOS / Linux)
Download the archive for your platform from the release (…-<os>-<arch>.tar.gz / .zip), extract the single orchestrad binary, and run it. See Running & Service Management below.
Docker
docker pull prod.git.gracesolution.info/gsadmin/orchestrad:latest
docker run -d --name orchestrad -p 18090:18090 \
-e ORCHESTRAD_SECRET_KEY=<32+ byte secret> \
-v orchestrad-data:/data \
prod.git.gracesolution.info/gsadmin/orchestrad:latest
Or use the bundled docker-compose.yml. Images are tagged latest and the unified commit date yyyy.MM.dd.HHmm.
▶️ Running & Service Management
The single binary is both the app and its own service manager (Windows service, systemd/upstart/sysv, or launchd).
orchestrad <command>
run Run in the foreground (also the service entry point; used by Docker)
initialize Install and start the service (idempotent)
remove Stop and remove the service (idempotent)
install Alias for initialize
uninstall Alias for remove
start Start the installed service
stop Stop the installed service
init Initialize the database and apply migrations
migrate Apply database migrations
backup Create a manual backup
restore --file <path> Restore the database from a backup
doctor Validate configuration and system health
version Show version information
initialize and remove are idempotent, so they are safe to re-run (and are what the MSI uses under the hood). On first start the default admin account is admin / admin, and a password change is forced at first login.
Key environment variables (see the CLI help for the full list):
| Variable | Default | Purpose |
|---|---|---|
ORCHESTRAD_DATA_PATH |
./data (next to the binary for the service) |
Database, logs, backups |
ORCHESTRAD_SECRET_KEY |
insecure dev key | AEAD key for credential encryption (set in production) |
ORCHESTRAD_SECRET_KEY_FILE |
— | Read the secret key from a file instead (for Docker/K8s secrets) |
ORCHESTRAD_HOST / ORCHESTRAD_PORT |
0.0.0.0 / 18090 |
HTTP bind address |
ORCHESTRAD_LOG_LEVEL |
info |
debug / info / warn / error |
⚠️ The secret key must stay the same for the life of the installation. Stored credential passwords are encrypted with it, so a changed or lost key leaves them intact but unreadable, and every directory bind fails. Back it up somewhere durable.
On startup OrchestrAD verifies that the stored credentials decrypt with the current key and logs a clear error naming the affected credentials if they do not;
orchestrad doctorruns the same check on demand. If the original key is genuinely gone, re-enter the affected passwords to re-encrypt them under the new key.
📦 Build & Versioning
- Version format:
yyyy.MM.dd.HHmm(derived from the HEAD commit date in CI) - Pure Go — SQLite via
modernc.org/sqlite, soCGO_ENABLED=0and every target cross-compiles from one machine with no C toolchain - Multi-platform: Windows, macOS, Linux (amd64 + arm64)
- Windows binaries embed the application icon and version metadata from
/resources/icons(viagoversioninfo)
Local build (Windows host, builds the frontend and stages it into the binary):
./scripts/build.ps1 -All # or -Windows / -MacOS / -Linux
Output lands under /binaries/<os>/<arch>/.
🐳 Docker
OrchestrAD ships as a single small (~60 MB) image built by a multi-stage Dockerfile that compiles the Next.js UI, embeds it, and builds the static Go binary:
- Runs in foreground mode by default; graceful shutdown on
SIGTERM /dataholds the database, logs, and backups (bind-mount or named volume)HEALTHCHECKprobes/health(answers both GET and HEAD)
🚀 CI/CD
A single Gitea Actions workflow (.gitea/workflows/release.yml) runs only on merge to main (docs-only merges are skipped — no per-commit or per-PR CI):
releasejob (ubuntu-host): runs the Go test suite as a gate, builds the frontend, cross-compiles all six binaries, builds and pushes the container image (latest+ version), and creates the Gitea release with the binaries + checksums attached.msijob (windows-host): wraps the Windows binary into the MSI with WiX and attaches it to the release.
Required repository secret
| Secret | Purpose |
|---|---|
REGISTRY_PASSWORD |
A Gitea personal access token with write:package scope, used to push images to the container registry (with REGISTRY_HOST + REGISTRY_USERNAME). |
REGISTRY_HOST / REGISTRY_USERNAME point at the container registry (the Gitea instance itself by default). The release itself is created with the auto-injected Actions token, so no other secrets are needed.
📌 Use Cases
- Dynamic group membership automation
- Directory cleanup and normalization
- Organizational policy enforcement
- Zero-touch user and device placement
- Identity lifecycle orchestration



