5301b47f16
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>
396 lines
16 KiB
Markdown
396 lines
16 KiB
Markdown
# 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 |
|
|
| --- | --- |
|
|
| [](docs/screenshots/dashboard.png) | [](docs/screenshots/rules.png) |
|
|
|
|
| Credentials | Sign in |
|
|
| --- | --- |
|
|
| [](docs/screenshots/credentials.png) | [](docs/screenshots/login.png) |
|
|
|
|
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/docs` while signed out redirects to the login page and returns you there afterwards.
|
|
* **From a client:** send `Authorization: Bearer <token>` or `X-API-Key: <key>` to `/api/openapi.json` or `/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:
|
|
|
|
```powershell
|
|
$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`](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):
|
|
|
|
```powershell
|
|
# 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](https://prod.git.gracesolution.info/gsadmin/OrchestrAD/releases) 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
|
|
|
|
```bash
|
|
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`](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 doctor` runs 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`, so `CGO_ENABLED=0` and 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` (via `goversioninfo`)
|
|
|
|
Local build (Windows host, builds the frontend and stages it into the binary):
|
|
|
|
```powershell
|
|
./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`
|
|
* `/data` holds the database, logs, and backups (bind-mount or named volume)
|
|
* `HEALTHCHECK` probes `/health` (answers both GET and HEAD)
|
|
|
|
---
|
|
|
|
## 🚀 CI/CD
|
|
|
|
A single Gitea Actions workflow ([`.gitea/workflows/release.yml`](.gitea/workflows/release.yml)) runs **only on merge to `main`** (docs-only merges are skipped — no per-commit or per-PR CI):
|
|
|
|
1. **`release`** job (`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.
|
|
2. **`msi`** job (`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
|
|
|
|
---
|