From cc5f6c829a024611a9ea65c17e7ad6728ae7783a Mon Sep 17 00:00:00 2001 From: Sergio Arroutbi Date: Tue, 12 May 2026 15:08:23 +0200 Subject: [PATCH] Add Cockpit plugin as alternative frontend deployment model Incorporate cockpit-keylime (React + PatternFly 6 + esbuild) as a Cockpit-native plugin that communicates with the existing backend through cockpit-bridge. The backend API remains unchanged. SRS: Add FR-088 through FR-094, NFR-026 through NFR-028, SR-030 with Gherkin acceptance criteria. Annotate FR-008, FR-009, FR-072, and SR-001 with Cockpit deployment notes. Extend implementation phasing and SDD cross-reference table. SDD: Add Cockpit context view (3.1.3), plugin composition (3.2.4), bridge interface contracts (3.4.6), Cockpit authentication flow, design rationale entries, security overlay rows, and traceability matrix entries for all new requirements. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Sergio Arroutbi --- SDD-Keylime-Monitoring-Tool.md | 247 ++++++++++++++++++++- SRS-Keylime-Monitoring-Tool.md | 380 ++++++++++++++++++++++++++++++++- 2 files changed, 625 insertions(+), 2 deletions(-) diff --git a/SDD-Keylime-Monitoring-Tool.md b/SDD-Keylime-Monitoring-Tool.md index fb1e693..bfb72d9 100644 --- a/SDD-Keylime-Monitoring-Tool.md +++ b/SDD-Keylime-Monitoring-Tool.md @@ -15,7 +15,7 @@ This Software Design Description (SDD) documents the *how* of the Keylime Monito ### 1.2 Scope -The SDD covers the full system: a React.js + TypeScript single-page application frontend, a Rust (Axum) asynchronous backend, and their integration with Keylime Verifier/Registrar APIs, TimescaleDB, and Redis. +The SDD covers the full system: a React.js + TypeScript single-page application frontend, a Rust (Axum) asynchronous backend, and their integration with Keylime Verifier/Registrar APIs, TimescaleDB, and Redis. An alternative Cockpit plugin frontend (`cockpit-keylime`) — built with React + PatternFly 6 + esbuild — is also covered as a parallel deployment option running inside Cockpit (:9090) via cockpit-bridge. Both frontends share the same backend API; the SDD documents both deployment architectures. ### 1.3 Definitions @@ -31,6 +31,11 @@ The SDD covers the full system: a React.js + TypeScript single-page application | IMA | Integrity Measurement Architecture | | SRS | Software Requirements Specification | | SDD | Software Design Description | +| Cockpit | Linux system administration web console (cockpit-project.org) | +| cockpit-bridge | Server-side process that proxies HTTP/WebSocket between Cockpit shell and backend services | +| PAM | Pluggable Authentication Modules — Linux modular authentication framework | +| PatternFly | Red Hat's open-source design system and component library for enterprise web applications | +| RPM | RPM Package Manager — Linux package format used by RHEL, CentOS, Fedora | ### 1.4 Design Stakeholders and Concerns @@ -110,6 +115,60 @@ This SDD uses the following IEEE 1016 viewpoints: | Redis | TCP | Connection string | NFR-019 | | SIEM (Syslog/Splunk/ECS) | TCP/HTTPS | API token | FR-063 | +#### 3.1.3 Cockpit Deployment Context View + +When deployed as a Cockpit plugin, the system boundary shifts: the browser communicates with Cockpit (:9090), and cockpit-bridge proxies all requests to the backend. The backend API remains unchanged. + +```text + +---------------------------+ + | PAM (System Login) | + +------------+--------------+ + | + | PAM auth (SR-030) + v ++----------------+ TLS 1.3 +---------+---------+ +| Browser | <-----------> | Cockpit (:9090) | +| (Plugin | (SR-008) | cockpit-ws | +| iframe) | +----+----------+----+ ++----------------+ | | + | | + cockpit.http() | | websocket-stream1 + (FR-089) | | (FR-090) + v v + +------+----------+------+ mTLS +-----------------+ + | cockpit-bridge | <----------> | Keylime | + | (server-side proxy) | (SR-004) | Verifier API v2 | + +-----------+------------+ +-----------------+ + | + | mTLS (SR-009) + v + +-----------------+ + | Keylime | + | Registrar API | + +-----------------+ +``` + +**Key Differences from Standalone SPA (3.1.1):** + +| Aspect | Standalone SPA | Cockpit Plugin | +|--------|---------------|----------------| +| Browser → Backend | Direct TLS 1.3 + JWT Bearer | Cockpit (:9090) → cockpit-bridge proxy | +| Authentication | OIDC/SAML → JWT (SR-001) | PAM system login (SR-030) | +| mTLS to Keylime | Backend reads certs from disk | cockpit-bridge reads certs via tls options (FR-091) | +| WebSocket | Direct WSS + JWT query param | websocket-stream1 raw channel (FR-090) | +| Frontend routing | React Router (hash/history) | cockpit.location (hash-based) | +| Theme control | Per-app toggle (FR-008) | OS-level via Cockpit (NFR-026) | + +**Cockpit External Interfaces:** + +| Interface | Protocol | Authentication | SRS Trace | +|-----------|----------|---------------|-----------| +| Browser → Cockpit | HTTPS (TLS 1.3) | PAM session cookie | SR-008, SR-030 | +| cockpit-bridge → Backend REST | HTTP/HTTPS (mTLS) | System user identity | FR-089, FR-091 | +| cockpit-bridge → Backend WebSocket | WS/WSS (mTLS) | System user identity | FR-090, FR-091 | + +**Trace:** SRS FR-088, FR-089, FR-090, FR-091, FR-092, SR-030 + ### 3.2 Composition View #### 3.2.1 Backend Components @@ -254,8 +313,75 @@ keylime-webtool-frontend/src/ | Backend Observability | tracing + OpenTelemetry | 0.1 / 0.27 | — | | Database | TimescaleDB (PostgreSQL) | — | — | | Cache | Redis | — | NFR-019 | +| Cockpit Plugin Framework | React | 18.3.1 | FR-088 | +| Cockpit Plugin Language | TypeScript | 5.6 | FR-088 | +| Cockpit Plugin Build | esbuild | 0.21+ | FR-088 | +| Cockpit Plugin Design System | PatternFly | 6.x | NFR-026 | +| Cockpit Plugin API | cockpit.js | Cockpit 300+ | FR-089, FR-090 | +| Cockpit Plugin Packaging | RPM | — | FR-093 | + + +#### 3.2.4 Cockpit Plugin Composition + +```text +cockpit-keylime/ ++-- manifest.json Cockpit sidebar registration, conditions (FR-088, FR-094) ++-- index.html Plugin entry point loaded in Cockpit iframe ++-- keylime.js esbuild-bundled JavaScript output ++-- keylime.css esbuild-bundled CSS output (PatternFly 6) ++-- src/ + +-- index.tsx React DOM root, cockpit.location router + +-- App.tsx React Query provider, cockpit.http() client setup + +-- api/ + | +-- cockpitClient.ts cockpit.http() wrapper (replaces Axios client) + | +-- agents.ts Agent API methods (shared contract with SPA) + | +-- attestations.ts Attestation API methods + | +-- ... (mirrors SPA api/ modules, using cockpitClient) + +-- hooks/ + | +-- useCockpitChannel.ts websocket-stream1 hook with reconnection (FR-090) + | +-- useCockpitUser.ts System user identity from cockpit.user() (FR-092) + +-- components/ + | +-- PageLayout.tsx PatternFly Page + PageSection (no sidebar — Cockpit provides it) + | +-- ... (PatternFly 6 versions of SPA components) + +-- config/ + +-- keylime.conf Bridge configuration reader (FR-091) +``` + +**manifest.json Structure:** + +```json +{ + "version": 0, + "name": "keylime", + "requires": { "cockpit": "300" }, + "menu": { + "keylime": { + "label": "Keylime Attestation", + "order": 500, + "docs": [{ "label": "Keylime Documentation", "url": "https://keylime.dev/docs" }] + } + }, + "content-security-policy": "default-src 'self' 'unsafe-inline'", + "conditions": [{ "path-exists": "/etc/cockpit/keylime.conf" }] +} +``` + +**Key Architectural Differences from Standalone SPA (3.2.2):** + +| Aspect | Standalone SPA | Cockpit Plugin | +|--------|---------------|----------------| +| HTTP Client | Axios (`src/api/client.ts`) | cockpit.http() (`src/api/cockpitClient.ts`) | +| WebSocket | Native WebSocket (`useWebSocket.ts`) | cockpit.channel() websocket-stream1 (`useCockpitChannel.ts`) | +| Routing | React Router 6 (`router.tsx`) | cockpit.location hash-based routing | +| Layout | Custom sidebar + top bar (`Layout/`) | PatternFly `Page` (Cockpit provides sidebar) | +| State Management | Zustand + localStorage | Zustand (no localStorage — iframe-scoped) | +| Auth | Zustand authStore + JWT | cockpit.user() system identity | +| Build | Vite | esbuild (Cockpit convention) | +| Deployment | Static assets served by backend or CDN | RPM → /usr/share/cockpit/keylime/ | + +**Trace:** SRS FR-088, FR-089, FR-090, FR-091, FR-092, FR-093, FR-094; Implementation -- `cockpit-keylime/` ### 3.3 Logical View @@ -853,8 +979,89 @@ All list endpoints use a standard pagination wrapper inside `data`: All routes except `/login` are wrapped in the `Layout` component (Sidebar + TopBar). +> **Cockpit Deployment Note:** The Cockpit plugin uses `cockpit.location` for hash-based routing instead of React Router. Routes are mapped to the same logical pages but use hash fragments (e.g., `#/agents`, `#/agents/uuid`). The `/login` route is not used — Cockpit handles authentication before the plugin loads. The plugin does not render its own sidebar; Cockpit's global sidebar provides navigation to the plugin entry point. + **Trace:** Implementation -- `keylime-webtool-frontend/src/router.tsx` +#### 3.4.6 Cockpit Bridge Interface Contracts + +##### cockpit.http() — REST Proxy Contract + +The Cockpit plugin uses `cockpit.http()` to proxy REST API requests through cockpit-bridge. The bridge establishes HTTP connections from the server side, eliminating CORS requirements and keeping mTLS certificates server-side. + +**Connection Setup:** + +```javascript +const http = cockpit.http({ + address: backendHost, // e.g., "localhost" or "backend.local" + port: backendPort, // e.g., 8080 + tls: { + authority: { file: "/etc/keylime/certs/cacert.crt" }, + certificate: { file: "/etc/keylime/certs/client-cert.crt" }, + key: { file: "/etc/keylime/certs/client-private.pem" } + } +}); +``` + +**Request/Response Contract:** + +| Property | Value | +|----------|-------| +| Method | `http.get(path, headers)`, `http.post(path, headers, body)`, `http.request({ method, path, headers, body })` | +| Content-Type | `application/json` | +| Response Format | Standard API envelope (3.4.1) — identical to standalone SPA | +| Error Handling | cockpit.http() rejects on HTTP errors; plugin parses error envelope | +| Authentication | No `Authorization: Bearer` header — bridge authenticates via system user identity | + +##### websocket-stream1 — WebSocket Proxy Contract + +The plugin uses `cockpit.channel()` with `payload: "websocket-stream1"` to establish WebSocket connections through the bridge. + +**Channel Setup:** + +```javascript +const channel = cockpit.channel({ + payload: "websocket-stream1", + address: backendHost, + port: backendPort, + path: "/ws/events", + tls: { + authority: { file: "/etc/keylime/certs/cacert.crt" }, + certificate: { file: "/etc/keylime/certs/client-cert.crt" }, + key: { file: "/etc/keylime/certs/client-private.pem" } + } +}); +``` + +**Channel Behavior:** + +| Property | Value | +|----------|-------| +| Message format | JSON strings — identical to standalone WebSocket (3.4.4) | +| Channels | `kpis`, `agents`, `alerts`, `policies` (same as 3.4.4) | +| Reconnection | Plugin-managed: exponential backoff 2^n * 1000ms, capped at 30s | +| Authentication | No JWT query parameter — bridge authenticates via system identity | +| Channel lifecycle | `channel.addEventListener("message", ...)`, `channel.addEventListener("close", ...)` | + +##### TLS Configuration + +Certificate paths are read from `/etc/cockpit/keylime.conf` by the plugin at initialization. The configuration file follows INI format: + +```ini +[backend] +host = localhost +port = 8080 + +[tls] +authority = /etc/keylime/certs/cacert.crt +certificate = /etc/keylime/certs/client-cert.crt +key = /etc/keylime/certs/client-private.pem +``` + +cockpit-bridge reads the certificate files from disk (as the system user) and uses them to establish mTLS connections. The certificate content never reaches the browser — only file paths are passed through the cockpit.js API. + +**Trace:** SRS FR-089, FR-090, FR-091, NFR-028; Implementation -- `cockpit-keylime/src/api/cockpitClient.ts`, `cockpit-keylime/src/hooks/useCockpitChannel.ts` + ### 3.5 Interaction View #### 3.5.1 Data Flow: Browser to Keylime @@ -941,6 +1148,24 @@ Re-render with new data **Trace:** SRS SR-001, SR-002, SR-010, SR-011 +**Cockpit Authentication Flow (Alternative):** + +```text +1. User navigates to Cockpit at :9090 +2. Cockpit login page presents PAM authentication (username + password) +3. PAM validates credentials (+ MFA if configured, e.g., pam_google_authenticator) +4. Cockpit establishes authenticated session, starts cockpit-bridge as the system user +5. User clicks "Keylime Attestation" in Cockpit sidebar → plugin iframe loads +6. Plugin calls cockpit.user() to retrieve system user identity and groups +7. Plugin sends API requests via cockpit.http() — no Authorization header needed +8. cockpit-bridge proxies requests with system user context +9. Backend maps system user/groups to RBAC role (Viewer/Operator/Admin) +10. Session lifetime governed by Cockpit session (not JWT) +11. Cockpit logout terminates bridge → plugin session ends immediately +``` + +**Trace:** SRS SR-030, FR-092 + #### 3.5.4 Data Flow: Background Attestation Recording The backend spawns a long-lived Tokio task at startup that records attestation observations independently of frontend API requests, ensuring the time-series attestation history (FR-024) is complete regardless of dashboard usage. @@ -1257,6 +1482,12 @@ Maximum 5 parallel concurrent log fetches to the Verifier API, enforced via Toki | No `AgentRepository` — agents excluded from repository pattern | Agents are Keylime-owned data observed via pass-through proxy; all operations (list, detail, actions, bulk) forward to Verifier/Registrar APIs and cache responses (10s TTL). Keeping agents out of the repository layer preserves graceful degradation (NFR-016): agent listings work even when the webtool DB is down. `agent_id` in attestation/alert records is a bare UUID reference, not a foreign key requiring local agent persistence | FR-012, NFR-016 | + + +| Cockpit plugin as alternative deployment | Integrates Keylime monitoring directly into the system administrator's existing Cockpit workflow; eliminates context-switching between tools; leverages Cockpit's established authentication, session management, and plugin discovery | FR-088 | +| PatternFly 6 for Cockpit plugin | Ensures visual consistency with Cockpit core pages; PatternFly is Cockpit's native design system, so users experience a seamless UI across all Cockpit plugins | NFR-026 | +| cockpit-bridge as mandatory proxy | Eliminates CORS configuration; keeps mTLS certificates server-side (never exposed to browser); leverages Cockpit's existing transport security; backend requires zero changes to support Cockpit deployment | NFR-028, FR-089, FR-090 | +| PAM replacing OIDC in Cockpit mode | System administrators already authenticate to the host via PAM; requiring a second OIDC login would add friction without security benefit; PAM supports MFA via standard modules (pam_google_authenticator, SSSD) | SR-030, FR-092 | --- @@ -1285,6 +1516,9 @@ Maximum 5 parallel concurrent log fetches to the Verifier API, enforced via Toki | Re-registration alert | TPM key change detection via `regcount` | SR-025 | | Idle timeout | Configurable session timeout (default: 900s) | SR-028 | | Rate limiting | Per-user and global request rate limiting | SR-029, NFR-018 | +| Cockpit authentication | PAM-based system login replaces OIDC/JWT in Cockpit mode; role mapped from system groups | SR-030, FR-092 | +| Cockpit transport | cockpit-bridge proxies all traffic; mTLS certs stay server-side; no CORS | NFR-028, FR-089 | +| Cockpit isolation | Plugin runs in Cockpit iframe; no cross-plugin shared state | NFR-027 | ### 5.2 Performance Overlay @@ -1365,6 +1599,13 @@ Maximum 5 parallel concurrent log fetches to the Verifier API, enforced via Toki | FR-084 | 3.2.2 | `KpiCard.tsx`: optional `linkTo` prop wraps card in React Router ``; Dashboard page maps each KPI to its target route (e.g., Failed Agents → `/agents?state=failed,invalid_quote,tenant_failed`) | | FR-085 | 3.2.2 | `Alerts.tsx`: three Recharts donut `PieChart` components below alert table — By Severity, By Type, By State; clickable segments navigate to `/alerts?{dimension}={value}` with filter pre-applied; color maps match Dashboard alert chart (FR-047) | | FR-087 | 3.5.4, 3.8.1 | Background `tokio::spawn` observation task, `record_agent_observations()` reuse, dedup tracker, configurable interval | +| FR-088 | 3.1.3, 3.2.4 | Cockpit plugin manifest.json, sidebar registration, Cockpit context view | +| FR-089 | 3.1.3, 3.4.6 | cockpit.http() REST proxy through cockpit-bridge | +| FR-090 | 3.1.3, 3.4.6 | websocket-stream1 raw channel through cockpit-bridge | +| FR-091 | 3.4.6 | cockpit-bridge TLS options (tls.authority, tls.certificate, tls.key), /etc/cockpit/keylime.conf | +| FR-092 | 3.5.3, 5.1 | PAM authentication flow, system user/group → RBAC role mapping | +| FR-093 | 3.2.4 | RPM packaging, /usr/share/cockpit/keylime/ installation path | +| FR-094 | 3.2.4 | manifest.json conditions (path-exists) for conditional plugin visibility | ### 6.2 Non-Functional Requirements @@ -1379,6 +1620,9 @@ Maximum 5 parallel concurrent log fetches to the Verifier API, enforced via Toki | NFR-019 | 3.3.11, 3.8.3 | `CacheBackend` trait, tiered TTLs | | NFR-021 | 3.4.4 | WebSocket `/ws/events` | | NFR-023 | 3.8.4 | Tokio semaphore, max 5 parallel fetches | +| NFR-026 | 3.2.4, 5.1 | PatternFly 6 component library, CSS variable inheritance from Cockpit theme | +| NFR-027 | 3.2.4, 5.1 | Cockpit iframe isolation, no cross-plugin shared state | +| NFR-028 | 3.1.3, 3.4.6, 5.1 | cockpit-bridge mandatory proxy, no direct browser-to-backend connections | ### 6.3 Security Requirements @@ -1394,3 +1638,4 @@ Maximum 5 parallel concurrent log fetches to the Verifier API, enforced via Toki | SR-014 | 3.3.11, 5.1 | Repository trait API excludes PoP token types | | SR-015 | 3.3.11, 3.7.4 | `AuditRepository` append-only trait, SHA-256 hash chain | | SR-023 | 3.2.3 | `#![forbid(unsafe_code)]` on Rust crate | +| SR-030 | 3.5.3, 5.1 | PAM authentication replaces OIDC/JWT in Cockpit mode; system group → RBAC role mapping | diff --git a/SRS-Keylime-Monitoring-Tool.md b/SRS-Keylime-Monitoring-Tool.md index de81329..1b59d55 100644 --- a/SRS-Keylime-Monitoring-Tool.md +++ b/SRS-Keylime-Monitoring-Tool.md @@ -24,7 +24,7 @@ The Keylime Monitoring Dashboard (the "System") is a web-based security operatio The System transforms Keylime from a CLI-driven security tool into a visual operations platform, reducing mean time to detect (MTTD) attestation failures from hours to seconds, centralizing policy and certificate lifecycle management, and providing tamper-evident audit trails for compliance reporting. -**Technical Architecture:** React.js + TypeScript SPA frontend, Rust (Axum) async backend, TimescaleDB for production time-series storage (SQLite supported for development and small deployments), Redis for production caching (in-memory fallback when unavailable), mTLS + rustls for Keylime API communication. A hexagonal repository abstraction (`Arc`) decouples business logic from storage, enabling backend portability across database and cache implementations. +**Technical Architecture:** React.js + TypeScript SPA frontend, Rust (Axum) async backend, TimescaleDB for production time-series storage (SQLite supported for development and small deployments), Redis for production caching (in-memory fallback when unavailable), mTLS + rustls for Keylime API communication. A hexagonal repository abstraction (`Arc`) decouples business logic from storage, enabling backend portability across database and cache implementations. An alternative Cockpit plugin frontend (`cockpit-keylime`) — built with React + PatternFly 6 + esbuild — MAY be deployed as a Cockpit-native plugin running inside Cockpit (:9090), communicating with the same backend through `cockpit-bridge` (FR-088 through FR-094). The standalone SPA and the Cockpit plugin are parallel deployment options; the backend API remains unchanged. --- @@ -121,6 +121,13 @@ The System transforms Keylime from a CLI-driven security tool into a visual oper | FR-085 | Alert Center distribution pie charts (by severity, type, state) | MUST | Revocation - Alert Workflow | | FR-086 | Integrations topology view with SSH connect | SHOULD | Integration Status - Backend Connectivity | | FR-087 | Background attestation observation recording independent of frontend | MUST | Attestation Analytics - Overview Dashboard | +| FR-088 | Cockpit plugin manifest and sidebar registration | MUST | Cockpit Plugin - Deployment Model | +| FR-089 | REST communication via cockpit.http() bridge proxy | MUST | Cockpit Plugin - Communication | +| FR-090 | Real-time WebSocket via websocket-stream1 raw channel | MUST | Cockpit Plugin - Communication | +| FR-091 | mTLS certificate configuration via cockpit-bridge tls options | MUST | Cockpit Plugin - Security | +| FR-092 | PAM-based authentication for Cockpit deployments | MUST | Cockpit Plugin - Authentication | +| FR-093 | RPM packaging and /usr/share/cockpit/ installation | MUST | Cockpit Plugin - Packaging | +| FR-094 | Conditional plugin visibility via manifest conditions | SHOULD | Cockpit Plugin - Deployment Model | ### 2.2 Non-Functional Requirements @@ -151,6 +158,9 @@ The System transforms Keylime from a CLI-driven security tool into a visual oper | NFR-023 | Maximum 5 parallel concurrent log fetches to Verifier | MUST | Technical Architecture - IMA Log & Data Decoupling | | NFR-024 | AI Assistant query performance and rate limiting | SHOULD | AI Assistant - Conversational Interface | | NFR-025 | Zero-configuration development setup with storage backend portability | MUST | Technical Architecture - Repository Abstraction | +| NFR-026 | PatternFly 6 visual consistency with Cockpit core pages | MUST | Cockpit Plugin - UI Consistency | +| NFR-027 | iframe isolation with no cross-plugin shared state | MUST | Cockpit Plugin - Isolation | +| NFR-028 | cockpit-bridge as mandatory proxy for all backend communication | MUST | Cockpit Plugin - Communication Architecture | ### 2.3 Security Requirements @@ -185,6 +195,7 @@ The System transforms Keylime from a CLI-driven security tool into a visual oper | SR-027 | Emergency bypass with break-glass audit trail | MUST | Policy Management - Two-Person Rule | | SR-028 | Configurable idle session timeout | MUST | Dashboard Authentication - User Identity | | SR-029 | Rate limiting on dashboard session creation endpoint | MUST | Attestation Modes - Comparative View | +| SR-030 | PAM authentication replaces OIDC/JWT in Cockpit deployment mode | MUST | Cockpit Plugin - Authentication | --- @@ -449,6 +460,8 @@ Feature: Dark/Light Mode And top bar search input text MUST be legible ``` +> **Cockpit Deployment Note:** In Cockpit deployments, theming is controlled at the OS level by Cockpit itself; individual plugins do not provide per-app theme toggles. The theme toggle button in the top bar MUST NOT be rendered when the plugin runs inside Cockpit. The plugin MUST inherit Cockpit's active theme via PatternFly 6 CSS variables (NFR-026). See FR-088. + ### FR-009: In-App Notification System **Description:** The System MUST provide an in-app notification bell displaying an unread notification badge count. Notifications MUST include attestation failures, certificate expiry warnings, policy updates, agent registration events, and revocation events. Severity thresholds for generating notifications MUST be configurable. @@ -476,6 +489,8 @@ Feature: In-App Notification System And the notification bell MUST NOT display a badge count ``` +> **Cockpit Deployment Note:** Cockpit provides its own notification API for surfacing alerts to the system administrator. In Cockpit deployments, the plugin MAY delegate critical notifications to Cockpit's notification system instead of rendering a standalone notification bell. When delegating, the plugin MUST use `cockpit.transport.control()` or the PatternFly `Alert` component to surface notifications within the Cockpit UI paradigm. + ### FR-010: External Alert Integration **Description:** The System SHOULD support sending critical alert notifications to external channels including Email and Slack. Alert routing MUST be configurable by severity threshold. @@ -2276,6 +2291,8 @@ Feature: Runtime Keylime Connection Configuration Then the "Apply Changes" button MUST be enabled ``` +> **Cockpit Deployment Note:** In Cockpit deployments, the backend URL is configured via the cockpit-bridge configuration file (`/etc/cockpit/keylime.conf`), not through the Settings page. The Backend URL field MUST NOT be rendered in Cockpit deployments. Verifier and Registrar URLs remain configurable through the backend settings API. See FR-089, FR-091. + ### FR-073: mTLS Certificate Configuration UI **Description:** The System MUST provide a certificate settings section on the Settings page for configuring mTLS client certificates used to communicate with Keylime APIs. Two input modes MUST be supported: "By directory" (a single directory path, with standard Keylime filenames `client-cert.crt`, `client-private.pem`, `cacert.crt` assumed) and "Manually" (individual paths for certificate, private key, and CA certificate files). The mode MUST be auto-detected from saved settings. The Apply button MUST be disabled when no changes exist and MUST display backend error messages on failure. Certificate configuration MUST be persisted via the backend settings API (`GET/PUT /api/settings/certificates`) and applied at runtime by reconstructing the mTLS reqwest client. @@ -2815,6 +2832,229 @@ Feature: Background Attestation Observation Recording And previously recorded observations MUST remain intact in the repository ``` +### FR-088: Cockpit Plugin Manifest and Sidebar Registration + +**Description:** The cockpit-keylime plugin MUST include a `manifest.json` file that registers the plugin in the Cockpit sidebar navigation menu. The manifest MUST declare the plugin's display name, menu path, and entry point. When Cockpit loads, the plugin MUST appear as a sidebar entry under the appropriate category (e.g., "Networking" or a custom "Security" category). The plugin MUST load its entry point (`index.html`) inside a Cockpit iframe when the user clicks the sidebar entry. The manifest MUST declare the plugin's required Cockpit API version. + +**Trace:** Cockpit Plugin - Deployment Model; FR-003 (Sidebar Navigation) + +```gherkin +Feature: Cockpit Plugin Manifest and Sidebar Registration + + Scenario: Plugin appears in Cockpit sidebar + Given the cockpit-keylime RPM is installed to /usr/share/cockpit/keylime/ + And the manifest.json declares a menu entry "Keylime Attestation" + When the user logs into Cockpit at :9090 + Then a "Keylime Attestation" entry MUST appear in the Cockpit sidebar + And clicking the entry MUST load the plugin's index.html in the content iframe + + Scenario: Manifest declares required Cockpit API version + Given the manifest.json includes a "requires" field specifying the minimum Cockpit version + When Cockpit loads the plugin manifest + Then Cockpit MUST validate the version requirement before registering the plugin + And if the running Cockpit version is below the minimum, the plugin MUST NOT appear in the sidebar + + Scenario: Plugin loads in isolated iframe + Given the user clicks "Keylime Attestation" in the Cockpit sidebar + When the plugin content loads + Then the plugin MUST render inside a Cockpit-managed iframe + And the plugin's DOM MUST be isolated from other Cockpit pages and plugins +``` + +### FR-089: REST Communication via cockpit.http() Bridge Proxy + +**Description:** In Cockpit deployments, the plugin MUST use `cockpit.http()` to send REST API requests to the `keylime-webtool-backend`. The `cockpit.http()` call MUST be configured with the backend's host and port, and MUST include TLS options (`tls.authority`, `tls.certificate`, `tls.key`) when mTLS is required (FR-091). The cockpit-bridge acts as a server-side proxy: HTTP requests are issued from the server running Cockpit, not from the browser. The plugin MUST NOT make direct browser-to-backend HTTP requests. All API requests MUST flow through `cockpit.http()` and MUST use the same REST API contract as the standalone SPA (SDD 3.4.1 through 3.4.3). + +**Trace:** Cockpit Plugin - Communication; NFR-028 (Mandatory Bridge Proxy); SDD 3.4.3 (REST API) + +```gherkin +Feature: REST Communication via cockpit.http() Bridge Proxy + + Scenario: Fetch agent list through cockpit-bridge + Given the cockpit-keylime plugin is loaded in Cockpit + And the backend is running at host "backend.local" port 8080 + When the plugin requests the agent list + Then the request MUST be issued via cockpit.http("backend.local", { port: 8080 }) + And the HTTP request MUST originate from the Cockpit server, not the browser + And the response MUST conform to the standard API envelope (SDD 3.4.1) + + Scenario: cockpit.http() handles backend error + Given the backend returns HTTP 502 Bad Gateway + When the plugin receives the error response via cockpit.http() + Then the plugin MUST parse the standard error envelope + And the plugin MUST display a descriptive error message to the user + + Scenario: No direct browser-to-backend requests + Given the cockpit-keylime plugin is loaded in Cockpit + When the plugin communicates with the backend + Then no XMLHttpRequest or fetch() calls MUST be made directly to the backend from the browser + And all REST communication MUST flow through cockpit.http() via cockpit-bridge +``` + +### FR-090: Real-Time WebSocket via websocket-stream1 Raw Channel + +**Description:** In Cockpit deployments, the plugin MUST use `cockpit.channel()` with the `websocket-stream1` payload type to establish a WebSocket connection to the backend's `/ws/events` endpoint through cockpit-bridge. The channel MUST carry the same JSON message format as the standalone SPA's native WebSocket connection (SDD 3.4.4). The plugin MUST implement reconnection logic with exponential backoff (matching the standalone SPA's 2^n * 1000ms strategy, capped at 30 seconds) when the channel closes unexpectedly. The backend's WebSocket protocol remains unchanged — cockpit-bridge translates the raw channel into a standard WebSocket connection. + +**Trace:** Cockpit Plugin - Communication; NFR-021 (WebSocket Real-Time Updates); SDD 3.4.4 + +```gherkin +Feature: Real-Time WebSocket via websocket-stream1 Raw Channel + + Scenario: Receive real-time agent state updates + Given the cockpit-keylime plugin has opened a websocket-stream1 channel to /ws/events + When an agent transitions from GET_QUOTE to FAILED on the backend + Then the plugin MUST receive a JSON message through the channel + And the message format MUST match the WebSocket protocol defined in SDD 3.4.4 + And the plugin MUST update the UI to reflect the state change + + Scenario: Reconnect on channel close + Given the websocket-stream1 channel is established + When cockpit-bridge closes the channel unexpectedly + Then the plugin MUST attempt reconnection with exponential backoff + And the backoff interval MUST start at 1 second and cap at 30 seconds + And a connection status indicator MUST show "reconnecting" + + Scenario: Channel uses cockpit-bridge as proxy + Given the plugin opens a websocket-stream1 channel + When the channel connects to the backend WebSocket endpoint + Then the connection MUST be proxied through cockpit-bridge + And no direct browser WebSocket connection MUST be established to the backend +``` + +### FR-091: mTLS Certificate Configuration via cockpit-bridge TLS Options + +**Description:** In Cockpit deployments, mTLS certificates for backend communication MUST be configured via cockpit-bridge TLS options passed to `cockpit.http()` and `cockpit.channel()`. The plugin MUST read certificate paths from a configuration file (e.g., `/etc/cockpit/keylime.conf`) and pass them as `tls.authority` (CA certificate), `tls.certificate` (client certificate), and `tls.key` (client private key) parameters. Certificate files MUST be readable by the cockpit-ws process. The plugin MUST NOT expose certificate file paths in the browser DOM or JavaScript console. This replaces the Settings page certificate configuration (FR-073) for Cockpit deployments — the mTLS certificate section MUST NOT be rendered in Cockpit mode. + +**Trace:** Cockpit Plugin - Security; FR-073 (mTLS Certificate Configuration); SR-004 (mTLS) + +```gherkin +Feature: mTLS Certificate Configuration via cockpit-bridge TLS Options + + Scenario: cockpit-bridge uses mTLS for backend communication + Given /etc/cockpit/keylime.conf specifies certificate paths: + | Key | Value | + | tls.authority | /etc/keylime/certs/cacert.crt | + | tls.certificate | /etc/keylime/certs/client-cert.crt | + | tls.key | /etc/keylime/certs/client-private.pem | + When the plugin issues a cockpit.http() request to the backend + Then cockpit-bridge MUST establish the connection using the configured client certificate + And the backend MUST validate the client certificate via mTLS + + Scenario: Missing certificate configuration + Given /etc/cockpit/keylime.conf does not contain TLS configuration + When the plugin attempts to connect to the backend via cockpit.http() + Then the plugin MUST display an error indicating missing mTLS configuration + And the plugin MUST NOT fall back to unencrypted communication + + Scenario: Certificate paths not exposed to browser + Given the plugin reads certificate paths from the configuration file + When the plugin renders in the browser + Then certificate file paths MUST NOT appear in the browser DOM + And certificate file paths MUST NOT be logged to the JavaScript console +``` + +### FR-092: PAM-Based Authentication for Cockpit Deployments + +**Description:** In Cockpit deployments, user authentication MUST be handled by Cockpit's PAM-based system login. The OIDC/SAML authentication flow (SR-001) MUST NOT be used. The cockpit-bridge MUST pass the authenticated system user identity to the backend. The backend MUST map the system user to its RBAC role (SR-003) using a configurable mapping (e.g., system group membership → dashboard role). The plugin MUST NOT display a login page — Cockpit's login screen handles authentication before the plugin loads. Session lifetime MUST be governed by the Cockpit session, not by JWT expiry (SR-010). + +**Trace:** Cockpit Plugin - Authentication; SR-001 (OIDC/SAML Authentication); SR-030 (PAM Authentication); SR-003 (RBAC) + +```gherkin +Feature: PAM-Based Authentication for Cockpit Deployments + + Scenario: User authenticates via Cockpit login + Given the user navigates to Cockpit at :9090 + When the user enters valid system credentials (PAM authentication) + Then Cockpit MUST establish an authenticated session + And the cockpit-keylime plugin MUST load without requiring additional authentication + And the plugin MUST NOT display a login page + + Scenario: System user mapped to RBAC role + Given the system user "admin-user" belongs to the "keylime-admin" group + And the backend role mapping is configured as: keylime-admin → Admin + When the plugin requests user role information from the backend + Then the backend MUST return the Admin role for this user + And the plugin MUST render Admin-level controls + + Scenario: Unauthenticated access prevented + Given the user has not logged into Cockpit + When the user attempts to access the Cockpit URL directly + Then Cockpit MUST redirect the user to its PAM login screen + And no plugin content MUST be rendered until authentication succeeds + + Scenario: Session governed by Cockpit + Given the user is authenticated via Cockpit's PAM login + When the Cockpit session expires or the user logs out of Cockpit + Then the plugin session MUST terminate immediately + And the plugin MUST NOT maintain an independent session via JWT +``` + +### FR-093: RPM Packaging and /usr/share/cockpit/ Installation + +**Description:** The cockpit-keylime plugin MUST be packaged as an RPM. The RPM MUST install all plugin assets (JavaScript bundle, HTML entry point, manifest.json, CSS, and static resources) to `/usr/share/cockpit/keylime/`. The RPM MUST declare dependencies on `cockpit-ws` and `cockpit-bridge`. The RPM MUST NOT require a post-install service restart — Cockpit dynamically discovers plugins from `/usr/share/cockpit/`. The RPM MUST include a configuration template at `/etc/cockpit/keylime.conf.example` with documented backend connection and TLS settings. The RPM MUST be installable on RHEL 9+, CentOS Stream 9+, and Fedora 39+ systems. + +**Trace:** Cockpit Plugin - Packaging; NFR-012 (Air-Gapped Deployment); NFR-013 (Self-Contained Packaging); NFR-015 (RPM Deployment) + +```gherkin +Feature: RPM Packaging and Installation + + Scenario: Install plugin via RPM + Given the cockpit-keylime RPM package is available + When the administrator runs "dnf install cockpit-keylime" + Then the RPM MUST install files to /usr/share/cockpit/keylime/ + And the installed files MUST include manifest.json, index.html, and the JavaScript bundle + And the plugin MUST appear in the Cockpit sidebar without restarting cockpit-ws + + Scenario: RPM declares correct dependencies + Given the cockpit-keylime RPM spec + Then the RPM MUST declare "Requires: cockpit-ws" and "Requires: cockpit-bridge" + And the RPM MUST NOT declare dependencies on external CDNs or runtime internet access + + Scenario: Configuration template installed + Given the cockpit-keylime RPM is installed + Then a configuration template MUST exist at /etc/cockpit/keylime.conf.example + And the template MUST document all configurable settings including backend URL and TLS paths + + Scenario: Uninstall removes plugin cleanly + Given the cockpit-keylime RPM is installed + When the administrator runs "dnf remove cockpit-keylime" + Then all files in /usr/share/cockpit/keylime/ MUST be removed + And the plugin MUST no longer appear in the Cockpit sidebar + And user configuration in /etc/cockpit/keylime.conf MUST be preserved +``` + +### FR-094: Conditional Plugin Visibility via Manifest Conditions + +**Description:** The cockpit-keylime manifest.json SHOULD support a `conditions` field that controls plugin visibility based on system state. The plugin SHOULD use `path-exists` conditions to verify that required files (e.g., the backend configuration file or mTLS certificates) are present before registering the plugin in the sidebar. When conditions are not met, the plugin MUST NOT appear in the Cockpit sidebar. This prevents users from seeing a non-functional plugin entry when the backend or certificates are not configured on the local system. + +**Trace:** Cockpit Plugin - Deployment Model; FR-088 (Plugin Manifest) + +```gherkin +Feature: Conditional Plugin Visibility via Manifest Conditions + + Scenario: Plugin hidden when configuration is missing + Given the manifest.json declares a condition: path-exists "/etc/cockpit/keylime.conf" + And the file /etc/cockpit/keylime.conf does not exist on the system + When the user logs into Cockpit + Then the "Keylime Attestation" entry MUST NOT appear in the Cockpit sidebar + + Scenario: Plugin visible when configuration exists + Given the manifest.json declares a condition: path-exists "/etc/cockpit/keylime.conf" + And the file /etc/cockpit/keylime.conf exists on the system + When the user logs into Cockpit + Then the "Keylime Attestation" entry MUST appear in the Cockpit sidebar + + Scenario: Multiple conditions evaluated + Given the manifest.json declares conditions: + | Condition | Path | + | path-exists | /etc/cockpit/keylime.conf | + | path-exists | /etc/keylime/certs/client-cert.crt | + When all condition paths exist on the system + Then the plugin MUST appear in the Cockpit sidebar + But if any condition path is missing + Then the plugin MUST NOT appear in the Cockpit sidebar +``` + --- ## 4. Non-Functional Requirements Detail @@ -3413,6 +3653,95 @@ Feature: Zero-Configuration Development Setup Then the System MUST reject the self-approval with "self-approval not permitted" ``` +### NFR-026: PatternFly 6 Visual Consistency with Cockpit Core Pages + +**Description:** When deployed as a Cockpit plugin, the plugin MUST use PatternFly 6 as its component library to ensure visual consistency with Cockpit core pages and other Cockpit plugins. The plugin MUST inherit Cockpit's active theme via PatternFly CSS custom properties. The plugin MUST NOT use custom CSS that overrides PatternFly 6 visual tokens (colors, spacing, typography) in ways that create visual inconsistency with Cockpit's native pages. Navigation patterns MUST follow Cockpit conventions — the plugin MUST NOT render its own sidebar when running inside Cockpit, since Cockpit provides the global sidebar. + +**Trace:** Cockpit Plugin - UI Consistency; FR-003 (Sidebar Navigation); FR-008 (Dark/Light Mode) + +```gherkin +Feature: PatternFly 6 Visual Consistency + + Scenario: Plugin matches Cockpit visual theme + Given the cockpit-keylime plugin is loaded in Cockpit + And Cockpit is rendering in its default theme + When the user views the plugin content + Then all components MUST use PatternFly 6 components and CSS variables + And the plugin's visual appearance MUST be consistent with Cockpit core pages (Overview, Networking, Storage) + + Scenario: Plugin does not render its own sidebar + Given the cockpit-keylime plugin is loaded in Cockpit + When the plugin content renders + Then the plugin MUST NOT render a standalone sidebar navigation + And navigation within the plugin MUST use page-level tabs or breadcrumbs + And the Cockpit global sidebar MUST remain the primary navigation mechanism + + Scenario: PatternFly theme variables inherited + Given Cockpit applies a dark theme via OS-level preference + When the cockpit-keylime plugin renders + Then the plugin MUST inherit the dark theme via PatternFly 6 CSS custom properties + And no manual theme toggle MUST be rendered (FR-008 Cockpit note) +``` + +### NFR-027: iframe Isolation — No Cross-Plugin Shared State + +**Description:** Each Cockpit plugin runs inside its own iframe. The cockpit-keylime plugin MUST NOT share DOM state, JavaScript variables, or browser storage (localStorage, sessionStorage, cookies) with other Cockpit plugins or the Cockpit shell. All plugin state MUST be self-contained within the plugin's iframe context. Communication with the Cockpit shell MUST occur exclusively through the `cockpit.js` API (e.g., `cockpit.transport`, `cockpit.location`, `cockpit.http()`). The plugin MUST NOT use `window.parent`, `postMessage`, or other cross-frame APIs to interact with other plugins. + +**Trace:** Cockpit Plugin - Isolation; SR-019 (Multi-Tenancy Isolation) + +```gherkin +Feature: iframe Isolation + + Scenario: Plugin state isolated from other plugins + Given the cockpit-keylime plugin and another Cockpit plugin are both loaded + When the cockpit-keylime plugin stores data in its iframe's localStorage + Then the other plugin MUST NOT be able to access that data + And the Cockpit shell MUST NOT expose the plugin's localStorage to other plugins + + Scenario: No cross-frame API usage + Given the cockpit-keylime plugin is loaded in its iframe + When the plugin JavaScript executes + Then the plugin MUST NOT call window.parent or use postMessage to other frames + And all Cockpit shell communication MUST use the cockpit.js API exclusively + + Scenario: Plugin crash does not affect other plugins + Given the cockpit-keylime plugin encounters a JavaScript error + When the error is thrown in the plugin's iframe + Then other Cockpit plugins MUST continue to function normally + And the Cockpit shell MUST remain responsive +``` + +### NFR-028: cockpit-bridge as Mandatory Proxy for All Backend Communication + +**Description:** In Cockpit deployments, all communication between the plugin and the keylime-webtool-backend MUST flow through cockpit-bridge. The plugin MUST NOT make direct HTTP or WebSocket connections from the browser to the backend. cockpit-bridge acts as a server-side proxy, issuing network requests from the Cockpit server host on behalf of the plugin. This architecture eliminates CORS requirements, keeps mTLS certificates server-side (never exposed to the browser), and leverages Cockpit's existing authentication and access control. The backend API endpoints, request formats, and response formats MUST remain identical regardless of whether the request originates from the standalone SPA or the Cockpit plugin via cockpit-bridge. + +**Trace:** Cockpit Plugin - Communication Architecture; FR-089 (cockpit.http()); FR-090 (websocket-stream1); SR-004 (mTLS) + +```gherkin +Feature: cockpit-bridge as Mandatory Proxy + + Scenario: All REST requests flow through cockpit-bridge + Given the cockpit-keylime plugin is loaded in Cockpit + When the plugin fetches data from the backend REST API + Then the HTTP request MUST be issued by cockpit-bridge from the server side + And no CORS preflight request MUST be observed in browser network traffic + And the backend MUST NOT need CORS headers for Cockpit plugin requests + + Scenario: All WebSocket connections flow through cockpit-bridge + Given the cockpit-keylime plugin establishes a real-time event channel + When the websocket-stream1 channel is opened + Then cockpit-bridge MUST establish the WebSocket connection to the backend + And the browser MUST NOT hold a direct WebSocket connection to the backend + + Scenario: Backend receives identical requests from SPA and Cockpit + Given the standalone SPA sends GET /api/agents to the backend + And the Cockpit plugin sends the same request via cockpit.http() + When the backend processes both requests + Then the request format MUST be identical + And the response format MUST be identical + And the backend MUST NOT require any Cockpit-specific API logic +``` + --- ## 5. Security Requirements Detail @@ -3445,6 +3774,8 @@ Feature: OIDC/SAML Authentication And the user MUST be returned to their original page after re-authentication ``` +> **Cockpit Deployment Note:** In Cockpit deployments, PAM-based system login replaces OIDC/SAML authentication. The OIDC flow described above MUST NOT be used when the plugin runs inside Cockpit. Authentication is handled by Cockpit's PAM login screen before the plugin loads. See SR-030, FR-092. + ### SR-002: MFA for Admin Role **Description:** The System MUST require Multi-Factor Authentication for users with the Admin role. MFA MUST be enforced at the identity provider level. The System MUST verify the MFA claim in the OIDC/SAML token before granting Admin privileges. @@ -4058,6 +4389,47 @@ Feature: Session Creation Rate Limiting Then the client MUST be able to attempt login again ``` +### SR-030: PAM Authentication Replaces OIDC/JWT in Cockpit Deployment Mode + +**Description:** In Cockpit deployments, the System MUST authenticate users via PAM (Pluggable Authentication Modules) through Cockpit's login screen. The OIDC/SAML authentication flow (SR-001) and JWT session management (SR-010) MUST NOT be used in Cockpit mode. Cockpit's `cockpit-bridge` runs as the authenticated system user; the backend MUST derive the user identity from the system-level authentication context provided by the bridge. The backend MUST map system user identity (username and group membership) to RBAC roles (SR-003) using a configurable role mapping. MFA requirements (SR-002) MUST be enforced at the PAM level (e.g., via `pam_google_authenticator` or SSSD with IdP-backed MFA) rather than via OIDC claims. Session lifetime MUST be governed by the Cockpit session — the backend MUST NOT issue or validate JWTs for Cockpit-originated requests. + +**Trace:** Cockpit Plugin - Authentication; SR-001 (OIDC/SAML); SR-002 (MFA); SR-003 (RBAC); SR-010 (JWT); FR-092 (PAM Authentication) + +```gherkin +Feature: PAM Authentication in Cockpit Mode + + Scenario: Backend authenticates Cockpit requests via system user + Given the cockpit-keylime plugin sends a request via cockpit.http() + And cockpit-bridge is running as system user "secops-operator" + When the backend receives the proxied request + Then the backend MUST identify the user as "secops-operator" from the bridge context + And the backend MUST NOT require a JWT Bearer token in the Authorization header + + Scenario: RBAC role derived from system groups + Given system user "secops-operator" belongs to groups: keylime-operator, wheel + And the role mapping is configured as: + | System Group | Dashboard Role | + | keylime-admin | Admin | + | keylime-operator | Operator | + | keylime-viewer | Viewer | + When the backend resolves the role for "secops-operator" + Then the user MUST receive the Operator role + And RBAC enforcement (SR-003) MUST apply based on the Operator permission set + + Scenario: MFA enforced at PAM level + Given the system PAM configuration requires MFA for all logins + When the user attempts to log into Cockpit without completing MFA + Then PAM MUST reject the authentication attempt + And the user MUST NOT be able to access any Cockpit plugin, including cockpit-keylime + + Scenario: No JWT issued for Cockpit requests + Given the cockpit-keylime plugin is communicating with the backend + When the backend processes a Cockpit-originated request + Then the backend MUST NOT issue a JWT access token + And the backend MUST NOT validate a JWT Bearer token + And session state MUST be derived from the Cockpit session, not from a JWT claim +``` + --- ## 6. Implementation Phasing @@ -4070,6 +4442,8 @@ As defined in the source presentation, the implementation follows three phases: **Phase 3 — Enterprise Scale:** Multi-tenancy, compliance reports (NIST, PCI DSS, SOC 2, FedRAMP), incident response integration (ServiceNow, Jira, PagerDuty), HA deployment, air-gapped packaging, multi-cluster support, WCAG 2.1 AA accessibility. +**Cockpit Plugin Deployment:** The Cockpit plugin (`cockpit-keylime`) is an alternative frontend deployment target introduced in Phase 2 alongside the standalone SPA. It provides the same monitoring capabilities within the Cockpit system administration environment (FR-088 through FR-094, NFR-026 through NFR-028, SR-030). The backend API remains unchanged across both deployment models. + **Trace:** Implementation Roadmap --- @@ -4106,7 +4480,11 @@ The design details that realize these requirements -- including component decomp | IR-018: Backend Health Probes | 3.7.3 | Algorithm View | | IR-019: Repository Abstraction Layer | 3.3.11 | Logical View | | IR-020: Background Attestation Recording | 3.5.4 | Interaction View | +| IR-021: Cockpit Context View | 3.1.3 | Context View | +| IR-022: Cockpit Plugin Composition | 3.2.4 | Composition View | +| IR-023: Cockpit Bridge Interface Contracts | 3.4.6 | Interface View | + The SDD also includes a full SRS traceability matrix (Section 6) mapping every implemented requirement to its corresponding design element.