|
| 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