Skip to content

Commit 1fef9ee

Browse files
committed
docs: add Security-as-Code and Audit Log documentation (Issue #11)
- Add docs/guides/security-as-code.md — full nexus-cli guide covering plan/apply workflow, manifest format, --prune flag, and CI/CD setup - Add docs/reference/audit-log.md — GET /v1/audit endpoint reference with event types, query params, and response schema - Update docs/services/broker.md — add Audit Subsystem section (§5) and clarify STATE_KEY fatal-exit in the env vars table - Update docs/reference/security-model.md — add Audit Trail and STATE_KEY Startup Guard sections - Update docs/guides/managing-providers.md — add tip callout pointing to the declarative nexus-cli workflow - Update mkdocs.yml — wire new pages into nav
1 parent 091eaa6 commit 1fef9ee

6 files changed

Lines changed: 402 additions & 1 deletion

File tree

docs/guides/managing-providers.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
This guide provides a comprehensive overview of how to register, manage, and test identity providers within the Nexus OAuth Broker.
44

5+
!!! tip "Prefer a GitOps workflow?"
6+
For production deployments, consider using **[`nexus-cli`](security-as-code.md)** — a declarative reconciler that manages providers via a YAML manifest committed to your repository. It gives you version history, code review, and an automatic audit trail for every change.
7+
8+
59
## Provider Types
610

711
The broker supports two primary types of providers:

docs/guides/security-as-code.md

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# Security-as-Code: Declarative Provider Management
2+
3+
The **`nexus-cli`** tool brings a GitOps-compatible, Terraform-style workflow to managing your Nexus provider configurations. Instead of managing providers through direct API calls (which leave no version history and are impossible to review), you declare your desired state in a YAML manifest, commit it to your repository, and let `nexus-cli` reconcile the live Broker against that source of truth.
4+
5+
!!! tip "Why this matters"
6+
Nexus holds Refresh Tokens and API Keys for every provider a workspace connects to — it is critical infrastructure. Without declarative management, a single bad API call can silently break all agents that depend on a provider, with no git history to recover from.
7+
8+
---
9+
10+
## How It Works
11+
12+
`nexus-cli` follows a **plan → confirm → apply** workflow:
13+
14+
1. **Fetches** the current live state from `GET /v1/providers`.
15+
2. **Diffs** it against your `nexus-providers.yaml` manifest.
16+
3. **Prints** a human-readable plan showing creates, updates, and orphaned providers.
17+
4. **Applies** the changes only after you confirm with `yes` (or non-interactively in CI).
18+
19+
---
20+
21+
## Installation
22+
23+
Build from source within the repository:
24+
25+
```bash
26+
cd nexus-cli
27+
go build -o nexus-cli .
28+
```
29+
30+
Or install directly:
31+
32+
```bash
33+
go install github.com/Prescott-Data/nexus-framework/nexus-cli@latest
34+
```
35+
36+
---
37+
38+
## Configuration
39+
40+
`nexus-cli` is configured via environment variables:
41+
42+
| Variable | Description | Default |
43+
| :--- | :--- | :--- |
44+
| `BROKER_BASE_URL` | Base URL of the Nexus Broker | `http://localhost:8080` |
45+
| `API_KEY` | API key for Broker authentication | *(none)* |
46+
47+
---
48+
49+
## The Provider Manifest
50+
51+
Create a `nexus-providers.yaml` file and **commit it to your GitOps repository**. This file is your single source of truth for all provider configurations.
52+
53+
Environment variables are expanded at runtime, so secrets never need to be hardcoded.
54+
55+
```yaml title="nexus-providers.yaml"
56+
providers:
57+
- name: google-workspace
58+
auth_type: oauth2
59+
client_id: "${GOOGLE_CLIENT_ID}"
60+
client_secret: "${GOOGLE_CLIENT_SECRET}"
61+
issuer: "https://accounts.google.com"
62+
enable_discovery: true
63+
scopes:
64+
- openid
65+
- email
66+
- profile
67+
- offline_access
68+
69+
- name: github
70+
auth_type: oauth2
71+
client_id: "${GITHUB_CLIENT_ID}"
72+
client_secret: "${GITHUB_CLIENT_SECRET}"
73+
auth_url: "https://github.com/login/oauth/authorize"
74+
token_url: "https://github.com/login/oauth/access_token"
75+
api_base_url: "https://api.github.com"
76+
enable_discovery: false
77+
scopes:
78+
- read:user
79+
- user:email
80+
```
81+
82+
### Manifest Fields
83+
84+
| Field | Type | Description |
85+
| :--- | :--- | :--- |
86+
| `name` | string | Unique provider name (used as the reconciliation key) |
87+
| `auth_type` | string | `oauth2` or `api_key` |
88+
| `client_id` | string | OAuth client ID |
89+
| `client_secret` | string | OAuth client secret |
90+
| `issuer` | string | OIDC issuer URL for auto-discovery |
91+
| `auth_url` | string | Authorization endpoint (if not using discovery) |
92+
| `token_url` | string | Token endpoint (if not using discovery) |
93+
| `api_base_url` | string | Provider API root URL |
94+
| `enable_discovery` | bool | Use OIDC discovery if `true` |
95+
| `scopes` | list | Default scopes to request |
96+
| `params` | map | Provider-specific extra parameters |
97+
98+
---
99+
100+
## Commands
101+
102+
### `plan` — Preview Changes
103+
104+
Show what would change without making any mutations:
105+
106+
```bash
107+
nexus-cli plan
108+
# Or with a custom manifest path:
109+
nexus-cli plan --file ./path/to/nexus-providers.yaml
110+
```
111+
112+
**Example output:**
113+
114+
```
115+
Read 2 providers from nexus-providers.yaml
116+
117+
--- Execution Plan ---
118+
+ CREATE : github
119+
~ UPDATE : google-workspace
120+
! ORPHAN : old-slack-provider (would be deleted if --prune was passed)
121+
122+
Plan complete. Run 'nexus-cli apply' to perform these actions.
123+
```
124+
125+
The symbols mean:
126+
127+
| Symbol | Action |
128+
| :--- | :--- |
129+
| `+` | Provider will be created |
130+
| `~` | Provider will be updated |
131+
| `-` | Provider will be deleted (only shown with `--prune`) |
132+
| `!` | Provider exists in live state but not in manifest (orphan) |
133+
134+
### `apply` — Apply Changes
135+
136+
Apply the manifest, with an interactive confirmation prompt:
137+
138+
```bash
139+
nexus-cli apply
140+
```
141+
142+
```
143+
Read 2 providers from nexus-providers.yaml
144+
145+
--- Execution Plan ---
146+
+ CREATE : github
147+
~ UPDATE : google-workspace
148+
149+
Do you want to perform these actions?
150+
Nexus will perform the actions described above.
151+
Only 'yes' will be accepted to approve.
152+
153+
Enter a value: yes
154+
155+
--- Applying Changes ---
156+
Creating github... OK
157+
Updating google-workspace... OK
158+
```
159+
160+
#### Flags
161+
162+
| Flag | Default | Description |
163+
| :--- | :--- | :--- |
164+
| `--file` | `nexus-providers.yaml` | Path to the manifest file |
165+
| `--prune` | `false` | Also delete providers in live state not in the manifest |
166+
167+
!!! warning "Using `--prune`"
168+
The `--prune` flag will **delete** providers that exist in the Broker but are absent from your manifest. Only use this when you are certain your manifest is the complete desired state. Any agents depending on a pruned provider will immediately lose their connections.
169+
170+
---
171+
172+
## CI/CD Integration
173+
174+
The recommended pattern is to use `nexus-cli` in your GitHub Actions pipeline so that every change to the manifest goes through a code review + automated reconciliation cycle.
175+
176+
A workflow is included at `.github/workflows/nexus-cli.yml` and runs automatically when `nexus-cli/nexus-providers.yaml` is modified:
177+
178+
```yaml title=".github/workflows/nexus-cli.yml"
179+
on:
180+
pull_request:
181+
paths: ['nexus-cli/nexus-providers.yaml']
182+
push:
183+
branches: [main]
184+
paths: ['nexus-cli/nexus-providers.yaml']
185+
186+
jobs:
187+
plan-or-apply:
188+
runs-on: ubuntu-latest
189+
steps:
190+
- uses: actions/checkout@v4
191+
- uses: actions/setup-go@v5
192+
with:
193+
go-version: '1.21'
194+
- run: go build -o nexus-cli
195+
working-directory: ./nexus-cli
196+
197+
# On PRs: show a plan as a check
198+
- name: Plan (Pull Request)
199+
if: github.event_name == 'pull_request'
200+
env:
201+
BROKER_BASE_URL: ${{ secrets.BROKER_BASE_URL }}
202+
API_KEY: ${{ secrets.BROKER_API_KEY }}
203+
run: ./nexus-cli plan
204+
205+
# On merge to main: apply with prune
206+
- name: Apply (Push to Main)
207+
if: github.event_name == 'push'
208+
env:
209+
BROKER_BASE_URL: ${{ secrets.BROKER_BASE_URL }}
210+
API_KEY: ${{ secrets.BROKER_API_KEY }}
211+
run: ./nexus-cli apply --prune <<< "yes"
212+
working-directory: ./nexus-cli
213+
```
214+
215+
### Required GitHub Secrets
216+
217+
| Secret | Description |
218+
| :--- | :--- |
219+
| `BROKER_BASE_URL` | URL of your production Nexus Broker |
220+
| `BROKER_API_KEY` | API key for Broker authentication |
221+
222+
---
223+
224+
## Best Practices
225+
226+
1. **Treat `nexus-providers.yaml` as infrastructure code** — require PR reviews for all changes.
227+
2. **Never hardcode secrets** — always use `${ENV_VAR}` expansion and inject via CI secrets.
228+
3. **Start without `--prune`** — let orphans accumulate warnings first so you can audit them intentionally before deletion.
229+
4. **One manifest per environment** — keep a `nexus-providers.prod.yaml` and `nexus-providers.staging.yaml` and set `BROKER_BASE_URL` accordingly in each CI environment.
230+
5. **All mutations are audited** — every create, update, or delete applied by `nexus-cli` is recorded in the [Audit Log](../reference/audit-log.md).

docs/reference/audit-log.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Audit Log Reference
2+
3+
The Nexus Broker maintains a tamper-evident **audit log** of every control-plane mutation. Every time a provider is created, updated, or deleted — or an OAuth connection is established — a structured record is written to the `audit_events` table.
4+
5+
This provides a queryable history of who changed what and when, which is essential for operating Nexus as critical infrastructure.
6+
7+
---
8+
9+
## Audited Events
10+
11+
| Event Type | Trigger |
12+
| :--- | :--- |
13+
| `provider.created` | A new provider profile is registered |
14+
| `provider.updated` | A provider's configuration is modified (`PUT` or `PATCH`) |
15+
| `provider.deleted` | A provider is deleted (by ID or by name) |
16+
| `connection.created` | An OAuth callback completes successfully and a connection is established |
17+
| `connection.revoked` | A connection is explicitly revoked |
18+
19+
---
20+
21+
## Query the Audit Log
22+
23+
```
24+
GET /v1/audit
25+
```
26+
27+
Returns recent audit events in descending chronological order. This endpoint is protected by `ApiKeyMiddleware`.
28+
29+
### Query Parameters
30+
31+
| Parameter | Type | Description |
32+
| :--- | :--- | :--- |
33+
| `event_type` | string | Filter by event type (e.g. `provider.deleted`) |
34+
| `since` | string | RFC3339 timestamp — only return events after this time |
35+
| `limit` | integer | Maximum records to return (default: `50`, max: `1000`) |
36+
37+
### Examples
38+
39+
**Fetch the last 50 audit events:**
40+
```bash
41+
curl -s "http://localhost:8080/audit" \
42+
-H "X-API-Key: <YOUR_API_KEY>" | jq .
43+
```
44+
45+
**Filter by event type:**
46+
```bash
47+
curl -s "http://localhost:8080/audit?event_type=provider.deleted" \
48+
-H "X-API-Key: <YOUR_API_KEY>" | jq .
49+
```
50+
51+
**Filter by time window:**
52+
```bash
53+
curl -s "http://localhost:8080/audit?since=2026-05-01T00:00:00Z&limit=100" \
54+
-H "X-API-Key: <YOUR_API_KEY>" | jq .
55+
```
56+
57+
**Combine filters:**
58+
```bash
59+
curl -s "http://localhost:8080/audit?event_type=provider.created&since=2026-05-01T00:00:00Z" \
60+
-H "X-API-Key: <YOUR_API_KEY>" | jq .
61+
```
62+
63+
---
64+
65+
## Response Schema
66+
67+
```json
68+
[
69+
{
70+
"id": "a1b2c3d4-...",
71+
"connection_id": "f5e6d7c8-...",
72+
"event_type": "connection.created",
73+
"event_data": "{\"provider_id\": \"...\", \"workspace_id\": \"ws-123\"}",
74+
"ip_address": "10.0.0.1",
75+
"user_agent": "nexus-gateway/1.0",
76+
"created_at": "2026-05-05T10:30:00Z"
77+
},
78+
{
79+
"id": "b2c3d4e5-...",
80+
"connection_id": null,
81+
"event_type": "provider.deleted",
82+
"event_data": "{\"provider_id\": \"...\", \"provider_name\": \"old-slack\"}",
83+
"ip_address": "192.168.1.5",
84+
"user_agent": "curl/7.88.1",
85+
"created_at": "2026-05-05T09:15:00Z"
86+
}
87+
]
88+
```
89+
90+
### Field Descriptions
91+
92+
| Field | Type | Description |
93+
| :--- | :--- | :--- |
94+
| `id` | UUID | Unique audit event identifier |
95+
| `connection_id` | UUID \| null | Associated connection, if applicable |
96+
| `event_type` | string | The event type (see table above) |
97+
| `event_data` | string \| null | JSON payload with event-specific context |
98+
| `ip_address` | string \| null | IP of the caller (respects `X-Forwarded-For`) |
99+
| `user_agent` | string \| null | User-Agent of the caller |
100+
| `created_at` | RFC3339 | Timestamp of the event |
101+
102+
---
103+
104+
## Database
105+
106+
Audit events are stored in the `audit_events` PostgreSQL table, created in the initial migration (`00_create_tables.sql`). An index on `created_at DESC` (migration `11_add_audit_created_at_index.sql`) ensures fast time-range queries even at high volume.
107+
108+
!!! note "Retention Policy"
109+
There is currently no automatic retention/pruning policy for audit events. For long-running production deployments, consider adding a scheduled job to archive or delete records older than your compliance window (e.g., 90 days).
110+
111+
---
112+
113+
## Audit via `nexus-cli`
114+
115+
Every mutation performed by [`nexus-cli apply`](../guides/security-as-code.md) is automatically recorded in the audit log. You can correlate CLI runs with audit events using the `ip_address` field (the IP of your CI runner) and the `event_data.provider_name` field.
116+
117+
```bash
118+
# See all provider changes from a CI apply run
119+
curl -s "http://localhost:8080/audit?event_type=provider.created&since=2026-05-05T13:00:00Z" \
120+
-H "X-API-Key: <YOUR_API_KEY>" | jq .
121+
```

docs/reference/security-model.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,35 @@ The Broker supports an `ALLOWED_CIDRS` policy. In production, this should be res
4242

4343
### mTLS (Roadmap)
4444
Future versions of Nexus will support mutual TLS between the Gateway and Broker for cryptographically enforced identity beyond API keys.
45+
46+
---
47+
48+
## Audit Trail
49+
50+
Nexus maintains a tamper-evident **audit log** for all control-plane mutations. Every provider create, update, and delete — and every OAuth connection established — writes a record to the `audit_events` table with:
51+
52+
- The **event type** (`provider.created`, `provider.deleted`, `connection.created`, etc.)
53+
- **Structured event data** (provider ID, name, workspace ID)
54+
- The **caller IP address** and **User-Agent**
55+
56+
This audit log is queryable via the [`GET /v1/audit`](audit-log.md) endpoint and is the foundational building block for compliance, forensic analysis, and detecting unauthorized mutations.
57+
58+
!!! tip "GitOps for Auditability"
59+
For the strongest audit posture, use [`nexus-cli`](../guides/security-as-code.md) to manage providers declaratively. Every `nexus-cli apply` run goes through git history AND generates audit log entries — giving you two independent sources of truth.
60+
61+
---
62+
63+
## `STATE_KEY` Startup Guard
64+
65+
Both the Broker and Gateway will **fatal-exit at startup** if the `STATE_KEY` environment variable is absent:
66+
67+
```
68+
FATAL: STATE_KEY environment variable is required and must be identical across Broker and Gateway
69+
```
70+
71+
This prevents a class of silent misconfiguration where a randomly-generated key would cause all OAuth callbacks to fail with invalid state errors after any service restart. In production, `STATE_KEY` must be:
72+
73+
1. A 32-byte cryptographically random value, Base64 encoded.
74+
2. **Identical** on both the Broker and all Gateway instances.
75+
3. Stored as a managed secret (e.g., Google Secret Manager, AWS Secrets Manager) — not hardcoded.
76+

0 commit comments

Comments
 (0)