Skip to content

Commit fb5d9df

Browse files
docs: rewrite developer documentation
Replace old docs with comprehensive technical references: API, architecture, authentication, configuration, deployment, development, getting started, observability, and security.
1 parent f36a564 commit fb5d9df

19 files changed

Lines changed: 1777 additions & 3558 deletions

docs/API.md

Lines changed: 515 additions & 0 deletions
Large diffs are not rendered by default.

docs/AUTHENTICATION.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Authentication
2+
3+
Overview of all authentication and authorization mechanisms in the control plane.
4+
5+
## Web UI (Session Auth)
6+
7+
Browser-based login via `ZentinelCpWeb.Plugs.Auth`.
8+
9+
- Sessions use signed tokens stored in the `user_tokens` table (context: `"session"`)
10+
- Token validation: `ZentinelCp.Accounts.get_user_by_session_token/1`
11+
- Logout clears both the database record and session cookie
12+
- LiveView socket IDs tied to user sessions for real-time updates
13+
14+
Login: `POST /login` with `email` and `password` form fields.
15+
Registration: `POST /register` (or navigate to `/register` in browser).
16+
17+
## API Key Authentication
18+
19+
For operator and CI/CD access to the REST API.
20+
21+
```
22+
Authorization: Bearer <api_key>
23+
```
24+
25+
Plug: `ZentinelCpWeb.Plugs.ApiAuth`
26+
27+
### Key Generation
28+
29+
- 32 bytes of cryptographically random data, Base64-URL encoded
30+
- SHA256 hash stored in DB — raw key shown **once** at creation, cannot be retrieved later
31+
- `key_prefix` (first 8 chars) stored for identification in UI
32+
33+
### Creating an API Key
34+
35+
| Property | Required | Description |
36+
|----------|----------|-------------|
37+
| `name` | Yes | Display name |
38+
| `scopes` | No | Permission scopes (empty = full access) |
39+
| `project_id` | No | Restrict to specific project |
40+
| `expires_at` | No | Auto-expiration date |
41+
42+
### Scopes
43+
44+
| Scope | Access |
45+
|-------|--------|
46+
| `nodes:read` | List nodes, view details, stats |
47+
| `nodes:write` | Register, delete, drift operations |
48+
| `bundles:read` | List, view, download, verify, SBOM |
49+
| `bundles:write` | Create, assign, revoke |
50+
| `rollouts:read` | List, view rollout details |
51+
| `rollouts:write` | Create, pause, resume, cancel, rollback |
52+
| `services:read` | List services, upstreams, certs, etc. |
53+
| `services:write` | Create/update/delete services and related |
54+
| `api_keys:admin` | Create, list, revoke, delete API keys |
55+
56+
Keys with empty scopes have full access (backward compatibility for legacy keys).
57+
58+
### Project Scoping
59+
60+
When a key has `project_id` set, it can only access resources within that project. The `RequireScope` plug validates project match.
61+
62+
### Key Lifecycle
63+
64+
```
65+
Created (active) → Revoked (immediate rejection)
66+
→ Expired (auto-rejected past expires_at)
67+
→ Deleted
68+
```
69+
70+
`last_used_at` updated on every successful authentication.
71+
72+
## Node Authentication
73+
74+
Zentinel proxy nodes authenticate using one of two methods:
75+
76+
### Static Node Key
77+
78+
Simple shared secret, suitable for getting started:
79+
80+
```
81+
X-Zentinel-Node-Key: <base64-url-key>
82+
```
83+
84+
- Generated at registration: 32 bytes random data, Base64-URL encoded
85+
- SHA256 hash stored in DB
86+
- Validated by `ZentinelCp.Nodes.Node.valid_node_key?/2`
87+
88+
### JWT Token (Recommended for Production)
89+
90+
Short-lived token exchanged from the static key:
91+
92+
```
93+
Authorization: Bearer <jwt>
94+
```
95+
96+
**Token exchange:**
97+
```bash
98+
curl -X POST http://localhost:4000/api/v1/nodes/:node_id/token \
99+
-H "X-Zentinel-Node-Key: <node_key>"
100+
```
101+
102+
Response:
103+
```json
104+
{
105+
"token": "eyJ...",
106+
"token_type": "Bearer",
107+
"expires_at": "2026-02-21T19:00:00Z"
108+
}
109+
```
110+
111+
**JWT claims:**
112+
113+
| Claim | Value |
114+
|-------|-------|
115+
| `sub` | Node ID |
116+
| `prj` | Project ID |
117+
| `org` | Organization ID |
118+
| `kid` | Signing key ID |
119+
| `exp` | Expiration (12 hours from issuance) |
120+
121+
**Algorithm:** Ed25519 (EDDSA). Signing keys managed per organization in `signing_keys` table.
122+
123+
**Verification:** `ZentinelCp.Auth.verify_node_token/1` looks up the key by `kid`, verifies signature.
124+
125+
Plug: `ZentinelCpWeb.Plugs.NodeAuth` (accepts both static key and JWT).
126+
127+
## Signing Keys
128+
129+
Ed25519 key pairs for JWT issuance, managed per organization.
130+
131+
- **Create**: Generate new key pair — returns key ID
132+
- **List**: All signing keys (active and inactive)
133+
- **Deactivate**: Mark inactive (existing tokens valid until expiry)
134+
- **Expiration**: Optional `expires_at` for automatic rotation
135+
136+
At least one active signing key must exist when issuing tokens.
137+
138+
## TOTP Multi-Factor Authentication
139+
140+
TOTP-based MFA via `nimble_totp` library. Schema: `ZentinelCp.Accounts.UserTotp`.
141+
142+
1. Generate shared secret + QR code (`otpauth://` URI)
143+
2. User scans with authenticator app, enters verification code
144+
3. 10 single-use recovery codes generated
145+
4. Subsequent logins require TOTP code after password
146+
147+
Managed at `/profile` in the web UI. Recovery codes can be regenerated at any time.
148+
149+
## SSO Integration
150+
151+
### OIDC (OpenID Connect)
152+
153+
Authorization Code with PKCE flow. Controller: `ZentinelCpWeb.Auth.SsoController`.
154+
155+
| Setting | Description |
156+
|---------|-------------|
157+
| `client_id` | OIDC client identifier |
158+
| `client_secret` | Client secret (encrypted at rest) |
159+
| `issuer` | Provider issuer URL |
160+
| `authorize_url` | Authorization endpoint |
161+
| `token_url` | Token exchange endpoint |
162+
| `userinfo_url` | User info endpoint |
163+
| `scopes` | Requested OIDC scopes |
164+
| `group_mapping` | Map IdP groups → org roles |
165+
| `fallback_to_password` | Allow password login as fallback |
166+
167+
### SAML 2.0
168+
169+
Via `samly` library. Config: `ZentinelCp.Auth.SamlProvider`.
170+
171+
| Setting | Description |
172+
|---------|-------------|
173+
| `idp_metadata_url` | IdP metadata URL |
174+
| `idp_sso_url` | SSO endpoint |
175+
| `idp_cert_pem` | IdP signing certificate |
176+
| `sp_entity_id` | Service Provider entity ID |
177+
| `assertion_consumer_service_url` | ACS callback URL |
178+
| `group_mapping` | Map IdP groups → org roles |
179+
| `fallback_to_password` | Allow password login as fallback |
180+
181+
### Just-In-Time Provisioning
182+
183+
On first SSO login:
184+
185+
1. User account created with random password
186+
2. Account marked as confirmed
187+
3. Org membership created with role from group mapping
188+
4. SSO provider type and subject identifier recorded
189+
190+
### Group-to-Role Mapping
191+
192+
```json
193+
{
194+
"engineering-admins": "admin",
195+
"engineering": "operator",
196+
"default": "reader"
197+
}
198+
```
199+
200+
## Rate Limiting
201+
202+
Token-bucket rate limiting on API endpoints.
203+
204+
- **Key**: API key ID (if authenticated) or client IP
205+
- **Scope**: Different limits per endpoint category
206+
- **Headers**: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
207+
- **429 response**: Returned with `retry_after` value when limit exceeded
208+
209+
Plug: `ZentinelCpWeb.Plugs.RateLimit`.

0 commit comments

Comments
 (0)