Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
530f365
feat: implement security-as-code and audit logging (Issue #11)
sangalo20 May 5, 2026
091eaa6
fix(review): address anti-gravity review feedback for Issue #11
sangalo20 May 5, 2026
1fef9ee
docs: add Security-as-Code and Audit Log documentation (Issue #11)
sangalo20 May 5, 2026
fa64a4f
fix(copilot-review): address all 10 Copilot PR comments
sangalo20 May 5, 2026
ceea6cd
fix(ci): use repo comparison to guard plan step for forked PRs
sangalo20 May 5, 2026
8446fb3
fix(cli): fetch full profile for accurate drift detection
sangalo20 May 5, 2026
1adc5d9
fix(cli): fail fast on unset environment variables during manifest apply
sangalo20 May 5, 2026
18c82e5
fix(cli): use PATCH instead of PUT to prevent nulling omitted columns
sangalo20 May 5, 2026
483dbc4
fix(audit): return json.Marshal errors instead of silently discarding
sangalo20 May 5, 2026
ca9f1d2
docs: align audit event types with actual code emissions
sangalo20 May 5, 2026
9232602
docs: fix audit endpoint path in security-model.md (/v1/audit → /audit)
sangalo20 May 5, 2026
ee00e09
docs: fix providers endpoint path in security-as-code.md (/v1/provide…
sangalo20 May 5, 2026
4814ac2
perf(cli): fetch provider profiles concurrently with bounded worker pool
sangalo20 May 5, 2026
207fc74
feat(cli): show field-level diff in plan output for UPDATE actions
sangalo20 May 5, 2026
fb5ae72
chore(cli): run go mod tidy to fix indirect annotation on yaml.v3
sangalo20 May 5, 2026
bba0861
fix(ci): pass provider credential secrets to CLI workflow env
sangalo20 May 5, 2026
29b1138
chore: remove CI workflow from open repo, update docs
sangalo20 May 5, 2026
ba238c9
fix: address Copilot review round 2 (14 comments)
sangalo20 May 5, 2026
79bf0ee
fix: address Copilot review round 3 (5 comments)
sangalo20 May 5, 2026
75cfe25
fix: final Copilot review pass
sangalo20 May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/guides/managing-providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

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

!!! tip "Prefer a GitOps workflow?"
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.


## Provider Types

The broker supports two primary types of providers:
Expand Down
209 changes: 209 additions & 0 deletions docs/guides/security-as-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# Security-as-Code: Declarative Provider Management

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.

!!! tip "Why this matters"
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.

---

## How It Works

`nexus-cli` follows a **plan → confirm → apply** workflow:

1. **Fetches** the current live state from `GET /providers`.
2. **Diffs** it against your `nexus-providers.yaml` manifest.
3. **Prints** a human-readable plan showing creates, updates, and orphaned providers.
4. **Applies** the changes only after you confirm with `yes` (or non-interactively in CI).

---

## Installation

Build from source within the repository:

```bash
cd nexus-cli
go build -o nexus-cli .
```

Or install directly:

```bash
go install github.com/Prescott-Data/nexus-framework/nexus-cli@latest
```

---

## Configuration

`nexus-cli` is configured via environment variables:

| Variable | Description | Default |
| :--- | :--- | :--- |
| `BROKER_BASE_URL` | Base URL of the Nexus Broker | `http://localhost:8080` |
| `API_KEY` | API key for Broker authentication | *(none)* |

---

## The Provider Manifest

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.

Environment variables are expanded at runtime, so secrets never need to be hardcoded.

```yaml title="nexus-providers.yaml"
providers:
- name: google-workspace
auth_type: oauth2
client_id: "${GOOGLE_CLIENT_ID}"
client_secret: "${GOOGLE_CLIENT_SECRET}"
issuer: "https://accounts.google.com"
enable_discovery: true
scopes:
- openid
- email
- profile
- offline_access

- name: github
auth_type: oauth2
client_id: "${GITHUB_CLIENT_ID}"
client_secret: "${GITHUB_CLIENT_SECRET}"
auth_url: "https://github.com/login/oauth/authorize"
token_url: "https://github.com/login/oauth/access_token"
api_base_url: "https://api.github.com"
enable_discovery: false
scopes:
- read:user
- user:email
```

### Manifest Fields

| Field | Type | Description |
| :--- | :--- | :--- |
| `name` | string | Unique provider name (used as the reconciliation key) |
| `auth_type` | string | `oauth2` or `api_key` |
| `client_id` | string | OAuth client ID |
| `client_secret` | string | OAuth client secret |
| `issuer` | string | OIDC issuer URL for auto-discovery |
| `auth_url` | string | Authorization endpoint (if not using discovery) |
| `token_url` | string | Token endpoint (if not using discovery) |
| `api_base_url` | string | Provider API root URL |
| `enable_discovery` | bool | Use OIDC discovery if `true` |
| `scopes` | list | Default scopes to request |
| `params` | map | Provider-specific extra parameters |

---

## Commands

### `plan` — Preview Changes

Show what would change without making any mutations:

```bash
nexus-cli plan
# Or with a custom manifest path:
nexus-cli plan --file ./path/to/nexus-providers.yaml
```

**Example output:**

```
Read 2 providers from nexus-providers.yaml

--- Execution Plan ---
+ CREATE : github
~ UPDATE : google-workspace
! ORPHAN : old-slack-provider (would be deleted if --prune was passed)

Plan complete. Run 'nexus-cli apply' to perform these actions.
```

The symbols mean:

| Symbol | Action |
| :--- | :--- |
| `+` | Provider will be created |
| `~` | Provider will be updated |
| `-` | Provider will be deleted (only shown with `--prune`) |
| `!` | Provider exists in live state but not in manifest (orphan) |

### `apply` — Apply Changes

Apply the manifest, with an interactive confirmation prompt:

```bash
nexus-cli apply
```

```
Read 2 providers from nexus-providers.yaml

--- Execution Plan ---
+ CREATE : github
~ UPDATE : google-workspace

Do you want to perform these actions?
Nexus will perform the actions described above.
Only 'yes' will be accepted to approve.

Enter a value: yes

--- Applying Changes ---
Creating github... OK
Updating google-workspace... OK
```

#### Flags

| Flag | Default | Description |
| :--- | :--- | :--- |
| `--file` | `nexus-providers.yaml` | Path to the manifest file |
| `--prune` | `false` | Also delete providers in live state not in the manifest |

!!! warning "Using `--prune`"
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.

---

## CI/CD Integration (Optional)

`nexus-cli` is a standalone binary — you can run it from your laptop, a bastion host, or a CI pipeline. If you want to integrate it into your own CI/CD, here's a recommended pattern:

- **On pull requests**: run `nexus-cli plan` as an informational check so reviewers can see what would change.
- **Apply manually**: use a `workflow_dispatch` trigger or run `nexus-cli apply` from a trusted environment when you're ready.

> **Note:** Auto-applying on merge is discouraged. Provider configurations are live operational data — you should always review a plan before applying.

Comment on lines +172 to +180
### Example GitHub Actions Snippet

```yaml
# Add this to your internal repo's workflow — not the open-source framework repo.
- name: Plan
env:
BROKER_BASE_URL: ${{ secrets.BROKER_BASE_URL }}
API_KEY: ${{ secrets.BROKER_API_KEY }}
# Add all env vars referenced in your manifest
run: ./nexus-cli plan
```

### Required Environment Variables

| Variable | Description |
| :--- | :--- |
| `BROKER_BASE_URL` | URL of your target Nexus Broker (staging, prod, etc.) |
| `API_KEY` | API key for Broker authentication |
| `*_CLIENT_ID` / `*_CLIENT_SECRET` | Any provider credentials referenced via `${...}` in your manifest |

---

## Best Practices

1. **Treat `nexus-providers.yaml` as infrastructure code** — require PR reviews for all changes.
2. **Never hardcode secrets** — always use `${ENV_VAR}` expansion and inject via CI secrets.
3. **Start without `--prune`** — let orphans accumulate warnings first so you can audit them intentionally before deletion.
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.
5. **All mutations are audited** — every create, update, or delete applied by `nexus-cli` is recorded in the [Audit Log](../reference/audit-log.md).
129 changes: 129 additions & 0 deletions docs/reference/audit-log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Audit Log Reference

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.

This provides a queryable history of who changed what and when, which is essential for operating Nexus as critical infrastructure.

---

## Audited Events

| Event Type | Trigger |
| :--- | :--- |
| `provider.created` | A new provider profile is registered via `POST /providers` |
| `provider.updated` | A provider's configuration is modified (`PUT` or `PATCH`) |
| `provider.deleted` | A provider is deleted (by ID or by name) |
| `oauth_flow_completed` | An OAuth callback completes successfully and a connection is established |
| `token_exchange_failed` | The authorization code → token exchange failed |
| `token_storage_failed` | Tokens were exchanged but could not be encrypted/stored |
| `token_retrieved` | A downstream service fetched a connection's token via `GET /connections/{id}/token` |
| `token_retrieval_failed` | A token fetch failed (not found, decryption error, inactive connection, etc.) |
| `token_refresh_fatal` | A refresh token was rejected by the provider (4xx), connection moved to `attention` |
| `oauth_error` | The provider returned an error on the OAuth callback (e.g. `access_denied`) |

Comment on lines +11 to +23
---

## Query the Audit Log

```
GET /audit
```

Returns recent audit events in descending chronological order. This endpoint is protected by `ApiKeyMiddleware`.

> **Note:** The Nexus Broker API is unversioned — all routes are mounted at the root (e.g., `/providers`, `/audit`). The `/v1/audit` path referenced elsewhere is aspirational and will apply if/when the Broker adopts a versioned API prefix.

### Query Parameters

| Parameter | Type | Description |
| :--- | :--- | :--- |
| `event_type` | string | Filter by event type (e.g. `provider.deleted`) |
| `since` | string | RFC3339 timestamp — only return events after this time |
| `limit` | integer | Maximum records to return (default: `50`, max: `1000`) |

### Examples

**Fetch the last 50 audit events:**
```bash
curl -s "http://localhost:8080/audit" \
-H "X-API-Key: <YOUR_API_KEY>" | jq .
```

**Filter by event type:**
```bash
curl -s "http://localhost:8080/audit?event_type=provider.deleted" \
-H "X-API-Key: <YOUR_API_KEY>" | jq .
```

**Filter by time window:**
```bash
curl -s "http://localhost:8080/audit?since=2026-05-01T00:00:00Z&limit=100" \
-H "X-API-Key: <YOUR_API_KEY>" | jq .
```

**Combine filters:**
```bash
curl -s "http://localhost:8080/audit?event_type=provider.created&since=2026-05-01T00:00:00Z" \
-H "X-API-Key: <YOUR_API_KEY>" | jq .
```

---

## Response Schema

```json
[
{
"id": "a1b2c3d4-...",
"connection_id": "f5e6d7c8-...",
"event_type": "oauth_flow_completed",
"event_data": "{\"provider_id\": \"...\"}",
"ip_address": "10.0.0.1",
"user_agent": "nexus-gateway/1.0",
"created_at": "2026-05-05T10:30:00Z"
},
{
"id": "b2c3d4e5-...",
"event_type": "provider.deleted",
"event_data": "{\"provider_id\": \"...\", \"provider_name\": \"old-slack\"}",
"ip_address": "192.168.1.5",
"user_agent": "curl/7.88.1",
"created_at": "2026-05-05T09:15:00Z"
}
Comment on lines +74 to +92
]
```

> **Note:** Fields with `omitempty` (`connection_id`, `event_data`, `ip_address`, `user_agent`) are omitted from the response when their value is null, rather than being rendered as `null`.

### Field Descriptions

| Field | Type | Description |
| :--- | :--- | :--- |
| `id` | UUID | Unique audit event identifier |
| `connection_id` | UUID \| null | Associated connection, if applicable |
| `event_type` | string | The event type (see table above) |
| `event_data` | string \| null | JSON payload with event-specific context |
| `ip_address` | string \| null | IP of the caller (respects `X-Forwarded-For`) |
| `user_agent` | string \| null | User-Agent of the caller |
| `created_at` | RFC3339 | Timestamp of the event |

---

## Database

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.

!!! note "Retention Policy"
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).

---

## Audit via `nexus-cli`

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.

```bash
# See all provider changes from a CI apply run
curl -s "http://localhost:8080/audit?event_type=provider.created&since=2026-05-05T13:00:00Z" \
-H "X-API-Key: <YOUR_API_KEY>" | jq .
```
32 changes: 32 additions & 0 deletions docs/reference/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,35 @@ The Broker supports an `ALLOWED_CIDRS` policy. In production, this should be res

### mTLS (Roadmap)
Future versions of Nexus will support mutual TLS between the Gateway and Broker for cryptographically enforced identity beyond API keys.

---

## Audit Trail

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:

- The **event type** (`provider.created`, `provider.deleted`, `connection.created`, etc.)
- **Structured event data** (provider ID, name, workspace ID)
- The **caller IP address** and **User-Agent**

This audit log is queryable via the [`GET /audit`](audit-log.md) endpoint and is the foundational building block for compliance, forensic analysis, and detecting unauthorized mutations.
Comment on lines +50 to +56

!!! tip "GitOps for Auditability"
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.

---

## `STATE_KEY` Startup Guard

Both the Broker and Gateway will **fatal-exit at startup** if the `STATE_KEY` environment variable is absent:

```
FATAL: STATE_KEY environment variable is required and must be identical across Broker and Gateway
```

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:

1. A 32-byte cryptographically random value, Base64 encoded.
2. **Identical** on both the Broker and all Gateway instances.
3. Stored as a managed secret (e.g., Google Secret Manager, AWS Secrets Manager) — not hardcoded.

Loading
Loading