Add policy CRUD, cert management, and web UI updates
- Add PUT /v1/policy/rule endpoint for updating policy rules; expose full policy CRUD through the web UI with a dedicated policy page - Add certificate revoke, delete, and get-cert to CA engine and wire REST + gRPC routes; fix missing interceptor registrations - Update ARCHITECTURE.md to reflect v2 gRPC as the active implementation, document ACME endpoints, correct CA permission levels, and add policy/cert management route tables - Add POLICY.md documenting the priority-based ACL engine design - Add web/templates/policy.html for policy management UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
206
POLICY.md
Normal file
206
POLICY.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# Metacrypt Policy Engine
|
||||
|
||||
Metacrypt includes a priority-based access control policy engine that governs
|
||||
which authenticated users may perform which operations on which engine
|
||||
resources. This document explains the policy model, rule structure, evaluation
|
||||
algorithm, and how to manage rules via the API and web UI.
|
||||
|
||||
## Overview
|
||||
|
||||
Every request to `POST /v1/engine/request` is evaluated against the policy
|
||||
engine before being dispatched to the underlying cryptographic engine. The
|
||||
policy engine enforces a **default-deny** posture: unless a matching allow rule
|
||||
exists, non-admin users are denied.
|
||||
|
||||
**Admin bypass**: Users with the `admin` role in MCIAS always pass policy
|
||||
evaluation unconditionally. Policy rules only affect non-admin users.
|
||||
|
||||
## Rule Structure
|
||||
|
||||
A policy rule is a JSON object with the following fields:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------------|-----------------|----------|----------------------------------------------------------|
|
||||
| `id` | string | Yes | Unique identifier for the rule |
|
||||
| `priority` | integer | Yes | Evaluation order; lower number = higher priority |
|
||||
| `effect` | `"allow"` or `"deny"` | Yes | Decision when this rule matches |
|
||||
| `usernames` | []string | No | Match specific usernames (case-insensitive). Empty = any |
|
||||
| `roles` | []string | No | Match any of these roles (case-insensitive). Empty = any |
|
||||
| `resources` | []string | No | Glob patterns for the resource path. Empty = any |
|
||||
| `actions` | []string | No | `"read"` or `"write"`. Empty = any |
|
||||
|
||||
### Resource Paths
|
||||
|
||||
Resources follow the pattern `engine/<mount>/<operation>`. For example:
|
||||
|
||||
- `engine/pki/list-certs` — list certificates on the `pki` mount
|
||||
- `engine/pki/issue` — issue a certificate on the `pki` mount
|
||||
- `engine/transit/*` — any operation on the `transit` mount
|
||||
|
||||
Glob patterns use standard filepath matching (`*` matches within a path
|
||||
segment; `**` is not supported — use `engine/pki/*` to match all operations on
|
||||
a mount).
|
||||
|
||||
### Actions
|
||||
|
||||
Operations are classified as either `read` or `write`:
|
||||
|
||||
| Action | Operations |
|
||||
|---------|-------------------------------------------------------------------|
|
||||
| `read` | `list-issuers`, `list-certs`, `get-cert`, `get-root`, `get-chain`, `get-issuer` |
|
||||
| `write` | All other operations (`issue`, `renew`, `sign-csr`, `create-issuer`, `delete-issuer`, etc.) |
|
||||
|
||||
## Evaluation Algorithm
|
||||
|
||||
1. If the caller has the `admin` role → **allow** immediately (bypass all rules).
|
||||
2. Load all rules from the barrier.
|
||||
3. Sort rules by `priority` ascending (lower number = evaluated first).
|
||||
4. Iterate rules in order; for each rule, check:
|
||||
- If `usernames` is non-empty, the caller's username must match one entry.
|
||||
- If `roles` is non-empty, the caller must have at least one matching role.
|
||||
- If `resources` is non-empty, the resource must match at least one glob pattern.
|
||||
- If `actions` is non-empty, the action must match one entry.
|
||||
- All specified conditions must match (AND logic within a rule).
|
||||
5. The **first matching rule** wins; return its `effect`.
|
||||
6. If no rule matches → **deny** (default deny).
|
||||
|
||||
## Managing Rules via the REST API
|
||||
|
||||
All policy endpoints require an authenticated admin token
|
||||
(`Authorization: Bearer <token>`).
|
||||
|
||||
### List Rules
|
||||
|
||||
```
|
||||
GET /v1/policy/rules
|
||||
```
|
||||
|
||||
Returns a JSON array of all policy rules.
|
||||
|
||||
### Create a Rule
|
||||
|
||||
```
|
||||
POST /v1/policy/rules
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "allow-users-read-pki",
|
||||
"priority": 10,
|
||||
"effect": "allow",
|
||||
"roles": ["user"],
|
||||
"resources": ["engine/pki/*"],
|
||||
"actions": ["read"]
|
||||
}
|
||||
```
|
||||
|
||||
Returns `201 Created` with the created rule on success.
|
||||
|
||||
### Get a Rule
|
||||
|
||||
```
|
||||
GET /v1/policy/rule?id=allow-users-read-pki
|
||||
```
|
||||
|
||||
### Update a Rule
|
||||
|
||||
```
|
||||
PUT /v1/policy/rule?id=allow-users-read-pki
|
||||
Content-Type: application/json
|
||||
|
||||
{ ... updated rule fields ... }
|
||||
```
|
||||
|
||||
### Delete a Rule
|
||||
|
||||
```
|
||||
DELETE /v1/policy/rule?id=allow-users-read-pki
|
||||
```
|
||||
|
||||
## Managing Rules via the Web UI
|
||||
|
||||
Navigate to **Policy** in the top navigation bar (visible to admin users only).
|
||||
|
||||
The policy page shows:
|
||||
|
||||
- A table of all active rules with their ID, priority, effect, and match
|
||||
conditions.
|
||||
- A **Create Rule** form (expandable) for adding new rules.
|
||||
- A **Delete** button on each row to remove a rule.
|
||||
|
||||
## Managing Rules via gRPC
|
||||
|
||||
The `PolicyService` gRPC service (defined in `proto/metacrypt/v2/policy.proto`)
|
||||
provides equivalent operations:
|
||||
|
||||
| RPC | Description |
|
||||
|------------------|--------------------------|
|
||||
| `CreatePolicy` | Create a new rule |
|
||||
| `ListPolicies` | List all rules |
|
||||
| `GetPolicy` | Get a rule by ID |
|
||||
| `DeletePolicy` | Delete a rule by ID |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Allow users to read PKI resources
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "allow-users-read-pki",
|
||||
"priority": 10,
|
||||
"effect": "allow",
|
||||
"roles": ["user"],
|
||||
"resources": ["engine/pki/*"],
|
||||
"actions": ["read"]
|
||||
}
|
||||
```
|
||||
|
||||
### Allow a specific user to issue certificates
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "allow-alice-issue",
|
||||
"priority": 5,
|
||||
"effect": "allow",
|
||||
"usernames": ["alice"],
|
||||
"resources": ["engine/pki/issue"],
|
||||
"actions": ["write"]
|
||||
}
|
||||
```
|
||||
|
||||
### Deny guests all access to a mount
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "deny-guests-transit",
|
||||
"priority": 1,
|
||||
"effect": "deny",
|
||||
"roles": ["guest"],
|
||||
"resources": ["engine/transit/*"]
|
||||
}
|
||||
```
|
||||
|
||||
### Allow users read-only access to all mounts
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "allow-users-read-all",
|
||||
"priority": 50,
|
||||
"effect": "allow",
|
||||
"roles": ["user"],
|
||||
"actions": ["read"]
|
||||
}
|
||||
```
|
||||
|
||||
## Role Summary
|
||||
|
||||
| Role | Default access |
|
||||
|---------|-----------------------------------------------------|
|
||||
| `admin` | Full access to everything (policy bypass) |
|
||||
| `user` | Denied by default; grant access via policy rules |
|
||||
| `guest` | Denied by default; grant access via policy rules |
|
||||
|
||||
## Storage
|
||||
|
||||
Policy rules are stored in the encrypted barrier under the prefix
|
||||
`policy/rules/<id>`. They are encrypted at rest with the master encryption key
|
||||
and are only accessible when the service is unsealed.
|
||||
Reference in New Issue
Block a user