Web UI: Added browser-based management for all three remaining engines (SSH CA, Transit, User E2E). Includes gRPC client wiring, handler files, 7 HTML templates, dashboard mount forms, and conditional navigation links. Fixed REST API routes to match design specs (SSH CA cert singular paths, Transit PATCH for update-key-config). Security audit: Conducted full-system audit covering crypto core, all engine implementations, API servers, policy engine, auth, deployment, and documentation. Identified 42 new findings (#39-#80) across all severity levels. Remediation of all 8 High findings: - #68: Replaced 14 JSON-injection-vulnerable error responses with safe json.Encoder via writeJSONError helper - #48: Added two-layer path traversal defense (barrier validatePath rejects ".." segments; engine ValidateName enforces safe name pattern) - #39: Extended RLock through entire crypto operations in barrier Get/Put/Delete/List to eliminate TOCTOU race with Seal - #40: Unified ReWrapKeys and seal_config UPDATE into single SQLite transaction to prevent irrecoverable data loss on crash during MEK rotation - #49: Added resolveTTL to CA engine enforcing issuer MaxTTL ceiling on handleIssue and handleSignCSR - #61: Store raw ECDH private key bytes in userState for effective zeroization on Seal - #62: Fixed user engine policy resource path from mountPath to mountName() so policy rules match correctly - #69: Added newPolicyChecker helper and passed service-level policy evaluation to all 25 typed REST handler engine.Request structs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
845 lines
40 KiB
Markdown
845 lines
40 KiB
Markdown
# Web UI Implementation Plan: SSH CA, Transit, and User Engines
|
|
|
|
## Overview
|
|
|
|
Three engines (SSH CA, Transit, User) are fully implemented at the core,
|
|
gRPC, and REST layers but have no web UI. This plan adds browser-based
|
|
management for each, following the patterns established by the PKI engine UI.
|
|
|
|
## Architecture
|
|
|
|
The web UI is served by `metacrypt-web`, a separate binary that talks to the
|
|
API server over gRPC. All data access flows through the gRPC client — the web
|
|
server has no direct database or barrier access. Authorization is enforced by
|
|
the API server; the web UI only controls visibility (e.g. hiding admin-only
|
|
forms from non-admin users).
|
|
|
|
### Existing Patterns (from PKI reference)
|
|
|
|
| Concern | Pattern |
|
|
|---------|---------|
|
|
| Template composition | `layout.html` defines `"layout"` block; page templates define `"title"` and `"content"` blocks |
|
|
| Template rendering | `renderTemplate(w, "page.html", data)` — parses `layout.html` + page template, injects CSRF func |
|
|
| gRPC calls | `ws.vault.Method(ctx, token)` via `vaultBackend` interface; token from cookie |
|
|
| CSRF | Signed double-submit cookie; `{{csrfField}}` in every form |
|
|
| Error display | `{{if .Error}}<div class="error">{{.Error}}</div>{{end}}` at top of content |
|
|
| Success display | `<div class="success">...</div>` inline after action |
|
|
| Mount discovery | `findCAMount()` pattern — iterate `ListMounts()`, match on `.Type` |
|
|
| Tables | `.table-wrapper` > `<table>` with `<thead>`/`<tbody>` |
|
|
| Detail views | `.card` with `.card-title` + `.kv-table` for metadata |
|
|
| Admin actions | `{{if .IsAdmin}}` guards around admin-only cards |
|
|
| Forms | `.form-row` > `.form-group` > `<label>` + `<input>` |
|
|
| Navigation | Breadcrumb in `.page-meta`: `← Dashboard` |
|
|
|
|
---
|
|
|
|
## Shared Changes
|
|
|
|
### 1. Navigation (layout.html)
|
|
|
|
Add engine links to the top nav, conditionally displayed based on mounted
|
|
engines. The template data already includes `Username` and `IsAdmin`; extend
|
|
it with engine availability flags.
|
|
|
|
```html
|
|
<!-- After PKI link -->
|
|
{{if .HasSSHCA}}<a href="/sshca" class="btn btn-ghost btn-sm">SSH CA</a>{{end}}
|
|
{{if .HasTransit}}<a href="/transit" class="btn btn-ghost btn-sm">Transit</a>{{end}}
|
|
{{if .HasUser}}<a href="/user" class="btn btn-ghost btn-sm">User Crypto</a>{{end}}
|
|
```
|
|
|
|
To populate these flags without an extra gRPC call per page, cache mount
|
|
types in the web server after the first `ListMounts()` call per request (the
|
|
`requireAuth` middleware already validates the token — extend it to also
|
|
populate a `mountTypes` set on the request context).
|
|
|
|
### 2. Dashboard (dashboard.html)
|
|
|
|
Extend the mount table to link all engine types, not just CA:
|
|
|
|
```html
|
|
{{if eq (printf "%s" .Type) "ca"}}
|
|
<a href="/pki">{{.Name}}</a>
|
|
{{else if eq (printf "%s" .Type) "sshca"}}
|
|
<a href="/sshca">{{.Name}}</a>
|
|
{{else if eq (printf "%s" .Type) "transit"}}
|
|
<a href="/transit">{{.Name}}</a>
|
|
{{else if eq (printf "%s" .Type) "user"}}
|
|
<a href="/user">{{.Name}}</a>
|
|
{{else}}
|
|
{{.Name}}
|
|
{{end}}
|
|
```
|
|
|
|
Add mount forms for the three new engine types (admin only), following the
|
|
existing `<details><summary>Mount a CA engine</summary>` pattern.
|
|
|
|
### 3. gRPC Client (client.go)
|
|
|
|
Add gRPC service clients to `VaultClient`:
|
|
|
|
```go
|
|
type VaultClient struct {
|
|
// ... existing fields ...
|
|
sshca pb.SSHCAServiceClient
|
|
transit pb.TransitServiceClient
|
|
user pb.UserServiceClient
|
|
}
|
|
```
|
|
|
|
Add wrapper request/response types for each engine (see per-engine sections
|
|
below). Follow the existing pattern: thin structs that translate between
|
|
protobuf and template-friendly Go types.
|
|
|
|
### 4. vaultBackend Interface (server.go)
|
|
|
|
Add methods for each engine to the `vaultBackend` interface. Group by engine.
|
|
|
|
### 5. Mount Helpers
|
|
|
|
Add `findSSHCAMount()`, `findTransitMount()`, `findUserMount()` following
|
|
the `findCAMount()` pattern.
|
|
|
|
---
|
|
|
|
## SSH CA Engine Web UI
|
|
|
|
### Route: `/sshca`
|
|
|
|
File: `internal/webserver/routes.go` — register under `r.Route("/sshca", ...)`
|
|
|
|
| Method | Path | Handler | Auth |
|
|
|--------|------|---------|------|
|
|
| GET | `/sshca` | `handleSSHCA` | User |
|
|
| POST | `/sshca/sign-user` | `handleSSHCASignUser` | User |
|
|
| POST | `/sshca/sign-host` | `handleSSHCASignHost` | User |
|
|
| GET | `/sshca/cert/{serial}` | `handleSSHCACertDetail` | User |
|
|
| POST | `/sshca/cert/{serial}/revoke` | `handleSSHCACertRevoke` | Admin |
|
|
| POST | `/sshca/cert/{serial}/delete` | `handleSSHCACertDelete` | Admin |
|
|
| POST | `/sshca/profile/create` | `handleSSHCACreateProfile` | Admin |
|
|
| GET | `/sshca/profile/{name}` | `handleSSHCAProfileDetail` | User |
|
|
| POST | `/sshca/profile/{name}/update` | `handleSSHCAUpdateProfile` | Admin |
|
|
| POST | `/sshca/profile/{name}/delete` | `handleSSHCADeleteProfile` | Admin |
|
|
|
|
### Template: `sshca.html`
|
|
|
|
Main page for the SSH CA engine. Structure:
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ SSH CA: {mount} ← Dashboard│
|
|
├─────────────────────────────────────────┤
|
|
│ CA Public Key │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Algorithm: ed25519 │ │
|
|
│ │ Fingerprint: SHA256:xxxx │ │
|
|
│ │ [Download Public Key] │ │
|
|
│ │ ssh-ed25519 AAAA... (readonly area) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Sign User Certificate │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Profile: [select] │ │
|
|
│ │ Public Key: [textarea, ssh format] │ │
|
|
│ │ Principals: [textarea, one/line] │ │
|
|
│ │ TTL: [input, optional] │ │
|
|
│ │ [Sign Certificate] │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Sign Host Certificate │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Profile: [select] │ │
|
|
│ │ Public Key: [textarea, ssh format] │ │
|
|
│ │ Hostnames: [textarea, one/line] │ │
|
|
│ │ TTL: [input, optional] │ │
|
|
│ │ [Sign Certificate] │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Signing Profiles │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Name │ Type │ Max TTL │ Actions │ │
|
|
│ │ ─────┼──────┼─────────┼──────────── │ │
|
|
│ │ eng │ user │ 8760h │ [View] │ │
|
|
│ │ ops │ host │ 720h │ [View] │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
│ [Create Profile] (admin, <details>) │
|
|
├─────────────────────────────────────────┤
|
|
│ Recent Certificates │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Serial │ Type │ Principals │ Issued │ │
|
|
│ │ │ │ │ By │ │
|
|
│ │ ───────┼──────┼────────────┼─────── │ │
|
|
│ │ abc123 │ user │ kyle │ kyle │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ KRL (admin only) │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Version: 7 │ │
|
|
│ │ [Download KRL] │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Template: `sshca_cert_detail.html`
|
|
|
|
Detail view for a single SSH certificate:
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ SSH Certificate: {serial} ← SSH CA │
|
|
├─────────────────────────────────────────┤
|
|
│ Details │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Type: user │ │
|
|
│ │ Serial: abc123 │ │
|
|
│ │ Key ID: kyle@host │ │
|
|
│ │ Principals: kyle, root │ │
|
|
│ │ Profile: eng │ │
|
|
│ │ Valid After: 2026-03-16T... │ │
|
|
│ │ Valid Before: 2026-03-17T... │ │
|
|
│ │ Issued By: kyle │ │
|
|
│ │ Issued At: 2026-03-16T... │ │
|
|
│ │ Revoked: No │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Certificate (readonly textarea) │
|
|
├─────────────────────────────────────────┤
|
|
│ Admin Actions (admin only) │
|
|
│ [Revoke] [Delete] │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Template: `sshca_profile_detail.html`
|
|
|
|
Detail view for a signing profile:
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Profile: {name} ← SSH CA │
|
|
├─────────────────────────────────────────┤
|
|
│ Configuration │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Name: eng │ │
|
|
│ │ Type: user │ │
|
|
│ │ Max TTL: 8760h │ │
|
|
│ │ Default TTL: 24h │ │
|
|
│ │ Allowed Principals: * │ │
|
|
│ │ Force Command: (none) │ │
|
|
│ │ Source Addresses: (none) │ │
|
|
│ │ Allow Agent Fwd: yes │ │
|
|
│ │ Allow Port Fwd: yes │ │
|
|
│ │ Allow PTY: yes │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Edit Profile (admin only, <details>) │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ (form with pre-populated fields) │ │
|
|
│ │ [Update Profile] │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Admin Actions (admin only) │
|
|
│ [Delete Profile] │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Handler File: `internal/webserver/sshca.go`
|
|
|
|
Handlers for all SSH CA web routes. Follow `pki.go` patterns:
|
|
|
|
- `handleSSHCA` — main page: fetch CA pubkey, profiles, recent certs; render
|
|
- `handleSSHCASignUser` / `handleSSHCASignHost` — parse form, call gRPC,
|
|
render success inline (show signed cert in readonly textarea) or re-render
|
|
with error
|
|
- `handleSSHCACertDetail` — fetch cert by serial, render detail page
|
|
- `handleSSHCACertRevoke` / `handleSSHCACertDelete` — admin action, redirect
|
|
back to main page
|
|
- `handleSSHCACreateProfile` — admin form, redirect to profile detail
|
|
- `handleSSHCAProfileDetail` — fetch profile, render detail page
|
|
- `handleSSHCAUpdateProfile` — admin form, redirect to profile detail
|
|
- `handleSSHCADeleteProfile` — admin action, redirect to main page
|
|
|
|
### gRPC Client Methods
|
|
|
|
```go
|
|
// client.go additions
|
|
GetSSHCAPublicKey(ctx, token, mount string) (*SSHCAPublicKey, error)
|
|
SSHCASignUser(ctx, token, mount string, req *SSHCASignRequest) (*SSHCACert, error)
|
|
SSHCASignHost(ctx, token, mount string, req *SSHCASignRequest) (*SSHCACert, error)
|
|
ListSSHCACerts(ctx, token, mount string) ([]SSHCACertSummary, error)
|
|
GetSSHCACert(ctx, token, mount, serial string) (*SSHCACertDetail, error)
|
|
RevokeSSHCACert(ctx, token, mount, serial string) error
|
|
DeleteSSHCACert(ctx, token, mount, serial string) error
|
|
CreateSSHCAProfile(ctx, token, mount string, req *SSHCAProfileRequest) error
|
|
GetSSHCAProfile(ctx, token, mount, name string) (*SSHCAProfile, error)
|
|
ListSSHCAProfiles(ctx, token, mount string) ([]SSHCAProfileSummary, error)
|
|
UpdateSSHCAProfile(ctx, token, mount, name string, req *SSHCAProfileRequest) error
|
|
DeleteSSHCAProfile(ctx, token, mount, name string) error
|
|
GetSSHCAKRL(ctx, token, mount string) ([]byte, uint64, error)
|
|
```
|
|
|
|
### Wrapper Types
|
|
|
|
```go
|
|
type SSHCAPublicKey struct {
|
|
Algorithm string
|
|
PublicKey string // authorized_keys format
|
|
Fingerprint string
|
|
}
|
|
|
|
type SSHCASignRequest struct {
|
|
PublicKey string
|
|
Principals []string
|
|
TTL string
|
|
Profile string
|
|
}
|
|
|
|
type SSHCACert struct {
|
|
Serial string
|
|
Certificate string // SSH certificate text
|
|
}
|
|
|
|
type SSHCACertSummary struct {
|
|
Serial string
|
|
Type string // "user" or "host"
|
|
Principals string // comma-joined
|
|
IssuedBy string
|
|
IssuedAt string
|
|
ExpiresAt string
|
|
Revoked bool
|
|
}
|
|
|
|
type SSHCACertDetail struct {
|
|
SSHCACertSummary
|
|
KeyID string
|
|
Profile string
|
|
Certificate string // full cert text
|
|
}
|
|
|
|
type SSHCAProfileSummary struct {
|
|
Name string
|
|
Type string
|
|
MaxTTL string
|
|
DefaultTTL string
|
|
}
|
|
|
|
type SSHCAProfile struct {
|
|
SSHCAProfileSummary
|
|
AllowedPrincipals []string
|
|
ForceCommand string
|
|
SourceAddresses []string
|
|
AllowAgentFwd bool
|
|
AllowPortFwd bool
|
|
AllowPTY bool
|
|
}
|
|
|
|
type SSHCAProfileRequest struct {
|
|
Name string
|
|
Type string // "user" or "host"
|
|
MaxTTL string
|
|
DefaultTTL string
|
|
AllowedPrincipals []string
|
|
ForceCommand string
|
|
SourceAddresses []string
|
|
AllowAgentFwd bool
|
|
AllowPortFwd bool
|
|
AllowPTY bool
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Transit Engine Web UI
|
|
|
|
### Route: `/transit`
|
|
|
|
| Method | Path | Handler | Auth |
|
|
|--------|------|---------|------|
|
|
| GET | `/transit` | `handleTransit` | User |
|
|
| GET | `/transit/key/{name}` | `handleTransitKeyDetail` | User |
|
|
| POST | `/transit/key/create` | `handleTransitCreateKey` | Admin |
|
|
| POST | `/transit/key/{name}/rotate` | `handleTransitRotateKey` | Admin |
|
|
| POST | `/transit/key/{name}/config` | `handleTransitUpdateConfig` | Admin |
|
|
| POST | `/transit/key/{name}/trim` | `handleTransitTrimKey` | Admin |
|
|
| POST | `/transit/key/{name}/delete` | `handleTransitDeleteKey` | Admin |
|
|
| POST | `/transit/encrypt` | `handleTransitEncrypt` | User |
|
|
| POST | `/transit/decrypt` | `handleTransitDecrypt` | User |
|
|
| POST | `/transit/rewrap` | `handleTransitRewrap` | User |
|
|
| POST | `/transit/sign` | `handleTransitSign` | User |
|
|
| POST | `/transit/verify` | `handleTransitVerify` | User |
|
|
| POST | `/transit/hmac` | `handleTransitHMAC` | User |
|
|
|
|
### Template: `transit.html`
|
|
|
|
Main page. Structure:
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Transit: {mount} ← Dashboard│
|
|
├─────────────────────────────────────────┤
|
|
│ Named Keys │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Name │ Type │ Versions │ │
|
|
│ │ ─────────┼────────────┼─────────── │ │
|
|
│ │ payments │ aes256-gcm │ 3 │ │
|
|
│ │ signing │ ed25519 │ 1 │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
│ [Create Key] (admin, <details>) │
|
|
│ Name: [input] │
|
|
│ Type: [select: aes256-gcm, etc.] │
|
|
│ [Create] │
|
|
├─────────────────────────────────────────┤
|
|
│ Encrypt │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Key: [select from named keys] │ │
|
|
│ │ Plaintext (base64): [textarea] │ │
|
|
│ │ Context (optional): [input] │ │
|
|
│ │ [Encrypt] │ │
|
|
│ │ │ │
|
|
│ │ Result (if present): │ │
|
|
│ │ metacrypt:v1:xxxxx (readonly area) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Decrypt │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Key: [select] │ │
|
|
│ │ Ciphertext: [textarea] │ │
|
|
│ │ Context (optional): [input] │ │
|
|
│ │ [Decrypt] │ │
|
|
│ │ │ │
|
|
│ │ Result (if present): │ │
|
|
│ │ base64 plaintext (readonly area) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Rewrap │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Key: [select] │ │
|
|
│ │ Ciphertext: [textarea] │ │
|
|
│ │ Context (optional): [input] │ │
|
|
│ │ [Rewrap] │ │
|
|
│ │ │ │
|
|
│ │ Result: (new ciphertext) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Sign │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Key: [select, signing keys only] │ │
|
|
│ │ Input (base64): [textarea] │ │
|
|
│ │ [Sign] │ │
|
|
│ │ │ │
|
|
│ │ Result: metacrypt:v1:sig (readonly) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Verify │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Key: [select, signing keys only] │ │
|
|
│ │ Input (base64): [textarea] │ │
|
|
│ │ Signature: [textarea] │ │
|
|
│ │ [Verify] │ │
|
|
│ │ │ │
|
|
│ │ Result: Valid ✓ / Invalid ✗ │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ HMAC │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Key: [select, HMAC keys only] │ │
|
|
│ │ Input (base64): [textarea] │ │
|
|
│ │ [Generate HMAC] │ │
|
|
│ │ │ │
|
|
│ │ Result: metacrypt:v1:hmac (ro) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
Note: Batch operations are API-only. The web UI does not expose batch
|
|
encrypt/decrypt/rewrap — those are designed for programmatic use.
|
|
|
|
### Template: `transit_key_detail.html`
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ Key: {name} ← Transit │
|
|
├─────────────────────────────────────────┤
|
|
│ Key Details │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Name: payments │ │
|
|
│ │ Type: aes256-gcm │ │
|
|
│ │ Latest Version: 3 │ │
|
|
│ │ Min Decrypt Version: 1 │ │
|
|
│ │ Min Encrypt Version: 3 │ │
|
|
│ │ Allow Deletion: no │ │
|
|
│ │ Exportable: no │ │
|
|
│ │ Max Key Versions: 0 (unlimited) │ │
|
|
│ │ Created: 2026-03-16... │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Key Versions │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Version │ Created │ │
|
|
│ │ ────────┼────────────────────────── │ │
|
|
│ │ 3 │ 2026-03-16T10:00:00Z │ │
|
|
│ │ 2 │ 2026-03-10T08:00:00Z │ │
|
|
│ │ 1 │ 2026-03-01T12:00:00Z │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Public Key (signing keys only) │
|
|
│ [textarea, PEM, readonly] │
|
|
├─────────────────────────────────────────┤
|
|
│ Admin Actions (admin only) │
|
|
│ [Rotate Key] │
|
|
│ │
|
|
│ Update Config (<details>) │
|
|
│ Min Decrypt Version: [input] │
|
|
│ Allow Deletion: [checkbox] │
|
|
│ [Update] │
|
|
│ │
|
|
│ Trim Versions (<details>) │
|
|
│ Keep from version: [input] │
|
|
│ [Trim] │
|
|
│ │
|
|
│ [Delete Key] (danger, requires confirm) │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Handler File: `internal/webserver/transit.go`
|
|
|
|
- `handleTransit` — list keys, render main page
|
|
- `handleTransitKeyDetail` — fetch key metadata + versions, render detail
|
|
- `handleTransitCreateKey` — parse form, call gRPC, redirect to key detail
|
|
- `handleTransitRotateKey` — admin, redirect to key detail
|
|
- `handleTransitUpdateConfig` — admin, redirect to key detail
|
|
- `handleTransitTrimKey` — admin, redirect to key detail
|
|
- `handleTransitDeleteKey` — admin, redirect to main page
|
|
- `handleTransitEncrypt` — parse form, call gRPC, re-render main page with
|
|
result in `EncryptResult` field
|
|
- `handleTransitDecrypt` — same pattern, `DecryptResult` field
|
|
- `handleTransitRewrap` — same pattern, `RewrapResult` field
|
|
- `handleTransitSign` — same pattern, `SignResult` field
|
|
- `handleTransitVerify` — same pattern, `VerifyResult` field (boolean)
|
|
- `handleTransitHMAC` — same pattern, `HMACResult` field
|
|
|
|
### gRPC Client Methods
|
|
|
|
```go
|
|
ListTransitKeys(ctx, token, mount string) ([]TransitKeySummary, error)
|
|
GetTransitKey(ctx, token, mount, name string) (*TransitKeyDetail, error)
|
|
CreateTransitKey(ctx, token, mount string, req *CreateTransitKeyRequest) error
|
|
DeleteTransitKey(ctx, token, mount, name string) error
|
|
RotateTransitKey(ctx, token, mount, name string) error
|
|
UpdateTransitKeyConfig(ctx, token, mount, name string, req *UpdateTransitKeyConfigRequest) error
|
|
TrimTransitKey(ctx, token, mount, name string, minVersion int) error
|
|
TransitEncrypt(ctx, token, mount, key string, req *TransitEncryptRequest) (string, error)
|
|
TransitDecrypt(ctx, token, mount, key string, req *TransitDecryptRequest) (string, error)
|
|
TransitRewrap(ctx, token, mount, key string, req *TransitRewrapRequest) (string, error)
|
|
TransitSign(ctx, token, mount, key string, input string) (string, error)
|
|
TransitVerify(ctx, token, mount, key string, input, signature string) (bool, error)
|
|
TransitHMAC(ctx, token, mount, key string, input string) (string, error)
|
|
GetTransitPublicKey(ctx, token, mount, name string) (string, error)
|
|
```
|
|
|
|
### Wrapper Types
|
|
|
|
```go
|
|
type TransitKeySummary struct {
|
|
Name string
|
|
Type string
|
|
LatestVersion int
|
|
}
|
|
|
|
type TransitKeyVersion struct {
|
|
Version int
|
|
CreatedAt string
|
|
}
|
|
|
|
type TransitKeyDetail struct {
|
|
Name string
|
|
Type string
|
|
LatestVersion int
|
|
MinDecryptVersion int
|
|
MinEncryptVersion int
|
|
AllowDeletion bool
|
|
Exportable bool
|
|
MaxKeyVersions int
|
|
CreatedAt string
|
|
Versions []TransitKeyVersion
|
|
PublicKeyPEM string // empty for symmetric keys
|
|
}
|
|
|
|
type CreateTransitKeyRequest struct {
|
|
Name string
|
|
Type string // aes256-gcm, chacha20-poly, ed25519, etc.
|
|
}
|
|
|
|
type UpdateTransitKeyConfigRequest struct {
|
|
MinDecryptVersion int
|
|
AllowDeletion *bool // nil = no change
|
|
}
|
|
|
|
type TransitEncryptRequest struct {
|
|
Plaintext string // base64
|
|
Context string // optional AAD
|
|
}
|
|
|
|
type TransitDecryptRequest struct {
|
|
Ciphertext string // metacrypt:v1:...
|
|
Context string
|
|
}
|
|
|
|
type TransitRewrapRequest struct {
|
|
Ciphertext string
|
|
Context string
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## User Engine Web UI
|
|
|
|
### Route: `/user`
|
|
|
|
| Method | Path | Handler | Auth |
|
|
|--------|------|---------|------|
|
|
| GET | `/user` | `handleUser` | User |
|
|
| POST | `/user/register` | `handleUserRegister` | User |
|
|
| POST | `/user/provision` | `handleUserProvision` | Admin |
|
|
| GET | `/user/key/{username}` | `handleUserKeyDetail` | User |
|
|
| POST | `/user/encrypt` | `handleUserEncrypt` | User |
|
|
| POST | `/user/decrypt` | `handleUserDecrypt` | User |
|
|
| POST | `/user/re-encrypt` | `handleUserReEncrypt` | User |
|
|
| POST | `/user/rotate` | `handleUserRotateKey` | User |
|
|
| POST | `/user/delete/{username}` | `handleUserDeleteUser` | Admin |
|
|
|
|
### Template: `user.html`
|
|
|
|
Main page. Structure:
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ User Crypto: {mount} ← Dashboard│
|
|
├─────────────────────────────────────────┤
|
|
│ Your Key │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ (if registered) │ │
|
|
│ │ Algorithm: x25519 │ │
|
|
│ │ Public Key: base64... │ │
|
|
│ │ │ │
|
|
│ │ (if not registered) │ │
|
|
│ │ You have no keypair. │ │
|
|
│ │ [Register] │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Encrypt │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Recipients: [textarea, one/line] │ │
|
|
│ │ Plaintext (base64): [textarea] │ │
|
|
│ │ Metadata (optional): [input] │ │
|
|
│ │ [Encrypt] │ │
|
|
│ │ │ │
|
|
│ │ Result (if present): │ │
|
|
│ │ JSON envelope (readonly textarea) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Decrypt │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Envelope (JSON): [textarea] │ │
|
|
│ │ [Decrypt] │ │
|
|
│ │ │ │
|
|
│ │ Result (if present): │ │
|
|
│ │ base64 plaintext (readonly) │ │
|
|
│ │ Metadata: ... (readonly) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Re-Encrypt │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Envelope (JSON): [textarea] │ │
|
|
│ │ [Re-Encrypt] │ │
|
|
│ │ │ │
|
|
│ │ Result: updated envelope (readonly) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Key Rotation (danger zone) │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ ⚠ Rotating your key will make all │ │
|
|
│ │ existing envelopes unreadable │ │
|
|
│ │ unless re-encrypted first. │ │
|
|
│ │ [Rotate Key] (confirm dialog) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Registered Users │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Username │ Algorithm │ Actions │ │
|
|
│ │ ───────────┼───────────┼─────────── │ │
|
|
│ │ kyle │ x25519 │ [View Key] │ │
|
|
│ │ alice │ x25519 │ [View Key] │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
│ [Provision User] (admin, <details>) │
|
|
│ Username: [input] │
|
|
│ [Provision] │
|
|
├─────────────────────────────────────────┤
|
|
│ Admin: Delete User (admin only) │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Username: [input] │ │
|
|
│ │ [Delete User] (danger, confirm) │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Template: `user_key_detail.html`
|
|
|
|
```
|
|
┌─────────────────────────────────────────┐
|
|
│ User Key: {username} ← User │
|
|
├─────────────────────────────────────────┤
|
|
│ Public Key │
|
|
│ ┌─────────────────────────────────────┐ │
|
|
│ │ Username: kyle │ │
|
|
│ │ Algorithm: x25519 │ │
|
|
│ │ Public Key: base64... │ │
|
|
│ └─────────────────────────────────────┘ │
|
|
├─────────────────────────────────────────┤
|
|
│ Admin Actions (admin only) │
|
|
│ [Delete User] (danger, confirm) │
|
|
└─────────────────────────────────────────┘
|
|
```
|
|
|
|
### Handler File: `internal/webserver/user.go`
|
|
|
|
- `handleUser` — fetch own key (if registered), list users, render main page
|
|
- `handleUserRegister` — call gRPC, redirect to main page with success
|
|
- `handleUserProvision` — admin, call gRPC, redirect to main page
|
|
- `handleUserKeyDetail` — fetch public key for username, render detail
|
|
- `handleUserEncrypt` — parse form, call gRPC, re-render with envelope result
|
|
- `handleUserDecrypt` — parse form, call gRPC, re-render with plaintext result
|
|
- `handleUserReEncrypt` — parse form, call gRPC, re-render with new envelope
|
|
- `handleUserRotateKey` — confirm + call gRPC, redirect to main page
|
|
- `handleUserDeleteUser` — admin, call gRPC, redirect to main page
|
|
|
|
### gRPC Client Methods
|
|
|
|
```go
|
|
UserRegister(ctx, token, mount string) error
|
|
UserProvision(ctx, token, mount, username string) error
|
|
GetUserPublicKey(ctx, token, mount, username string) (*UserPublicKey, error)
|
|
ListUsers(ctx, token, mount string) ([]UserSummary, error)
|
|
UserEncrypt(ctx, token, mount string, req *UserEncryptRequest) (string, error)
|
|
UserDecrypt(ctx, token, mount string, envelope string) (*UserDecryptResult, error)
|
|
UserReEncrypt(ctx, token, mount string, envelope string) (string, error)
|
|
UserRotateKey(ctx, token, mount string) error
|
|
DeleteUser(ctx, token, mount, username string) error
|
|
```
|
|
|
|
### Wrapper Types
|
|
|
|
```go
|
|
type UserPublicKey struct {
|
|
Username string
|
|
Algorithm string
|
|
PublicKey string // base64
|
|
}
|
|
|
|
type UserSummary struct {
|
|
Username string
|
|
Algorithm string
|
|
}
|
|
|
|
type UserEncryptRequest struct {
|
|
Recipients []string
|
|
Plaintext string // base64
|
|
Metadata string // optional
|
|
}
|
|
|
|
type UserDecryptResult struct {
|
|
Plaintext string // base64
|
|
Metadata string
|
|
Sender string
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Order
|
|
|
|
Implement in this order to build incrementally on shared infrastructure:
|
|
|
|
### Phase 1: Shared Infrastructure
|
|
|
|
1. Add gRPC service clients to `VaultClient` struct
|
|
2. Add `findSSHCAMount()`, `findTransitMount()`, `findUserMount()` helpers
|
|
3. Update `layout.html` navigation with conditional engine links
|
|
4. Update `dashboard.html` mount table links and mount forms
|
|
5. Extend `requireAuth` middleware to populate mount availability flags
|
|
|
|
### Phase 2: SSH CA (closest to existing PKI patterns)
|
|
|
|
6. Implement SSH CA wrapper types in `client.go`
|
|
7. Implement SSH CA gRPC client methods in `client.go`
|
|
8. Add SSH CA methods to `vaultBackend` interface
|
|
9. Create `sshca.html`, `sshca_cert_detail.html`, `sshca_profile_detail.html`
|
|
10. Implement `internal/webserver/sshca.go` handlers
|
|
11. Register SSH CA routes
|
|
|
|
### Phase 3: Transit
|
|
|
|
12. Implement Transit wrapper types and gRPC client methods
|
|
13. Add Transit methods to `vaultBackend` interface
|
|
14. Create `transit.html`, `transit_key_detail.html`
|
|
15. Implement `internal/webserver/transit.go` handlers
|
|
16. Register Transit routes
|
|
|
|
### Phase 4: User
|
|
|
|
17. Implement User wrapper types and gRPC client methods
|
|
18. Add User methods to `vaultBackend` interface
|
|
19. Create `user.html`, `user_key_detail.html`
|
|
20. Implement `internal/webserver/user.go` handlers
|
|
21. Register User routes
|
|
|
|
### Phase 5: Polish
|
|
|
|
22. Test all engine UIs end-to-end against a running instance
|
|
23. Update `ARCHITECTURE.md` web routes table
|
|
24. Update `CLAUDE.md` project structure (if template list changed)
|
|
|
|
---
|
|
|
|
## Design Decisions
|
|
|
|
### No batch operations in UI
|
|
|
|
Batch encrypt/decrypt/rewrap (transit) are for programmatic use. The web UI
|
|
exposes single-operation forms only. Users needing batch operations should use
|
|
the REST or gRPC API directly.
|
|
|
|
### Operation results inline, not separate pages
|
|
|
|
Encrypt/decrypt/sign/verify results are shown inline on the same page (in a
|
|
readonly textarea or success div), not on a separate result page. This
|
|
follows the PKI "Sign CSR" pattern where the signed cert appears in the same
|
|
card after submission. It avoids navigation complexity and keeps the workflow
|
|
tight.
|
|
|
|
### No JavaScript beyond htmx
|
|
|
|
All forms use standard POST submission. HTMX is used only where it already
|
|
is in the codebase (seal button). No client-side validation, key filtering,
|
|
or dynamic form behavior.
|
|
|
|
### Key selection via `<select>`
|
|
|
|
Transit and User operations that reference a named key use a `<select>`
|
|
dropdown populated server-side. The handler fetches the key list on every
|
|
page render. For mounts with many keys, this is acceptable — transit engines
|
|
typically have tens of keys, not thousands.
|
|
|
|
### Profile-gated signing
|
|
|
|
The SSH CA sign forms require selecting a profile. The profile list is
|
|
fetched from the backend — the user can only sign with profiles they have
|
|
policy access to. If the gRPC call returns a permission error, the web UI
|
|
shows it as a form error, not a 403 page.
|
|
|
|
### Danger zone pattern
|
|
|
|
Destructive operations (delete key, delete user, rotate user key) use the
|
|
existing pattern: red `.btn-danger` button with `onclick="return
|
|
confirm('...')"`. No additional confirmation pages.
|