From 7ad5b7ecc3d5f24f89cbd91babf66ef365bf56ce Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Wed, 11 Feb 2026 23:20:41 -0500 Subject: [PATCH] docs: add Priority 2 documentation (ARCHITECTURE, DEPLOYMENT, SECURITY) - Add docs/ARCHITECTURE.md with system architecture, data flow diagrams, component hierarchy, design decisions, and known limitations - Add docs/DEPLOYMENT.md with comprehensive installation guide including Helm integration, RBAC configuration, network policies, plugin manager setup, and troubleshooting - Add SECURITY.md with security model, RBAC requirements, network security, vulnerability reporting, dependency scanning, and compliance considerations Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- SECURITY.md | 358 ++++++++++++++++++++++ docs/ARCHITECTURE.md | 556 ++++++++++++++++++++++++++++++++++ docs/DEPLOYMENT.md | 689 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1603 insertions(+) create mode 100644 SECURITY.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/DEPLOYMENT.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c14bf64 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,358 @@ +# Security Policy + +## Overview + +The Headlamp Polaris Plugin is a read-only visualization tool that displays Fairwinds Polaris audit results within the Headlamp UI. Security considerations primarily revolve around Kubernetes RBAC, network policies, and data access controls. + +## Security Model + +### Read-Only Operation + +The plugin performs **only read operations** via the Kubernetes API server's service proxy mechanism: + +- **No write operations**: The plugin never creates, updates, or deletes Kubernetes resources +- **No CRD installation**: No custom resource definitions or cluster-level modifications +- **No secrets**: The plugin does not read or store Kubernetes secrets +- **No PII**: Polaris audit data contains resource metadata but no personally identifiable information + +### Data Flow + +``` +User Browser + ↓ (HTTPS) +Headlamp Pod + ↓ (in-cluster service account or user token) +Kubernetes API Server + ↓ (service proxy: /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/) +Polaris Dashboard Service + ↓ (returns audit JSON) +Plugin Frontend (React) +``` + +All communication uses Kubernetes authentication and authorization mechanisms. The plugin never stores credentials or bypasses RBAC. + +## RBAC Requirements + +### Minimal Permissions + +The plugin requires only one permission: + +| Verb | API Group | Resource | Resource Name | Namespace | +|------|-----------|----------|---------------|-----------| +| `get` | `""` (core) | `services/proxy` | `polaris-dashboard` | `polaris` | + +**Example minimal Role:** + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: polaris-proxy-reader + namespace: polaris +rules: + - apiGroups: [""] + resources: ["services/proxy"] + resourceNames: ["polaris-dashboard"] + verbs: ["get"] +``` + +### RoleBinding Options + +**Option 1: Service Account (Recommended)** + +Bind to the Headlamp service account for all users: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: headlamp-polaris-proxy + namespace: polaris +subjects: + - kind: ServiceAccount + name: headlamp + namespace: kube-system +roleRef: + kind: Role + name: polaris-proxy-reader + apiGroup: rbac.authorization.k8s.io +``` + +**Option 2: OIDC Groups** + +Bind to user groups for OIDC authentication: + +```yaml +subjects: + - kind: Group + name: "developers" + apiGroup: rbac.authorization.k8s.io +``` + +**Option 3: Specific Users** + +Bind to individual users: + +```yaml +subjects: + - kind: User + name: "jane@example.com" + apiGroup: rbac.authorization.k8s.io +``` + +### ⚠️ Security Best Practices + +1. **Principle of Least Privilege**: Grant only `services/proxy` access, not broader `services` permissions +2. **Namespace Scoping**: Use a namespaced `Role`, not a `ClusterRole`, to limit access to the `polaris` namespace only +3. **Resource Name Restriction**: Always specify `resourceNames: ["polaris-dashboard"]` to prevent proxy access to other services +4. **Audit Logging**: Enable Kubernetes audit logging to track all service proxy requests +5. **Network Policies**: Restrict network access to the Polaris dashboard service (see Network Security below) + +## Network Security + +### Network Policies + +If your cluster uses NetworkPolicies, ensure the Headlamp pod (or more specifically, the Kubernetes API server performing the proxy hop) can reach the Polaris dashboard service. + +**Example NetworkPolicy for Polaris namespace:** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-api-server-to-polaris + namespace: polaris +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: polaris + policyTypes: + - Ingress + ingress: + # Allow from API server (adjust based on your cluster setup) + - from: + - namespaceSelector: {} # API server typically runs in kube-system or no namespace label + ports: + - protocol: TCP + port: 8080 # Polaris dashboard default port +``` + +**Note**: The Kubernetes API server performs the service proxy hop, so network policies should allow traffic from the API server to Polaris, not directly from Headlamp to Polaris. + +### TLS/HTTPS + +- **External Access**: Always access Headlamp over HTTPS, especially when using OIDC authentication +- **Internal Communication**: Communication between Headlamp and the Kubernetes API server uses the service account token over the cluster's internal network +- **Service Proxy**: The API server → Polaris dashboard communication happens over HTTP within the cluster (ClusterIP service) + +## Authentication Methods + +### Service Account (Default) + +Headlamp runs with a dedicated service account (`headlamp` in `kube-system`). All users share the same permissions defined by this service account's RBAC bindings. + +**Security Considerations:** +- All users have identical access to the plugin +- Suitable for trusted internal environments +- Simpler RBAC management + +### OIDC Token Authentication + +Headlamp can be configured for OIDC authentication, where each user provides their own bearer token. RBAC is enforced per-user. + +**Security Considerations:** +- Fine-grained access control per user +- Users without the `polaris-proxy-reader` role will see 403 errors +- Requires OIDC provider integration +- Suitable for multi-tenant or compliance-focused environments + +**Configuration Example:** + +```yaml +config: + oidc: + clientID: "headlamp" + clientSecret: "secret" + issuerURL: "https://authentik.example.com/application/o/headlamp/" + scopes: "openid profile email groups" +``` + +When OIDC is enabled, each user's token is used for API requests, including service proxy calls. + +## Vulnerability Reporting + +### Supported Versions + +We apply security updates to the latest release only. Please ensure you are running the most recent version. + +| Version | Supported | +| ------- | ------------------ | +| latest | :white_check_mark: | +| < latest| :x: | + +### Reporting a Vulnerability + +If you discover a security vulnerability in this plugin, please report it via: + +1. **GitHub Security Advisories**: [Report a vulnerability](https://github.com/cpfarhood/headlamp-polaris-plugin/security/advisories/new) +2. **Email**: Create a GitHub issue and mark it as "security" if advisories are not available + +**Please do not:** +- Open public GitHub issues for security vulnerabilities +- Disclose vulnerabilities publicly before a fix is available + +**Response Timeline:** +- **Acknowledgment**: Within 48 hours +- **Initial Assessment**: Within 1 week +- **Fix Timeline**: Depends on severity (critical: 1-2 weeks, high: 2-4 weeks, medium/low: next release cycle) + +## Dependency Security + +### Dependency Scanning + +The project uses: +- **npm audit**: Runs automatically during `npm install` +- **Dependabot**: GitHub Dependabot monitors dependencies and creates PRs for updates +- **GitHub Actions**: CI workflow runs `npm audit` on every commit + +### Updating Dependencies + +Security patches are applied as follows: + +1. **Critical vulnerabilities**: Emergency patch release within 48 hours +2. **High severity**: Patched in next minor release (typically within 1-2 weeks) +3. **Medium/Low severity**: Included in regular release cycle + +### Headlamp Plugin API + +This plugin depends on `@kinvolk/headlamp-plugin` as a peer dependency. Security updates to Headlamp itself should be applied by upgrading your Headlamp installation. + +**Minimum supported Headlamp version**: v0.26.0 + +## Deployment Security + +### Production Checklist + +Before deploying to production, verify: + +- [ ] **RBAC configured**: `polaris-proxy-reader` Role and RoleBinding exist +- [ ] **Network policies**: Allow API server → Polaris dashboard traffic +- [ ] **TLS enabled**: Headlamp accessible only via HTTPS +- [ ] **OIDC configured** (if using per-user auth): Token-based authentication working +- [ ] **Audit logging enabled**: Kubernetes API audit logs capture service proxy requests +- [ ] **Plugin version**: Running latest release +- [ ] **Dependencies audited**: No critical vulnerabilities in npm dependencies +- [ ] **Polaris version**: Polaris dashboard is up-to-date + +### Kubernetes Cluster Security + +The plugin's security posture depends on your cluster's security: + +- **API Server Access**: Ensure API server is not publicly accessible without authentication +- **Service Account Tokens**: Use projected volume tokens with short expiration (Kubernetes 1.21+) +- **Pod Security Standards**: Apply appropriate pod security policies/standards to the Headlamp namespace +- **RBAC Auditing**: Regularly review RoleBindings to ensure least privilege + +## Common Security Scenarios + +### Scenario 1: 403 Forbidden Error + +**Symptom**: Plugin shows "403 Forbidden" when loading data + +**Cause**: User or service account lacks `services/proxy` permission on `polaris-dashboard` + +**Resolution**: +1. Verify RoleBinding exists in `polaris` namespace +2. Check RoleBinding references correct subject (service account, group, or user) +3. Confirm Role includes `resourceNames: ["polaris-dashboard"]` + +**Security Note**: This is expected behavior when RBAC is correctly enforced. Do not grant broader permissions to "fix" 403 errors. + +### Scenario 2: Exposing Polaris Dashboard Externally + +**Question**: Can I expose Polaris dashboard via Ingress instead of using service proxy? + +**Recommendation**: **Avoid exposing Polaris dashboard externally**. The service proxy approach: +- Enforces Kubernetes RBAC on every request +- Avoids exposing internal services to the internet +- Prevents authentication bypass attacks + +If you must expose Polaris externally: +- Use OAuth2 proxy or similar authentication layer +- Configure NetworkPolicies to restrict access +- Enable TLS with valid certificates +- Consider IP allowlisting + +### Scenario 3: Multi-Tenant Clusters + +**Question**: How do I restrict plugin access in a multi-tenant cluster? + +**Solution**: Use OIDC authentication with per-user RoleBindings: + +```yaml +# Bind only to specific groups or users +subjects: + - kind: Group + name: "team-a" + apiGroup: rbac.authorization.k8s.io +``` + +Users not in `team-a` will receive 403 errors when accessing the plugin, preventing unauthorized access to Polaris audit data. + +## Compliance Considerations + +### Data Residency + +All data remains within your Kubernetes cluster. The plugin does not: +- Send data to external services +- Store data in browser localStorage (except refresh interval preference) +- Use third-party analytics or tracking + +### Audit Trail + +All service proxy requests are logged in Kubernetes API audit logs (if enabled): + +```json +{ + "verb": "get", + "requestURI": "/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json", + "user": { + "username": "system:serviceaccount:kube-system:headlamp", + "groups": ["system:serviceaccounts", "system:authenticated"] + } +} +``` + +### GDPR/Privacy + +The plugin processes only technical metadata (resource names, namespaces, check results). No personal data is collected, stored, or transmitted. + +## Security Updates and Notifications + +### Notification Channels + +Subscribe to security updates via: + +1. **GitHub Watch**: Click "Watch" → "Custom" → "Security alerts" +2. **GitHub Releases**: Monitor [releases page](https://github.com/cpfarhood/headlamp-polaris-plugin/releases) +3. **ArtifactHub**: Follow package at [ArtifactHub](https://artifacthub.io/packages/headlamp/headlamp-polaris-plugin/headlamp-polaris-plugin) + +### Security Patch Process + +When a security vulnerability is identified: + +1. **Private Fix**: Develop fix in private fork +2. **Security Advisory**: Publish GitHub Security Advisory +3. **Release**: Create new version with fix +4. **Notification**: Update advisory with fix version +5. **Disclosure**: Public disclosure after fix is available + +## Contact + +- **Security Issues**: [GitHub Security Advisories](https://github.com/cpfarhood/headlamp-polaris-plugin/security/advisories) +- **General Questions**: [GitHub Discussions](https://github.com/cpfarhood/headlamp-polaris-plugin/discussions) +- **Bug Reports**: [GitHub Issues](https://github.com/cpfarhood/headlamp-polaris-plugin/issues) + +## License + +This plugin is provided under the MIT License. See [LICENSE](LICENSE) for details. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..9c3fd40 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,556 @@ +# Architecture + +This document describes the architecture, design decisions, and data flow of the Headlamp Polaris Plugin. + +## Table of Contents + +- [Overview](#overview) +- [System Architecture](#system-architecture) +- [Data Flow](#data-flow) +- [Component Hierarchy](#component-hierarchy) +- [State Management](#state-management) +- [Design Decisions](#design-decisions) +- [Integration Points](#integration-points) +- [Known Limitations](#known-limitations) + +## Overview + +The Headlamp Polaris Plugin is a **read-only dashboard** that surfaces Fairwinds Polaris audit results within the Headlamp UI. It fetches data from the Polaris dashboard API via the Kubernetes service proxy and presents it in a hierarchical navigation structure. + +**Key Characteristics:** +- **Read-only:** No write operations to cluster or Polaris +- **Service proxy based:** Uses K8s API server proxy to reach Polaris +- **React Context for state:** Shared data fetch across components +- **Headlamp plugin API:** Integrates via official plugin system +- **Type-safe:** Full TypeScript with strict mode + +## System Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Headlamp UI (React) │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ App Bar │ │ Sidebar │ │ Routes │ │ +│ │ (Badge) │ │ (Navigation)│ │ (Views) │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ └──────────────────┼──────────────────┘ │ +│ │ │ +│ ┌────────▼────────┐ │ +│ │ Plugin Registry │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ┌─────────────▼──────────────┐ │ +│ │ Polaris Plugin (This!) │ │ +│ ├────────────────────────────┤ │ +│ │ • registerSidebarEntry │ │ +│ │ • registerRoute │ │ +│ │ • registerAppBarAction │ │ +│ │ • registerPluginSettings │ │ +│ │ • registerDetailsViewSection│ │ +│ └─────────────┬──────────────┘ │ +│ │ │ +│ ┌─────────────▼──────────────┐ │ +│ │ PolarisDataContext │ │ +│ │ (React Context Provider) │ │ +│ └─────────────┬──────────────┘ │ +│ │ │ +│ ┌──────────────────┼──────────────────┐ │ +│ │ │ │ │ +│ ┌────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐ │ +│ │Dashboard │ │ Namespaces │ │ Namespace │ │ +│ │View │ │ ListView │ │ Detail │ │ +│ └──────────┘ └─────────────┘ └─────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌───────▼────────┐ + │ ApiProxy │ + │ (Headlamp) │ + └───────┬────────┘ + │ + ┌───────▼────────┐ + │ Kubernetes │ + │ API Server │ + └───────┬────────┘ + │ + ┌───────▼────────┐ + │ Service Proxy │ + │ /api/v1/ns/ │ + │ polaris/svcs/ │ + │ polaris- │ + │ dashboard/ │ + │ proxy/ │ + └───────┬────────┘ + │ + ┌───────▼────────┐ + │ Polaris │ + │ Dashboard │ + │ (ClusterIP) │ + └───────┬────────┘ + │ + ┌───────▼────────┐ + │ results.json │ + │ (AuditData) │ + └────────────────┘ +``` + +## Data Flow + +### 1. Initial Load + +``` +User loads Headlamp + ↓ +Headlamp loads plugins + ↓ +Plugin registers routes, sidebar, app bar actions + ↓ +User navigates to /polaris + ↓ +DashboardView mounts + ↓ +PolarisDataContext.Provider wraps component + ↓ +usePolarisDataContext() hook triggers fetch + ↓ +ApiProxy.request() → K8s API → Service Proxy → Polaris + ↓ +AuditData returned and cached in Context + ↓ +Components receive data and render +``` + +### 2. Data Refresh + +``` +User clicks "Refresh" button or auto-refresh interval elapses + ↓ +refresh() function called in Context + ↓ +setRefreshKey() increments (forces re-fetch) + ↓ +useEffect dependency triggers new fetch + ↓ +ApiProxy.request() → Polaris Dashboard + ↓ +Context state updated with new data + ↓ +All consuming components re-render automatically +``` + +### 3. Navigation Flow + +``` +User clicks "Polaris" in sidebar + ↓ +Route: /c/main/polaris (DashboardView) + ↓ +Display cluster score, check distribution + ↓ +User clicks "Namespaces" submenu + ↓ +Route: /c/main/polaris/namespaces (NamespacesListView) + ↓ +Display table of namespaces with scores + ↓ +User clicks namespace button in table + ↓ +Drawer opens, URL hash updates (#namespace-name) + ↓ +NamespaceDetailView renders in drawer + ↓ +Display namespace score + resource table +``` + +## Component Hierarchy + +### Plugin Entry Point + +**`src/index.tsx`** +- Registers sidebar entries (Polaris → Overview, Namespaces) +- Registers routes (`/polaris`, `/polaris/namespaces`) +- Registers app bar action (score badge) +- Registers plugin settings page +- Registers details view section (inline audit) + +### Data Layer + +**`src/api/PolarisDataContext.tsx`** +- React Context Provider for shared data +- Fetches AuditData from Polaris dashboard +- Handles auto-refresh based on user settings +- Provides `{ data, loading, error, refresh }` to consumers + +**`src/api/polaris.ts`** +- TypeScript types for AuditData schema +- Utility functions: `countResults()`, `computeScore()` +- Settings management: `getRefreshInterval()`, `setRefreshInterval()` +- Constants: `DASHBOARD_URL_DEFAULT`, `INTERVAL_OPTIONS` + +**`src/api/checkMapping.ts`** +- Maps Polaris check IDs to human-readable names +- Used for display in UI (e.g., "hostIPCSet" → "Host IPC") + +**`src/api/topIssues.ts`** +- Aggregates failing checks across cluster +- Groups by check ID and severity +- Used for top issues dashboard + +### View Components + +**`src/components/DashboardView.tsx`** +- **Route:** `/polaris` +- **Purpose:** Cluster-wide overview +- **Features:** + - Cluster score (percentage) + - Check distribution (pass/warning/danger/skipped) + - Cluster info (Polaris version, last audit time) + - Refresh button +- **Data:** Uses `usePolarisDataContext()` + +**`src/components/NamespacesListView.tsx`** +- **Route:** `/polaris/namespaces` +- **Purpose:** List all namespaces with scores +- **Features:** + - Table with namespace, score, pass/warning/danger counts + - Clickable namespace buttons (opens drawer) + - Sorted by score (lowest first) +- **Data:** Uses `usePolarisDataContext()`, aggregates by namespace + +**`src/components/NamespaceDetailView.tsx`** +- **Route:** Drawer on `/polaris/namespaces#` +- **Purpose:** Namespace-level drill-down +- **Features:** + - Namespace score + - Resource table (kind, name, score, counts) + - URL hash navigation + - Keyboard shortcuts (Escape to close) +- **Data:** Filters `usePolarisDataContext()` by namespace + +### UI Components + +**`src/components/AppBarScoreBadge.tsx`** +- **Location:** Headlamp app bar (top-right) +- **Purpose:** Quick cluster score visibility +- **Features:** + - Color-coded badge (green ≥80%, orange ≥50%, red <50%) + - Clickable (navigates to `/polaris`) + - Shield emoji icon +- **Data:** Uses `usePolarisDataContext()` + +**`src/components/PolarisSettings.tsx`** +- **Location:** Settings → Plugins → Polaris +- **Purpose:** Plugin configuration +- **Features:** + - Refresh interval selector (1 min to 30 min) + - Dashboard URL input (custom Polaris instances) + - Connection test button +- **Data:** localStorage for persistence + +**`src/components/InlineAuditSection.tsx`** +- **Location:** Resource detail pages (Deployment, StatefulSet, etc.) +- **Purpose:** Show Polaris audit inline +- **Features:** + - Pass/warning/danger counts + - Check details with messages + - Severity badges +- **Data:** Uses `usePolarisDataContext()`, filters by resource + +**`src/components/ExemptionManager.tsx`** +- **Location:** (Planned feature, UI exists but not fully integrated) +- **Purpose:** Manage Polaris exemptions via annotations +- **Features:** + - View current exemptions + - Add exemptions for failing checks + - Remove exemptions + +## State Management + +### Why React Context? + +**Decision:** Use React Context instead of Redux/Zustand + +**Rationale:** +1. **Simple state:** Single AuditData object shared across views +2. **Read-only:** No complex mutations or transactions +3. **Headlamp constraints:** Plugin cannot add dependencies (Redux not bundled) +4. **Performance:** Data changes infrequently (refresh interval 1-30 min) + +### Context Structure + +```typescript +interface PolarisDataContextValue { + data: AuditData | null; // Audit results or null if loading/error + loading: boolean; // True during initial fetch + error: string | null; // Error message if fetch failed + refresh: () => void; // Manual refresh function +} +``` + +### Data Fetching Strategy + +1. **Initial fetch:** On first mount of any component using the context +2. **Auto-refresh:** Based on user setting (default 5 minutes) +3. **Manual refresh:** Via refresh button in UI +4. **Caching:** Data persists in context until refresh (no per-route refetch) + +### localStorage Usage + +Settings persisted in localStorage: +- **`polaris-plugin-refresh-interval`**: Number (seconds), default 300 +- **`polaris-plugin-dashboard-url`**: String, default service proxy path + +No sensitive data stored in localStorage. + +## Design Decisions + +### 1. Service Proxy vs. Direct Access + +**Decision:** Use Kubernetes service proxy, not direct ClusterIP access + +**Rationale:** +- Headlamp already has K8s API credentials (service account or user token) +- Service proxy leverages existing RBAC (no new credentials needed) +- Works with Headlamp's token auth and OIDC +- Simpler deployment (no additional network policies for plugin) + +**Trade-off:** +- Requires `get` permission on `services/proxy` resource +- Path is longer: `/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json` + +### 2. Two-Level Sidebar Nesting + +**Decision:** Sidebar has "Polaris" → "Overview" and "Namespaces" (2 levels max) + +**Rationale:** +- Headlamp sidebar supports 2-level nesting maximum +- Deeper nesting (e.g., Polaris → Namespaces → ) doesn't work +- Sidebar Collapse component is route-based, not click-to-toggle + +**Alternative Considered:** +- Dynamic sidebar with namespace entries → rejected (Headlamp limitation) + +**Current Solution:** +- Use table in NamespacesListView with clickable namespace buttons +- Namespace detail opens in drawer (not new route) + +### 3. Drawer Navigation Instead of Routes + +**Decision:** Namespace detail uses drawer, not dedicated route + +**Rationale:** +- Better UX (drawer overlays table, no navigation loss) +- URL hash preserves navigation state (`#namespace-name`) +- Keyboard shortcuts (Escape to close) +- Sidebar doesn't support 3-level nesting for per-namespace routes + +**Implementation:** +- URL: `/polaris/namespaces#kube-system` +- Drawer controlled by hash presence +- `useEffect` watches hash changes + +### 4. No MUI Direct Imports + +**Decision:** Never import from `@mui/material` or `@mui/icons-material` + +**Rationale:** +- Headlamp plugin environment doesn't provide full MUI library +- Importing MUI causes `createSvgIcon undefined` error +- Plugins must use Headlamp CommonComponents only + +**Alternative:** +- Use standard HTML elements with inline styles +- Use theme-aware CSS variables (`--mui-palette-*`) + +### 5. TypeScript Strict Mode + +**Decision:** Enable all TypeScript strict checks + +**Rationale:** +- Catch errors at compile time +- Better IDE support and autocomplete +- Enforces type safety (no `any`, no implicit unknowns) + +**Impact:** +- More verbose code (explicit types required) +- Better maintainability and refactorability + +### 6. Auto-Refresh Default: 5 Minutes + +**Decision:** Default refresh interval is 5 minutes (configurable) + +**Rationale:** +- Polaris audits typically run every 10-30 minutes +- Balance between data freshness and API load +- User can configure from 1 minute to 30 minutes + +**Considered:** +- WebSocket/SSE for real-time updates → rejected (Polaris dashboard doesn't support) +- Shorter default → rejected (unnecessary API calls) + +## Integration Points + +### Headlamp Plugin API + +**Version:** ≥ v0.13.0 + +**Registration Functions Used:** + +```typescript +// Sidebar navigation +registerSidebarEntry({ parent, name, label, url, icon }) + +// Routes +registerRoute({ path, sidebar, name, exact, component }) + +// App bar actions +registerAppBarAction(component) + +// Plugin settings +registerPluginSettings(name, component, displaySaveButton) + +// Resource detail sections +registerDetailsViewSection(component) +``` + +**Key Changes in v0.13.0:** +- `registerDetailsViewSection` now takes 1 argument (component), not 2 (name, component) +- `registerAppBarAction` now takes 1 argument (component), not 2 (name, component) + +### Headlamp CommonComponents + +**Used Components:** +- `SectionBox` - Card-like container with title +- `SectionHeader` - Page header with title +- `StatusLabel` - Color-coded status badges +- `NameValueTable` - Key-value table layout +- `SimpleTable` - Data table with sorting +- `Drawer` - Right-side overlay panel +- `Loader` - Loading spinner + +**Router:** +- `Router.createRouteURL()` - Generate plugin route URLs +- React Router's `useHistory()`, `useParams()`, `useLocation()` + +### Kubernetes API (via ApiProxy) + +**Used for:** +- Fetching Polaris results: `ApiProxy.request(dashboardUrl + 'results.json')` +- No direct K8s API calls (all data from Polaris dashboard) + +**RBAC Required:** +- `get` on `services/proxy` for `polaris-dashboard` in `polaris` namespace + +## Known Limitations + +### 1. Sidebar Nesting Depth + +**Limitation:** Headlamp sidebar supports only 2 levels + +**Impact:** Cannot have dynamic per-namespace sidebar entries + +**Workaround:** Use table with drawer navigation + +### 2. Skipped Checks Visibility + +**Limitation:** Skipped checks (severity "ignore") counted but details not shown in dashboard + +**Reason:** Polaris API groups skipped checks but doesn't provide per-check details + +**Impact:** Users see skipped count but can't drill down to specific skipped checks + +**Documented:** README, tooltip on skipped count + +### 3. No Write Operations + +**Limitation:** Plugin cannot modify Polaris configuration or exemptions + +**Reason:** Read-only by design (service proxy only has `get` permission) + +**Impact:** Exemption manager UI exists but requires manual annotation edits + +**Future:** Could add PATCH permission to enable exemption annotations via UI + +### 4. No Real-Time Updates + +**Limitation:** Data refreshes on interval (1-30 minutes), not real-time + +**Reason:** Polaris dashboard doesn't support WebSocket/SSE + +**Impact:** Users may see stale data between refreshes + +**Workaround:** Manual refresh button, configurable interval + +### 5. MUI Import Restrictions + +**Limitation:** Cannot import MUI components directly + +**Reason:** Headlamp plugin environment doesn't provide full MUI bundle + +**Impact:** Must use Headlamp CommonComponents or HTML elements + +**Documented:** CLAUDE.md, CONTRIBUTING.md + +### 6. Single Cluster Support + +**Limitation:** Plugin shows data for current cluster only + +**Reason:** Headlamp's multi-cluster support is route-based (`/c//...`) + +**Impact:** Users must switch clusters in Headlamp to see different cluster's Polaris data + +**Future:** Could enhance to aggregate multi-cluster if Headlamp API supports it + +## Performance Considerations + +### Bundle Size + +- **Current:** ~27 KB minified (gzip: ~7.6 KB) +- **Target:** Keep under 50 KB to ensure fast loading +- **Strategy:** No heavy dependencies, tree-shaking enabled + +### Data Fetching + +- **Lazy loading:** Data not fetched until user navigates to plugin +- **Caching:** Single fetch shared across all views (React Context) +- **Refresh strategy:** User-controlled interval prevents excessive API calls + +### Rendering + +- **React.memo:** Not needed (data changes infrequently) +- **Virtual scrolling:** Not needed (namespace/resource lists typically <100 items) +- **Component splitting:** Lazy load views if bundle grows significantly + +## Future Architecture Enhancements + +### Potential Improvements + +1. **WebWorker for data processing** + - Offload `countResults()` aggregation for large clusters + - Keep UI responsive during heavy computation + +2. **IndexedDB caching** + - Cache audit data offline + - Show stale data + "refresh available" indicator + +3. **GraphQL/REST API abstraction** + - Decouple from Polaris dashboard JSON format + - Support multiple backend sources + +4. **Plugin-to-plugin communication** + - Integrate with other Headlamp plugins (e.g., policy enforcement) + - Shared state between plugins + +5. **Incremental updates** + - Fetch only changed namespaces/resources + - Reduce bandwidth and processing + +## References + +- [Headlamp Plugin Development](https://headlamp.dev/docs/latest/development/plugins/) +- [Fairwinds Polaris Documentation](https://polaris.docs.fairwinds.com/) +- [React Context API](https://react.dev/reference/react/useContext) +- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/) diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..7dcdbc6 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,689 @@ +# Deployment Guide + +This document provides comprehensive deployment instructions for the Headlamp Polaris Plugin in production Kubernetes environments. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Installation Methods](#installation-methods) +- [Helm Integration](#helm-integration) +- [RBAC Configuration](#rbac-configuration) +- [Network Policies](#network-policies) +- [Plugin Manager Setup](#plugin-manager-setup) +- [Production Checklist](#production-checklist) +- [Troubleshooting](#troubleshooting) + +## Prerequisites + +### Required Components + +1. **Kubernetes Cluster:** v1.19 or later +2. **Headlamp:** v0.26 or later (v0.39+ recommended) +3. **Polaris:** Deployed and accessible via service +4. **RBAC:** Permissions to create Roles and RoleBindings + +### Pre-Deployment Verification + +```bash +# Verify Polaris is deployed +kubectl -n polaris get pods +kubectl -n polaris get svc polaris-dashboard + +# Verify Polaris dashboard is responding +kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion + +# Verify Headlamp is deployed +kubectl -n kube-system get pods -l app.kubernetes.io/name=headlamp +``` + +## Installation Methods + +### Method 1: Headlamp Plugin Manager (Recommended) + +**Best for:** Production deployments, managed updates + +1. **Enable Plugin Manager in Headlamp:** + + ```yaml + # headlamp-values.yaml + config: + pluginsDir: "/headlamp/plugins" + +pluginsManager: + enabled: true + repositories: + - https://artifacthub.io/packages/search?kind=4 + ``` + +2. **Deploy/Update Headlamp:** + + ```bash + helm upgrade --install headlamp headlamp/headlamp \ + --namespace kube-system \ + --values headlamp-values.yaml + ``` + +3. **Install Plugin via UI:** + - Navigate to Headlamp → Settings → Plugins + - Search for "Polaris" + - Click "Install" + - Refresh browser (Cmd+Shift+R or Ctrl+Shift+R) + +### Method 2: Sidecar Container (Alternative) + +**Best for:** Controlled plugin versions, air-gapped environments + +```yaml +# headlamp-values.yaml +config: + pluginsDir: "/headlamp/plugins" + watchPlugins: false # CRITICAL: Must be false for plugin manager + +replicaCount: 1 + +initContainers: + - name: install-polaris-plugin + image: node:lts-alpine + command: + - sh + - -c + - | + npm install -g @kinvolk/headlamp-plugin + headlamp-plugin install --config /config/plugin.yml --plugins-dir /plugins + volumeMounts: + - name: plugins + mountPath: /plugins + - name: plugin-config + mountPath: /config + +volumes: + - name: plugins + emptyDir: {} + - name: plugin-config + configMap: + name: headlamp-plugin-config + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: headlamp-plugin-config + namespace: kube-system +data: + plugin.yml: | + - name: headlamp-polaris-plugin + version: 0.3.4 + url: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.3.4/headlamp-polaris-plugin-0.3.4.tar.gz +``` + +### Method 3: Volume Mount (Development) + +**Best for:** Local testing, development + +```yaml +# headlamp-values.yaml +config: + pluginsDir: "/plugins" + +volumes: + - name: plugins + hostPath: + path: /path/to/plugins + type: Directory + +volumeMounts: + - name: plugins + mountPath: /plugins + readOnly: true +``` + +Then manually place `headlamp-polaris-plugin/` in the host path. + +## Helm Integration + +### Complete Helm Values Example + +```yaml +# headlamp-values.yaml +replicaCount: 2 + +image: + repository: ghcr.io/headlamp-k8s/headlamp + tag: v0.39.0 + +config: + baseURL: "" + pluginsDir: "/headlamp/plugins" + watchPlugins: false # MUST be false for plugin manager + +pluginsManager: + enabled: true + repositories: + - https://artifacthub.io/packages/search?kind=4 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: true + className: nginx + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + hosts: + - host: headlamp.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: headlamp-tls + hosts: + - headlamp.example.com + +serviceAccount: + create: true + name: headlamp + +resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + +# OIDC Authentication (optional) +env: + - name: HEADLAMP_CONFIG_OIDC_CLIENT_ID + value: "headlamp" + - name: HEADLAMP_CONFIG_OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: headlamp-oidc + key: client-secret + - name: HEADLAMP_CONFIG_OIDC_ISSUER_URL + value: "https://auth.example.com/realms/kubernetes" + - name: HEADLAMP_CONFIG_OIDC_SCOPES + value: "openid,profile,email,groups" +``` + +### FluxCD HelmRelease Example + +```yaml +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: headlamp + namespace: kube-system +spec: + interval: 30m + chart: + spec: + chart: headlamp + version: 0.26.x + sourceRef: + kind: HelmRepository + name: headlamp + namespace: flux-system + interval: 12h + + values: + config: + pluginsDir: "/headlamp/plugins" + watchPlugins: false + + pluginsManager: + enabled: true + repositories: + - https://artifacthub.io/packages/search?kind=4 + + service: + type: ClusterIP + + ingress: + enabled: true + className: nginx + hosts: + - host: headlamp.example.com + paths: + - path: / + pathType: Prefix +``` + +## RBAC Configuration + +### Minimal Role for Plugin + +The plugin requires **read-only** access to the Polaris dashboard service proxy. + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: polaris-proxy-reader + namespace: polaris +rules: + - apiGroups: [""] + resources: ["services/proxy"] + resourceNames: ["polaris-dashboard"] + verbs: ["get"] +``` + +### RoleBinding Options + +#### Option A: Headlamp Service Account (In-Cluster Mode) + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: headlamp-polaris-proxy + namespace: polaris +subjects: + - kind: ServiceAccount + name: headlamp + namespace: kube-system +roleRef: + kind: Role + name: polaris-proxy-reader + apiGroup: rbac.authorization.k8s.io +``` + +#### Option B: User Groups (Token/OIDC Mode) + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: users-polaris-proxy + namespace: polaris +subjects: + - kind: Group + name: system:authenticated # All authenticated users + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: Role + name: polaris-proxy-reader + apiGroup: rbac.authorization.k8s.io +``` + +#### Option C: Specific Users (Fine-Grained Control) + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: devops-polaris-proxy + namespace: polaris +subjects: + - kind: User + name: alice@example.com + apiGroup: rbac.authorization.k8s.io + - kind: User + name: bob@example.com + apiGroup: rbac.authorization.k8s.io + - kind: Group + name: devops-team + apiGroup: rbac.authorization.k8s.io +roleRef: + kind: Role + name: polaris-proxy-reader + apiGroup: rbac.authorization.k8s.io +``` + +### Complete RBAC Manifest + +```yaml +--- +# Role: Read-only access to Polaris service proxy +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: polaris-proxy-reader + namespace: polaris + labels: + app.kubernetes.io/name: headlamp-polaris-plugin + app.kubernetes.io/component: rbac +rules: + - apiGroups: [""] + resources: ["services/proxy"] + resourceNames: ["polaris-dashboard"] + verbs: ["get"] + +--- +# RoleBinding: Grant Headlamp service account access +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: headlamp-polaris-proxy + namespace: polaris + labels: + app.kubernetes.io/name: headlamp-polaris-plugin + app.kubernetes.io/component: rbac +subjects: + - kind: ServiceAccount + name: headlamp + namespace: kube-system +roleRef: + kind: Role + name: polaris-proxy-reader + apiGroup: rbac.authorization.k8s.io +``` + +Apply with: +```bash +kubectl apply -f polaris-plugin-rbac.yaml +``` + +## Network Policies + +### Required Network Access + +The plugin requires network connectivity: +- **Headlamp pod** → **Kubernetes API server** (service proxy) +- **Kubernetes API server** → **Polaris dashboard service** (port 80) + +### Network Policy Example + +If your `polaris` namespace has strict NetworkPolicies: + +```yaml +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-headlamp-to-polaris + namespace: polaris +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: polaris + app.kubernetes.io/component: dashboard + policyTypes: + - Ingress + ingress: + # Allow from API server (service proxy) + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + - podSelector: + matchLabels: + component: kube-apiserver + ports: + - protocol: TCP + port: 80 +``` + +**Note:** The API server performs the proxy hop, not the Headlamp pod directly. + +## Plugin Manager Setup + +### Critical Configuration + +**❌ WRONG (Will not load plugins):** +```yaml +config: + watchPlugins: true # Default, treats catalog plugins as dev plugins +``` + +**✅ CORRECT:** +```yaml +config: + watchPlugins: false # Required for plugin manager catalog plugins +``` + +### Why `watchPlugins: false` is Required + +- **With `watchPlugins: true`:** Headlamp backend serves plugin metadata, but frontend never executes the JavaScript (treated as development directory plugin) +- **Result:** Plugins appear in Settings but no sidebar/routes/settings work +- **Fix:** Set `watchPlugins: false` in Headlamp configuration +- **Documentation:** See `deployment/PLUGIN_LOADING_FIX.md` for root cause analysis + +### Plugin Manager Verification + +```bash +# Check Headlamp config +kubectl -n kube-system get configmap headlamp -o yaml | grep watchPlugins + +# Expected output: +# watchPlugins: "false" + +# Check plugin is installed +kubectl -n kube-system exec -it deployment/headlamp -- ls -la /headlamp/plugins/ + +# Expected output: +# drwxr-xr-x headlamp-polaris-plugin/ +``` + +## Production Checklist + +### Pre-Deployment + +- [ ] Polaris deployed and running +- [ ] Polaris dashboard service exists (`polaris-dashboard` in `polaris` namespace) +- [ ] RBAC Role and RoleBinding created +- [ ] Headlamp v0.26+ deployed +- [ ] `watchPlugins: false` set in Headlamp config + +### Deployment + +- [ ] Plugin installed via plugin manager or sidecar +- [ ] Headlamp pods restarted (if config changed) +- [ ] Browser cache cleared (Cmd+Shift+R / Ctrl+Shift+R) + +### Post-Deployment Verification + +```bash +# 1. Verify Polaris is accessible via service proxy +kubectl get --raw /api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json | jq .PolarisOutputVersion + +# Expected: "1.0" or similar + +# 2. Verify RBAC is correct +kubectl auth can-i get services/proxy --as=system:serviceaccount:kube-system:headlamp -n polaris --resource-name=polaris-dashboard + +# Expected: yes + +# 3. Check Headlamp logs +kubectl -n kube-system logs deployment/headlamp | grep -i polaris + +# Expected: No errors related to plugin loading + +# 4. Verify plugin files exist +kubectl -n kube-system exec -it deployment/headlamp -- ls -la /headlamp/plugins/headlamp-polaris-plugin/ + +# Expected: dist/, package.json present +``` + +### UI Verification + +- [ ] Navigate to Headlamp → Settings → Plugins +- [ ] Plugin "headlamp-polaris-plugin" listed +- [ ] Sidebar shows "Polaris" entry +- [ ] Click "Polaris" → Overview page loads +- [ ] Cluster score displays correctly +- [ ] Namespaces page shows table +- [ ] App bar shows Polaris score badge + +## Troubleshooting + +### Plugin Not Appearing in Sidebar + +**Symptom:** Plugin listed in Settings → Plugins but no sidebar entry + +**Causes:** +1. `watchPlugins: true` (should be `false`) +2. Browser cache not cleared + +**Solution:** +```bash +# Fix Headlamp config +kubectl -n kube-system edit configmap headlamp +# Set watchPlugins: false + +# Restart Headlamp +kubectl -n kube-system rollout restart deployment/headlamp + +# Clear browser cache +# Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows/Linux) +``` + +### 403 Forbidden Error + +**Symptom:** Error loading Polaris data, 403 in console + +**Cause:** RBAC missing or incorrect + +**Solution:** +```bash +# Verify RBAC exists +kubectl -n polaris get role polaris-proxy-reader +kubectl -n polaris get rolebinding headlamp-polaris-proxy + +# Test permission +kubectl auth can-i get services/proxy --as=system:serviceaccount:kube-system:headlamp -n polaris --resource-name=polaris-dashboard + +# If "no", create RBAC (see RBAC Configuration section) +``` + +### 404 Not Found Error + +**Symptom:** Error loading Polaris data, 404 in console + +**Causes:** +1. Polaris not deployed +2. Polaris service name wrong +3. Polaris namespace wrong + +**Solution:** +```bash +# Check Polaris deployment +kubectl -n polaris get pods +kubectl -n polaris get svc polaris-dashboard + +# If service doesn't exist, install Polaris: +helm install polaris fairwinds-stable/polaris \ + --namespace polaris \ + --create-namespace \ + --set dashboard.enabled=true +``` + +### Custom Dashboard URL Not Working + +**Symptom:** Error when using custom Polaris URL in settings + +**Causes:** +1. URL format incorrect +2. CORS not configured on external Polaris +3. Network policy blocking external access + +**Solution:** +```bash +# Test URL manually +curl -v https://my-polaris.example.com/results.json + +# For external Polaris, check CORS headers +# Must allow Headlamp origin +``` + +### Plugin Shows Old Version + +**Symptom:** Plugin version in Settings doesn't match expected + +**Cause:** Plugin manager hasn't synced from ArtifactHub + +**Solution:** +```bash +# Wait 30 minutes (ArtifactHub sync interval) +# Or manually refresh plugin list in Headlamp UI + +# Force Headlamp restart +kubectl -n kube-system rollout restart deployment/headlamp +``` + +### Network Policy Blocking Access + +**Symptom:** Timeout or connection errors despite correct RBAC + +**Cause:** NetworkPolicy in `polaris` namespace blocking API server + +**Solution:** +```bash +# Check NetworkPolicies +kubectl -n polaris get networkpolicy + +# Test connectivity from API server (if possible) +# Add NetworkPolicy to allow API server → Polaris dashboard (see Network Policies section) +``` + +## Security Considerations + +### Least Privilege + +- Grant only `get` on `services/proxy`, not broader permissions +- Use `resourceNames` to restrict to specific service (`polaris-dashboard`) +- Scope to `polaris` namespace only (Role, not ClusterRole) + +### Audit Logging + +Kubernetes audit logs will record: +- User/service account accessing service proxy +- Timestamp and response code + +Configure audit policy if needed: +```yaml +apiVersion: audit.k8s.io/v1 +kind: Policy +rules: + - level: Metadata + verbs: ["get"] + resources: + - group: "" + resources: ["services/proxy"] + namespaces: ["polaris"] +``` + +### Data Sensitivity + +Polaris audit data may contain: +- Resource names and namespaces +- Configuration details +- Potential security vulnerabilities + +**Recommendation:** Restrict plugin access to authorized users only (not `system:authenticated` group unless appropriate). + +## Upgrading + +### Plugin Upgrade via Plugin Manager + +1. Navigate to Settings → Plugins +2. Find "headlamp-polaris-plugin" +3. Click "Update" if new version available +4. Refresh browser (Cmd+Shift+R / Ctrl+Shift+R) + +### Sidecar Method Upgrade + +1. Update ConfigMap with new version/URL +2. Restart Headlamp deployment +3. Verify new version in Settings → Plugins + +```bash +kubectl -n kube-system edit configmap headlamp-plugin-config +# Update version and URL + +kubectl -n kube-system rollout restart deployment/headlamp +``` + +## References + +- [Headlamp Deployment](https://headlamp.dev/docs/latest/installation/) +- [Headlamp Helm Chart](https://github.com/headlamp-k8s/headlamp/tree/main/charts/headlamp) +- [Polaris Installation](https://polaris.docs.fairwinds.com/infrastructure-as-code/) +- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) +- [Kubernetes Service Proxy](https://kubernetes.io/docs/tasks/administer-cluster/access-cluster-services/#manually-constructing-apiserver-proxy-urls)