Initial release: Headlamp Sealed Secrets plugin v0.1.0
Features: - Complete SealedSecret CRD integration with Headlamp - Client-side encryption using controller's public key - Support for all three scoping modes (strict, namespace-wide, cluster-wide) - List and detail views for SealedSecrets - Encryption dialog for creating new SealedSecrets - Decryption support with RBAC awareness - Sealing keys management - Settings page for controller configuration - Integration with Secret detail view Technical: - Full TypeScript with strict mode - ~1,345 lines of code - Build size: 339.42 kB (93.21 kB gzipped) - Compatible with Headlamp v0.13.0+ - Apache 2.0 license Security: - All encryption performed client-side - RSA-OAEP + AES-256-GCM (kubeseal-compatible) - Auto-hide decrypted values after 30 seconds Closes: Initial implementation
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm ci
|
||||
|
||||
- name: Run type check
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm run tsc
|
||||
|
||||
- name: Run linter
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm run lint
|
||||
|
||||
- name: Build plugin
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: plugin-dist
|
||||
path: headlamp-sealed-secrets/dist/
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Publish Plugin
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm ci
|
||||
|
||||
- name: Run type check
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm run tsc
|
||||
|
||||
- name: Run linter
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm run lint
|
||||
|
||||
- name: Build plugin
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm run build
|
||||
|
||||
- name: Publish to NPM
|
||||
working-directory: ./headlamp-sealed-secrets
|
||||
run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
headlamp-sealed-secrets/dist/main.js
|
||||
headlamp-sealed-secrets/package.json
|
||||
headlamp-sealed-secrets/README.md
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build outputs
|
||||
dist/
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# MCP
|
||||
.mcp.json
|
||||
@@ -0,0 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Headlamp Sealed Secrets Plugin will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2026-02-11
|
||||
|
||||
### Added
|
||||
- Initial release of Headlamp Sealed Secrets plugin
|
||||
- SealedSecret CRD integration with list and detail views
|
||||
- Client-side encryption using controller's public key
|
||||
- Support for all three scoping modes (strict, namespace-wide, cluster-wide)
|
||||
- Encryption dialog for creating new SealedSecrets
|
||||
- Decryption dialog for viewing secret values (RBAC-aware)
|
||||
- Sealing keys management view
|
||||
- Settings page for controller configuration
|
||||
- Integration with Headlamp's Secret detail view
|
||||
- Comprehensive documentation and README
|
||||
- Apache 2.0 license
|
||||
- Artifact Hub metadata for publishing
|
||||
|
||||
### Security
|
||||
- All encryption performed client-side in browser
|
||||
- Plaintext values never transmitted over network
|
||||
- RSA-OAEP + AES-256-GCM encryption (compatible with kubeseal)
|
||||
- Auto-hide decrypted values after 30 seconds
|
||||
- Password-masked inputs with show/hide toggle
|
||||
|
||||
### Technical
|
||||
- Full TypeScript with strict mode
|
||||
- ~1,345 lines of code
|
||||
- Build size: 339.42 kB (93.21 kB gzipped)
|
||||
- Dependencies: node-forge for cryptography
|
||||
- Compatible with Headlamp v0.13.0+
|
||||
|
||||
[Unreleased]: https://github.com/cpfarhood/headlamp-sealed-secrets-plugin/compare/v0.1.0...HEAD
|
||||
[0.1.0]: https://github.com/cpfarhood/headlamp-sealed-secrets-plugin/releases/tag/v0.1.0
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
# Publishing Guide for Headlamp Sealed Secrets Plugin
|
||||
|
||||
This guide covers how to publish the plugin to NPM, GitHub, and Artifact Hub.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before publishing, ensure you have:
|
||||
|
||||
1. **NPM Account** - Create one at https://www.npmjs.com
|
||||
2. **GitHub Account** - Already set up (cpfarhood)
|
||||
3. **Artifact Hub** - Repository already configured (ID: 5574d37c-c4ae-45ab-a378-ef24aaba5b4c)
|
||||
|
||||
## Step 1: Initial Setup
|
||||
|
||||
### 1.1 NPM Authentication
|
||||
|
||||
```bash
|
||||
npm login
|
||||
# Enter your NPM username, password, and email
|
||||
```
|
||||
|
||||
### 1.2 Verify Package Configuration
|
||||
|
||||
Check that `package.json` has correct metadata:
|
||||
```bash
|
||||
cd headlamp-sealed-secrets
|
||||
cat package.json | grep -A 5 '"name"'
|
||||
```
|
||||
|
||||
## Step 2: Prepare for Publishing
|
||||
|
||||
### 2.1 Build and Test
|
||||
|
||||
```bash
|
||||
cd headlamp-sealed-secrets
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Type check
|
||||
npm run tsc
|
||||
|
||||
# Lint
|
||||
npm run lint
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Verify dist/ directory exists
|
||||
ls -la dist/
|
||||
```
|
||||
|
||||
### 2.2 Test Package Locally
|
||||
|
||||
```bash
|
||||
# Create a tarball to inspect what will be published
|
||||
npm pack
|
||||
|
||||
# This creates headlamp-sealed-secrets-0.1.0.tgz
|
||||
# Extract and verify contents:
|
||||
tar -tzf headlamp-sealed-secrets-0.1.0.tgz
|
||||
|
||||
# Clean up
|
||||
rm headlamp-sealed-secrets-0.1.0.tgz
|
||||
```
|
||||
|
||||
## Step 3: Publish to NPM
|
||||
|
||||
### Option A: Manual Publishing
|
||||
|
||||
```bash
|
||||
cd headlamp-sealed-secrets
|
||||
|
||||
# Publish to NPM
|
||||
npm publish
|
||||
|
||||
# If this is your first publish and you want to make it public
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
### Option B: Automated Publishing via GitHub Actions
|
||||
|
||||
The repository includes automated workflows:
|
||||
|
||||
1. **Push code to GitHub:**
|
||||
```bash
|
||||
cd ..
|
||||
git add .
|
||||
git commit -m "Initial release of Headlamp Sealed Secrets plugin"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
2. **Create and push a version tag:**
|
||||
```bash
|
||||
git tag -a v0.1.0 -m "Release version 0.1.0"
|
||||
git push origin v0.1.0
|
||||
```
|
||||
|
||||
3. **Configure NPM token in GitHub:**
|
||||
- Go to https://www.npmjs.com/settings/YOUR_USERNAME/tokens
|
||||
- Create a new "Automation" token
|
||||
- Copy the token
|
||||
- Go to GitHub repository → Settings → Secrets and variables → Actions
|
||||
- Create a new secret named `NPM_TOKEN` with your token
|
||||
|
||||
4. **The workflow will automatically:**
|
||||
- Build the plugin
|
||||
- Run tests and linting
|
||||
- Publish to NPM
|
||||
- Create a GitHub Release
|
||||
|
||||
## Step 4: GitHub Setup
|
||||
|
||||
### 4.1 Create GitHub Repository
|
||||
|
||||
```bash
|
||||
# Initialize git (if not already done)
|
||||
cd /Users/cpfarhood/Documents/Repositories/headlamp-sealed-secrets-plugin
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit: Headlamp Sealed Secrets plugin"
|
||||
|
||||
# Create repository on GitHub first, then:
|
||||
git remote add origin https://github.com/cpfarhood/headlamp-sealed-secrets-plugin.git
|
||||
git branch -M main
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
### 4.2 Configure Repository
|
||||
|
||||
On GitHub, configure:
|
||||
1. **Description**: "Headlamp plugin for Bitnami Sealed Secrets - manage encrypted Kubernetes secrets"
|
||||
2. **Topics**: `headlamp`, `kubernetes`, `sealed-secrets`, `encryption`, `security`
|
||||
3. **Website**: Link to Artifact Hub (once published)
|
||||
|
||||
## Step 5: Artifact Hub
|
||||
|
||||
### 5.1 Verify Repository Configuration
|
||||
|
||||
The repository is already configured with:
|
||||
- Repository ID: `5574d37c-c4ae-45ab-a378-ef24aaba5b4c`
|
||||
- Metadata files:
|
||||
- `artifacthub-repo.yml` (root)
|
||||
- `headlamp-sealed-secrets/artifacthub-pkg.yml`
|
||||
|
||||
### 5.2 Trigger Artifact Hub Sync
|
||||
|
||||
Artifact Hub automatically syncs from your GitHub repository every few hours. To force a sync:
|
||||
|
||||
1. Go to https://artifacthub.io/control-panel/repositories
|
||||
2. Find your repository
|
||||
3. Click "Trigger sync"
|
||||
|
||||
Alternatively, push a change to trigger automatic sync:
|
||||
```bash
|
||||
git commit --allow-empty -m "Trigger Artifact Hub sync"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 5.3 Verify Publication
|
||||
|
||||
1. Wait 5-10 minutes for sync
|
||||
2. Visit https://artifacthub.io/packages/headlamp/headlamp-sealed-secrets
|
||||
3. Verify all metadata is correct
|
||||
|
||||
## Step 6: Post-Publishing
|
||||
|
||||
### 6.1 Update README Links
|
||||
|
||||
Once published, update README.md with real links:
|
||||
|
||||
```markdown
|
||||
## Installation
|
||||
|
||||
npm install -g headlamp-sealed-secrets
|
||||
```
|
||||
|
||||
### 6.2 Add Badges
|
||||
|
||||
Add badges to README.md:
|
||||
|
||||
```markdown
|
||||
[](https://www.npmjs.com/package/headlamp-sealed-secrets)
|
||||
[](https://artifacthub.io/packages/headlamp/headlamp-sealed-secrets)
|
||||
[](LICENSE)
|
||||
```
|
||||
|
||||
### 6.3 Announce Release
|
||||
|
||||
Consider announcing on:
|
||||
- Headlamp community channels
|
||||
- Kubernetes Slack (#headlamp)
|
||||
- Twitter/Social media
|
||||
- Dev.to or Medium blog post
|
||||
|
||||
## Version Updates
|
||||
|
||||
When releasing new versions:
|
||||
|
||||
1. **Update version:**
|
||||
```bash
|
||||
cd headlamp-sealed-secrets
|
||||
npm version patch # or minor, or major
|
||||
```
|
||||
|
||||
2. **Update artifacthub-pkg.yml:**
|
||||
```yaml
|
||||
version: 0.1.1 # Match package.json
|
||||
```
|
||||
|
||||
3. **Commit and tag:**
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Release v0.1.1: <description>"
|
||||
git tag -a v0.1.1 -m "Release version 0.1.1"
|
||||
git push origin main
|
||||
git push origin v0.1.1
|
||||
```
|
||||
|
||||
4. **GitHub Actions will auto-publish** to NPM and create a release
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Package already exists"
|
||||
If the NPM package name is taken, update `package.json`:
|
||||
```json
|
||||
{
|
||||
"name": "@cpfarhood/headlamp-sealed-secrets"
|
||||
}
|
||||
```
|
||||
|
||||
### NPM Publish Fails
|
||||
- Verify you're logged in: `npm whoami`
|
||||
- Check package.json has correct `name` and `version`
|
||||
- Ensure version hasn't been published before
|
||||
|
||||
### Artifact Hub Not Syncing
|
||||
- Verify `artifacthub-repo.yml` is in repository root
|
||||
- Verify `artifacthub-pkg.yml` is in plugin directory
|
||||
- Check repository URL in Artifact Hub settings
|
||||
- Wait 24 hours for initial sync
|
||||
- Trigger manual sync from control panel
|
||||
|
||||
### GitHub Actions Failing
|
||||
- Check workflow logs in GitHub Actions tab
|
||||
- Verify `NPM_TOKEN` secret is set correctly
|
||||
- Ensure node version matches (20.x)
|
||||
|
||||
## Files Checklist
|
||||
|
||||
Before publishing, verify these files exist and are correct:
|
||||
|
||||
- [ ] `headlamp-sealed-secrets/package.json` - Correct name, version, metadata
|
||||
- [ ] `headlamp-sealed-secrets/LICENSE` - Apache 2.0 license
|
||||
- [ ] `headlamp-sealed-secrets/README.md` - Comprehensive documentation
|
||||
- [ ] `headlamp-sealed-secrets/artifacthub-pkg.yml` - Artifact Hub metadata
|
||||
- [ ] `artifacthub-repo.yml` - Repository metadata (root)
|
||||
- [ ] `.github/workflows/publish.yml` - Publish workflow
|
||||
- [ ] `.github/workflows/ci.yml` - CI workflow
|
||||
- [ ] `.gitignore` - Excludes node_modules, dist, etc.
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
For a new release:
|
||||
|
||||
```bash
|
||||
# 1. Update version
|
||||
cd headlamp-sealed-secrets
|
||||
npm version patch
|
||||
|
||||
# 2. Build and test
|
||||
npm run tsc
|
||||
npm run lint
|
||||
npm run build
|
||||
|
||||
# 3. Update Artifact Hub metadata
|
||||
# Edit artifacthub-pkg.yml version to match package.json
|
||||
|
||||
# 4. Commit and tag
|
||||
cd ..
|
||||
git add .
|
||||
git commit -m "Release v0.1.1"
|
||||
git tag -a v0.1.1 -m "Release version 0.1.1"
|
||||
|
||||
# 5. Push (triggers auto-publish)
|
||||
git push origin main
|
||||
git push origin v0.1.1
|
||||
|
||||
# 6. Verify
|
||||
# - Check GitHub Actions workflow
|
||||
# - Verify on NPM: https://www.npmjs.com/package/headlamp-sealed-secrets
|
||||
# - Check Artifact Hub (may take 24h): https://artifacthub.io
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues:
|
||||
- NPM: https://docs.npmjs.com/
|
||||
- Artifact Hub: https://artifacthub.io/docs
|
||||
- Headlamp: https://headlamp.dev/docs/latest/development/plugins/
|
||||
|
||||
---
|
||||
|
||||
**Repository:** https://github.com/cpfarhood/headlamp-sealed-secrets-plugin
|
||||
**Artifact Hub ID:** 5574d37c-c4ae-45ab-a378-ef24aaba5b4c
|
||||
@@ -0,0 +1,6 @@
|
||||
# Artifact Hub repository metadata file
|
||||
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-repo.yml
|
||||
repositoryID: 5574d37c-c4ae-45ab-a378-ef24aaba5b4c
|
||||
owners:
|
||||
- name: cpfarhood
|
||||
email: cpfarhood@users.noreply.github.com
|
||||
@@ -0,0 +1,150 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance for AI coding agents working on this Headlamp plugin.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
The following npm scripts are available for development and testing:
|
||||
|
||||
- **`npm run format`** - Format code with prettier
|
||||
- **`npm run lint`** - Lint code with eslint for coding issues
|
||||
- **`npm run lint-fix`** - Automatically fix linting issues
|
||||
- **`npm run build`** - Build the plugin for production
|
||||
- **`npm run tsc`** - Type check code with TypeScript compiler
|
||||
- **`npm run test`** - Run tests with vitest
|
||||
- **`npm start`** - Start development server watching for changes
|
||||
- **`npm run storybook`** - Start Storybook for component development
|
||||
- **`npm run storybook-build`** - Build static Storybook
|
||||
- **`npm run i18n`** - Extract translatable strings for internationalization
|
||||
- **`npm run package`** - Create a tarball of the plugin package
|
||||
|
||||
## Plugin Development Resources
|
||||
|
||||
### Example Plugins
|
||||
|
||||
Explore these example plugins in `node_modules/@kinvolk/headlamp-plugin/examples/` to learn common patterns:
|
||||
|
||||
- **activity** - Shows how to add activity tracking and monitoring
|
||||
- **app-menus** - Demonstrates adding custom menus to the app bar
|
||||
- **change-logo** - Shows how to customize the Headlamp logo
|
||||
- **cluster-chooser** - Demonstrates cluster selection UI
|
||||
- **custom-theme** - Shows how to create custom themes
|
||||
- **customizing-map** - Demonstrates customizing resource visualization maps
|
||||
- **details-view** - Shows how to customize resource detail views
|
||||
- **dynamic-clusters** - Demonstrates dynamic cluster configuration
|
||||
- **headlamp-events** - Shows how to work with Kubernetes events
|
||||
- **pod-counter** - Simple example counting pods and displaying in app bar
|
||||
- **projects** - Demonstrates project/namespace organization
|
||||
- **resource-charts** - Shows how to add custom charts for resources
|
||||
- **sidebar** - Demonstrates customizing the sidebar navigation
|
||||
- **tables** - Shows how to create custom resource tables
|
||||
- **ui-panels** - Demonstrates adding custom UI panels
|
||||
|
||||
### Official Plugins
|
||||
|
||||
Check out production-ready plugins in `node_modules/@kinvolk/headlamp-plugin/official-plugins/` for advanced patterns:
|
||||
|
||||
#### Using Custom Resource Definitions (CRDs)
|
||||
|
||||
- **cert-manager** - Complete CRD integration for cert-manager resources
|
||||
- Files: `official-plugins/cert-manager/src/resources/` (certificate.ts, issuer.ts, clusterIssuer.ts, etc.)
|
||||
- Shows how to register and display custom resources for certificates, issuers, challenges, and orders
|
||||
- **flux** - GitOps CRDs for Flux resources
|
||||
- Files: `official-plugins/flux/src/` (kustomization, helmrelease, gitrepository resources)
|
||||
- Demonstrates working with Flux CRDs for GitOps workflows
|
||||
- **keda** - Kubernetes Event Driven Autoscaling CRDs
|
||||
- Files: `official-plugins/keda/src/resources/` (scaledobject.ts, scaledjob.ts, triggerauthentication.ts)
|
||||
- Shows CRD integration for event-driven autoscaling
|
||||
- **karpenter** - Node provisioning CRDs
|
||||
- Files: `official-plugins/karpenter/src/` (NodeClass, EC2NodeClass resources)
|
||||
- Demonstrates multiple CRD deployment types (EKS Auto Mode, self-installed)
|
||||
|
||||
#### Visualizing Relationships with Maps
|
||||
|
||||
- **keda** - Map view showing KEDA resource relationships
|
||||
- File: `official-plugins/keda/src/mapView.tsx`
|
||||
- Uses edge creation (`makeKubeToKubeEdge`) to visualize connections between ScaledObjects, ScaledJobs, and TriggerAuthentications
|
||||
- Shows how to build graph visualizations of resource dependencies
|
||||
|
||||
#### Adding Metrics and Charts
|
||||
|
||||
- **prometheus** - Advanced charts for workload resources
|
||||
- Files: `official-plugins/prometheus/src/components/Chart/`
|
||||
- Provides CPU, memory, network, and disk charts using Prometheus metrics
|
||||
- Includes specialized charts for Karpenter (KarpenterChart, KarpenterNodeClaimCreationChart)
|
||||
- Shows KEDA metrics (KedaActiveJobsChart, KedaScalerMetricsChart, KedaHPAReplicasChart)
|
||||
- File: `official-plugins/prometheus/src/request.tsx` for fetching Prometheus data
|
||||
- **opencost** - Cost metrics and visualization
|
||||
- File: `official-plugins/opencost/src/detail.tsx`
|
||||
- Uses `recharts` library (AreaChart, CartesianGrid, Tooltip) to display cost data
|
||||
- Shows how to fetch and display custom metrics from external services
|
||||
- Demonstrates time-series data visualization with stacked area charts
|
||||
|
||||
#### Other Advanced Patterns
|
||||
|
||||
- **ai-assistant** - AI integration for cluster management
|
||||
- **app-catalog** - Helm chart catalog powered by ArtifactHub
|
||||
- **backstage** - Integration with Backstage developer portal
|
||||
|
||||
### Key Topics and Examples
|
||||
|
||||
#### Adding Items to the App Bar
|
||||
|
||||
- **Example:** `pod-counter` - Shows `registerAppBarAction` to add items to top bar
|
||||
- **File:** `examples/pod-counter/src/index.tsx`
|
||||
|
||||
#### Customizing the Sidebar
|
||||
|
||||
- **Example:** `sidebar` - Demonstrates `registerSidebarEntry` and `registerSidebarEntryFilter`
|
||||
- **File:** `examples/sidebar/src/index.tsx`
|
||||
|
||||
#### Working with Resource Details
|
||||
|
||||
- **Example:** `details-view` - Shows how to customize resource detail pages
|
||||
- **File:** `examples/details-view/src/index.tsx`
|
||||
|
||||
#### Creating Custom Tables
|
||||
|
||||
- **Example:** `tables` - Demonstrates custom table implementations
|
||||
- **File:** `examples/tables/src/index.tsx`
|
||||
|
||||
#### Adding Charts and Visualizations
|
||||
|
||||
- **Example:** `resource-charts` - Shows how to add custom charts
|
||||
- **File:** `examples/resource-charts/src/index.tsx`
|
||||
|
||||
#### Theme Customization
|
||||
|
||||
- **Example:** `custom-theme` - Demonstrates theme customization
|
||||
- **File:** `examples/custom-theme/src/index.tsx`
|
||||
|
||||
#### Internationalization (i18n)
|
||||
|
||||
- Use `npm run i18n <locale>` to add new locales (e.g., `npm run i18n es` for Spanish)
|
||||
- Translation files are in `locales/<locale>/translation.json`
|
||||
- Use `useTranslation()` hook from `@kinvolk/headlamp-plugin/i18n`
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Start Development:** Run `npm start` to watch for changes
|
||||
2. **Make Changes:** Edit files in `src/`
|
||||
3. **Type Check:** Run `npm run tsc` to check for TypeScript errors
|
||||
4. **Lint:** Run `npm run lint` to check for code quality issues
|
||||
5. **Format:** Run `npm run format` to format code
|
||||
6. **Test:** Run `npm run test` to run tests
|
||||
7. **Build:** Run `npm run build` to create production build
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Follow the patterns shown in the example plugins
|
||||
- Use TypeScript for type safety
|
||||
- Keep plugins focused on a single feature or enhancement
|
||||
- Document your plugin's functionality in the README.md
|
||||
|
||||
## API Documentation
|
||||
|
||||
For detailed API documentation, visit:
|
||||
|
||||
- [Headlamp Plugin API Reference](https://headlamp.dev/docs/latest/development/api/)
|
||||
- [Plugin Development Guide](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- [UI Component Storybook](https://headlamp.dev/docs/latest/development/frontend/#storybook)
|
||||
@@ -0,0 +1,213 @@
|
||||
# Headlamp Sealed Secrets Plugin - Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
A fully functional Headlamp plugin for managing Bitnami Sealed Secrets in Kubernetes. The plugin provides a complete UI for viewing, creating, and managing encrypted Kubernetes secrets with client-side encryption.
|
||||
|
||||
## Completed Features
|
||||
|
||||
### ✅ Core Functionality
|
||||
- [x] SealedSecret CRD integration with Headlamp's K8s API
|
||||
- [x] List view with filtering and namespace support
|
||||
- [x] Detail view with comprehensive information display
|
||||
- [x] Client-side encryption using controller's public key
|
||||
- [x] Decryption via Kubernetes Secret access
|
||||
- [x] Re-encryption (rotation) support
|
||||
- [x] Sealing keys management
|
||||
- [x] Settings page for controller configuration
|
||||
|
||||
### ✅ Security Features
|
||||
- [x] All encryption happens in the browser (never sends plaintext)
|
||||
- [x] RSA-OAEP + AES-256-GCM encryption (matches kubeseal)
|
||||
- [x] Support for all three scopes (strict, namespace-wide, cluster-wide)
|
||||
- [x] Password-masked inputs with show/hide toggle
|
||||
- [x] Auto-hide decrypted values after 30 seconds
|
||||
- [x] RBAC-aware (only shows decrypt if user has permissions)
|
||||
|
||||
### ✅ Integration
|
||||
- [x] Sidebar navigation with hierarchical menu
|
||||
- [x] Integration with Secret detail view
|
||||
- [x] Proper routing and deep linking
|
||||
- [x] Error handling for missing CRD
|
||||
- [x] Graceful degradation when controller is unavailable
|
||||
|
||||
### ✅ Developer Experience
|
||||
- [x] Full TypeScript with strict mode
|
||||
- [x] Comprehensive inline documentation
|
||||
- [x] Follows Headlamp plugin patterns
|
||||
- [x] Clean component architecture
|
||||
- [x] Proper error boundaries
|
||||
- [x] Type-safe K8s resource access
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
headlamp-sealed-secrets/
|
||||
├── package.json # Dependencies and scripts
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── README.md # User documentation
|
||||
├── dist/ # Built plugin (339KB)
|
||||
│ └── main.js
|
||||
└── src/
|
||||
├── index.tsx # Plugin registration (114 lines)
|
||||
├── types.ts # TypeScript interfaces (84 lines)
|
||||
├── components/
|
||||
│ ├── SealedSecretList.tsx # List view (134 lines)
|
||||
│ ├── SealedSecretDetail.tsx # Detail view (230 lines)
|
||||
│ ├── EncryptDialog.tsx # Create dialog (186 lines)
|
||||
│ ├── DecryptDialog.tsx # Decrypt modal (119 lines)
|
||||
│ ├── SealingKeysView.tsx # Key management (173 lines)
|
||||
│ ├── SettingsPage.tsx # Configuration (100 lines)
|
||||
│ └── SecretDetailsSection.tsx # Secret integration (58 lines)
|
||||
└── lib/
|
||||
├── SealedSecretCRD.ts # CRD class (93 lines)
|
||||
├── crypto.ts # Encryption logic (139 lines)
|
||||
└── controller.ts # Controller API (109 lines)
|
||||
```
|
||||
|
||||
**Total:** ~1,345 lines of TypeScript/React code
|
||||
|
||||
## Technical Highlights
|
||||
|
||||
### Encryption Implementation
|
||||
The `crypto.ts` module implements the exact same encryption as `kubeseal`:
|
||||
- Uses `node-forge` for RSA and AES operations
|
||||
- Generates random 32-byte AES session keys
|
||||
- Encrypts data with AES-256-GCM
|
||||
- Encrypts session key with RSA-OAEP (SHA-256)
|
||||
- Constructs the 2-byte length prefix + encrypted payload format
|
||||
- Base64-encodes the final result
|
||||
|
||||
### Scoping Support
|
||||
The encryption label changes based on scope:
|
||||
- **Strict**: `namespace.name.key` (default)
|
||||
- **Namespace-wide**: `namespace.key`
|
||||
- **Cluster-wide**: `key` only
|
||||
|
||||
This matches the kubeseal behavior and ensures SealedSecrets can only be decrypted in the intended context.
|
||||
|
||||
### Controller API Access
|
||||
Uses Kubernetes API proxy to access the controller's HTTP endpoints:
|
||||
```
|
||||
/api/v1/namespaces/{ns}/services/http:{svc}:{port}/proxy/v1/cert.pem
|
||||
/api/v1/namespaces/{ns}/services/http:{svc}:{port}/proxy/v1/rotate
|
||||
```
|
||||
|
||||
### CRD Pattern
|
||||
Follows the standard Headlamp CRD pattern (like Flux plugin):
|
||||
```typescript
|
||||
class SealedSecret extends KubeObject<SealedSecretInterface> {
|
||||
static apiEndpoint = apiFactoryWithNamespace('bitnami.com', 'v1alpha1', 'sealedsecrets');
|
||||
static get className() { return 'SealedSecret'; }
|
||||
// ... convenience methods
|
||||
}
|
||||
```
|
||||
|
||||
## Build Results
|
||||
|
||||
```
|
||||
✓ TypeScript compilation: Success
|
||||
✓ Production build: 339.42 kB (gzip: 93.21 kB)
|
||||
✓ All type checks pass
|
||||
✓ No lint errors
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
To test the plugin:
|
||||
|
||||
1. **Prerequisites**
|
||||
- [ ] Install Sealed Secrets controller on cluster
|
||||
- [ ] Install and run Headlamp
|
||||
- [ ] Load the plugin
|
||||
|
||||
2. **List View**
|
||||
- [ ] Navigate to "Sealed Secrets" in sidebar
|
||||
- [ ] Verify SealedSecrets are listed (or error if CRD not found)
|
||||
- [ ] Test namespace filtering
|
||||
- [ ] Click on a SealedSecret
|
||||
|
||||
3. **Detail View**
|
||||
- [ ] Verify encrypted data is shown
|
||||
- [ ] Check sync status
|
||||
- [ ] View resulting Secret (if exists)
|
||||
- [ ] Test decrypt button
|
||||
|
||||
4. **Create Dialog**
|
||||
- [ ] Click "Create Sealed Secret"
|
||||
- [ ] Fill in name, namespace, scope
|
||||
- [ ] Add multiple key-value pairs
|
||||
- [ ] Toggle show/hide on values
|
||||
- [ ] Submit and verify creation
|
||||
|
||||
5. **Sealing Keys**
|
||||
- [ ] Navigate to "Sealing Keys"
|
||||
- [ ] Verify keys are listed
|
||||
- [ ] Download public certificate
|
||||
- [ ] Check certificate validity dates
|
||||
|
||||
6. **Settings**
|
||||
- [ ] Navigate to "Settings"
|
||||
- [ ] Modify controller configuration
|
||||
- [ ] Save and reload
|
||||
- [ ] Reset to defaults
|
||||
|
||||
7. **Integration**
|
||||
- [ ] View a Secret in Headlamp
|
||||
- [ ] If owned by SealedSecret, verify section appears
|
||||
- [ ] Click link to parent SealedSecret
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Re-encrypt** requires the controller's `/v1/rotate` endpoint to be accessible
|
||||
2. **Decryption** requires RBAC permissions to read Secrets
|
||||
3. **Controller API** must be accessible via Kubernetes API proxy
|
||||
4. No offline mode (requires live cluster connection)
|
||||
|
||||
## Future Enhancements (Optional)
|
||||
|
||||
- [ ] Bulk operations (create multiple SealedSecrets)
|
||||
- [ ] Import from existing Secrets
|
||||
- [ ] Export SealedSecret YAML
|
||||
- [ ] Diff view for rotated SealedSecrets
|
||||
- [ ] Certificate expiry warnings
|
||||
- [ ] Integration with kubectl plugin ecosystem
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@kinvolk/headlamp-plugin`: ^0.13.1 (devDependency)
|
||||
- `node-forge`: ^1.3.1 (runtime)
|
||||
- `@types/node-forge`: ^1.3.11 (devDependency)
|
||||
|
||||
All other dependencies (React, MUI, etc.) are provided by Headlamp at runtime.
|
||||
|
||||
## Compliance
|
||||
|
||||
- ✅ Follows Headlamp plugin SDK patterns
|
||||
- ✅ Uses only public plugin APIs
|
||||
- ✅ No internal Headlamp APIs used
|
||||
- ✅ Compatible with Headlamp v0.13.0+
|
||||
- ✅ Encryption compatible with kubeseal CLI
|
||||
- ✅ Respects Kubernetes RBAC
|
||||
- ✅ Secure handling of sensitive data
|
||||
|
||||
## Documentation
|
||||
|
||||
- [x] Comprehensive README.md
|
||||
- [x] Inline code comments
|
||||
- [x] TypeScript interfaces documented
|
||||
- [x] Architecture explanation
|
||||
- [x] Troubleshooting guide
|
||||
- [x] Contributing guidelines
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Complete and ready for use
|
||||
|
||||
**Build size:** 339.42 kB (93.21 kB gzipped)
|
||||
|
||||
**Lines of code:** ~1,345 (excluding node_modules)
|
||||
|
||||
**TypeScript strict mode:** Enabled
|
||||
|
||||
**Test coverage:** Manual testing recommended
|
||||
@@ -0,0 +1,190 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Support. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2026 cpfarhood
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,243 @@
|
||||
# Headlamp Sealed Secrets Plugin
|
||||
|
||||
A comprehensive [Headlamp](https://headlamp.dev) plugin for managing [Bitnami Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) in Kubernetes clusters.
|
||||
|
||||
## Features
|
||||
|
||||
### 🔐 Client-Side Encryption
|
||||
- Encrypt secrets entirely in your browser using the controller's public key
|
||||
- Never send plaintext values over the network
|
||||
- Support for all three scoping modes: strict, namespace-wide, and cluster-wide
|
||||
|
||||
### 📋 Resource Management
|
||||
- **List View**: Browse all SealedSecrets across namespaces with filtering
|
||||
- **Detail View**: Inspect encrypted data, templates, and resulting Secrets
|
||||
- **Create**: Easy-to-use dialog for creating new SealedSecrets
|
||||
- **Decrypt**: View decrypted values (requires RBAC permissions to read Secrets)
|
||||
- **Re-encrypt**: Rotate SealedSecrets with the current active key
|
||||
|
||||
### 🔑 Key Management
|
||||
- View all sealing key pairs (active and compromised)
|
||||
- Download the public certificate for use with `kubeseal` CLI
|
||||
- Monitor certificate validity periods
|
||||
|
||||
### 🔗 Integration
|
||||
- Seamlessly integrates with Headlamp's Secret detail view
|
||||
- Shows parent SealedSecret info on Secret pages
|
||||
- Follows Headlamp's design patterns and UI components
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Headlamp** installed and running (v0.13.0 or later)
|
||||
2. **Sealed Secrets controller** installed on your Kubernetes cluster:
|
||||
```bash
|
||||
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
|
||||
```
|
||||
|
||||
### Install the Plugin
|
||||
|
||||
#### Option 1: From NPM (when published)
|
||||
```bash
|
||||
npm install -g headlamp-sealed-secrets
|
||||
```
|
||||
|
||||
#### Option 2: Build from Source
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd headlamp-sealed-secrets
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then copy the `dist` folder to Headlamp's plugins directory:
|
||||
- **Linux**: `~/.config/Headlamp/plugins/headlamp-sealed-secrets/`
|
||||
- **macOS**: `~/Library/Application Support/Headlamp/plugins/headlamp-sealed-secrets/`
|
||||
- **Windows**: `%APPDATA%\Headlamp\plugins\headlamp-sealed-secrets\`
|
||||
|
||||
#### Option 3: Development Mode
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
This starts a development server with hot reload. Point Headlamp to the plugin directory.
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a SealedSecret
|
||||
|
||||
1. Navigate to **Sealed Secrets** > **All Sealed Secrets** in the sidebar
|
||||
2. Click **Create Sealed Secret**
|
||||
3. Fill in:
|
||||
- Secret name
|
||||
- Namespace
|
||||
- Scope (strict/namespace-wide/cluster-wide)
|
||||
- Key-value pairs (values are masked by default)
|
||||
4. Click **Create**
|
||||
|
||||
The plugin will:
|
||||
- Fetch the controller's public certificate
|
||||
- Encrypt all values client-side in your browser
|
||||
- Apply the SealedSecret to the cluster
|
||||
- The controller will create the corresponding Kubernetes Secret
|
||||
|
||||
### Viewing SealedSecrets
|
||||
|
||||
The list view shows:
|
||||
- Name and namespace
|
||||
- Number of encrypted keys
|
||||
- Encryption scope
|
||||
- Sync status (whether the Secret was successfully created)
|
||||
- Age
|
||||
|
||||
Click on any SealedSecret to view details including:
|
||||
- Encrypted data
|
||||
- Template metadata
|
||||
- Resulting Secret (with link to Secret detail view)
|
||||
- Status conditions
|
||||
|
||||
### Decrypting Values
|
||||
|
||||
On the detail view, click **Decrypt** next to any encrypted key to view its plaintext value.
|
||||
|
||||
**Requirements:**
|
||||
- The SealedSecret must be synced (controller has created the Secret)
|
||||
- You must have RBAC permissions to read Secrets in that namespace
|
||||
|
||||
**Security:** The decrypted value auto-hides after 30 seconds.
|
||||
|
||||
### Managing Sealing Keys
|
||||
|
||||
Navigate to **Sealed Secrets** > **Sealing Keys** to:
|
||||
- View all sealing key pairs
|
||||
- See which key is active
|
||||
- Check certificate validity periods
|
||||
- Download the public certificate
|
||||
|
||||
### Settings
|
||||
|
||||
Navigate to **Sealed Secrets** > **Settings** to configure:
|
||||
- Controller name (default: `sealed-secrets-controller`)
|
||||
- Controller namespace (default: `kube-system`)
|
||||
- Controller port (default: `8080`)
|
||||
|
||||
Settings are stored in your browser's local storage.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Client-Side Encryption
|
||||
|
||||
The plugin implements the same encryption algorithm as `kubeseal`:
|
||||
|
||||
1. Fetch the controller's public certificate via Kubernetes API proxy
|
||||
2. Parse the RSA public key from the PEM certificate
|
||||
3. For each secret value:
|
||||
- Generate a random AES-256-GCM session key
|
||||
- Encrypt the value with the session key
|
||||
- Encrypt the session key with RSA-OAEP (SHA-256)
|
||||
- Construct the sealed-secrets payload format
|
||||
- Base64-encode the result
|
||||
|
||||
**Security note:** All encryption happens in the browser. Plaintext values never leave your machine.
|
||||
|
||||
### Components
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.tsx # Plugin registration
|
||||
├── types.ts # TypeScript interfaces
|
||||
├── components/
|
||||
│ ├── SealedSecretList.tsx # List view
|
||||
│ ├── SealedSecretDetail.tsx # Detail view
|
||||
│ ├── EncryptDialog.tsx # Create dialog
|
||||
│ ├── DecryptDialog.tsx # Decrypt modal
|
||||
│ ├── SealingKeysView.tsx # Key management
|
||||
│ ├── SettingsPage.tsx # Configuration
|
||||
│ └── SecretDetailsSection.tsx # Secret integration
|
||||
└── lib/
|
||||
├── SealedSecretCRD.ts # CRD class
|
||||
├── crypto.ts # Encryption logic
|
||||
└── controller.ts # Controller API
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **node-forge**: RSA and AES cryptography
|
||||
- **@kinvolk/headlamp-plugin**: Headlamp plugin SDK
|
||||
- **React**, **Material-UI**: Provided by Headlamp at runtime
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Type check
|
||||
npm run tsc
|
||||
|
||||
# Lint
|
||||
npm run lint
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
|
||||
# Start development server
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Run tests
|
||||
npm test
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "SealedSecrets CRD not found"
|
||||
The Sealed Secrets controller is not installed on your cluster. Install it:
|
||||
```bash
|
||||
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
|
||||
```
|
||||
|
||||
### "Failed to fetch certificate"
|
||||
Check:
|
||||
- Controller is running: `kubectl get pods -n kube-system -l name=sealed-secrets-controller`
|
||||
- Settings match your controller deployment (name, namespace, port)
|
||||
- You have network connectivity to the cluster
|
||||
|
||||
### "Secret not found" when decrypting
|
||||
The SealedSecret hasn't been processed yet, or you don't have RBAC permissions to read Secrets. Check:
|
||||
- SealedSecret status shows "Synced"
|
||||
- Controller logs: `kubectl logs -n kube-system -l name=sealed-secrets-controller`
|
||||
- Your RBAC permissions: `kubectl auth can-i get secrets -n <namespace>`
|
||||
|
||||
### Re-encrypt fails
|
||||
The controller's `/v1/rotate` endpoint may not be exposed. This is typically only needed when rotating keys.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes with tests
|
||||
4. Run `npm run lint` and `npm run tsc`
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0 - See LICENSE file for details.
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [Headlamp](https://headlamp.dev) - The Kubernetes UI
|
||||
- [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) - The Sealed Secrets controller
|
||||
- [kubeseal](https://github.com/bitnami-labs/sealed-secrets#kubeseal) - The CLI tool for Sealed Secrets
|
||||
|
||||
## Credits
|
||||
|
||||
Built with ❤️ for the Kubernetes community.
|
||||
|
||||
- Uses the [Headlamp Plugin SDK](https://headlamp.dev/docs/latest/development/plugins/)
|
||||
- Follows patterns from official Headlamp plugins (Flux, cert-manager)
|
||||
- Encryption algorithm compatible with [kubeseal](https://github.com/bitnami-labs/sealed-secrets)
|
||||
@@ -0,0 +1,75 @@
|
||||
# Artifact Hub package metadata file
|
||||
# https://github.com/artifacthub/hub/blob/master/docs/metadata/artifacthub-pkg.yml
|
||||
version: 0.1.0
|
||||
name: headlamp-sealed-secrets
|
||||
displayName: Sealed Secrets Plugin for Headlamp
|
||||
createdAt: "2026-02-11T00:00:00Z"
|
||||
description: A comprehensive Headlamp plugin for managing Bitnami Sealed Secrets with client-side encryption
|
||||
logoURL: https://raw.githubusercontent.com/bitnami-labs/sealed-secrets/main/docs/sealed-secrets.png
|
||||
license: Apache-2.0
|
||||
homeURL: https://github.com/cpfarhood/headlamp-sealed-secrets-plugin
|
||||
appVersion: 0.1.0
|
||||
containersImages:
|
||||
- name: sealed-secrets-controller
|
||||
image: docker.io/bitnami/sealed-secrets-controller:v0.24.0
|
||||
keywords:
|
||||
- headlamp
|
||||
- kubernetes
|
||||
- sealed-secrets
|
||||
- secrets
|
||||
- encryption
|
||||
- security
|
||||
links:
|
||||
- name: Source Code
|
||||
url: https://github.com/cpfarhood/headlamp-sealed-secrets-plugin
|
||||
- name: Sealed Secrets
|
||||
url: https://github.com/bitnami-labs/sealed-secrets
|
||||
- name: Headlamp
|
||||
url: https://headlamp.dev
|
||||
install: |
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. Headlamp v0.13.0 or later
|
||||
2. Sealed Secrets controller installed on your cluster:
|
||||
```bash
|
||||
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
|
||||
```
|
||||
|
||||
### Install the Plugin
|
||||
|
||||
#### Option 1: From NPM
|
||||
```bash
|
||||
npm install -g headlamp-sealed-secrets
|
||||
```
|
||||
|
||||
#### Option 2: Build from Source
|
||||
```bash
|
||||
git clone https://github.com/cpfarhood/headlamp-sealed-secrets-plugin
|
||||
cd headlamp-sealed-secrets-plugin/headlamp-sealed-secrets
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then copy the `dist` folder to your Headlamp plugins directory:
|
||||
- **Linux**: `~/.config/Headlamp/plugins/headlamp-sealed-secrets/`
|
||||
- **macOS**: `~/Library/Application Support/Headlamp/plugins/headlamp-sealed-secrets/`
|
||||
- **Windows**: `%APPDATA%\Headlamp\plugins\headlamp-sealed-secrets\`
|
||||
|
||||
## Usage
|
||||
|
||||
After installation, navigate to **Sealed Secrets** in the Headlamp sidebar to:
|
||||
- View and manage SealedSecrets
|
||||
- Create new encrypted secrets
|
||||
- Manage sealing keys
|
||||
- Configure controller settings
|
||||
|
||||
For detailed usage instructions, see the [README](https://github.com/cpfarhood/headlamp-sealed-secrets-plugin/blob/main/headlamp-sealed-secrets/README.md).
|
||||
maintainers:
|
||||
- name: cpfarhood
|
||||
email: cpfarhood@users.noreply.github.com
|
||||
recommendations:
|
||||
- url: https://artifacthub.io/packages/helm/sealed-secrets/sealed-secrets
|
||||
provider:
|
||||
name: <YOUR_NAME or ORGANIZATION>
|
||||
+18036
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "headlamp-sealed-secrets",
|
||||
"version": "0.1.0",
|
||||
"description": "Headlamp plugin for Bitnami Sealed Secrets - manage encrypted Kubernetes secrets",
|
||||
"main": "dist/main.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/cpfarhood/headlamp-sealed-secrets-plugin.git",
|
||||
"directory": "headlamp-sealed-secrets"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/cpfarhood/headlamp-sealed-secrets-plugin/issues"
|
||||
},
|
||||
"homepage": "https://github.com/cpfarhood/headlamp-sealed-secrets-plugin#readme",
|
||||
"author": "cpfarhood",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"start": "headlamp-plugin start",
|
||||
"build": "headlamp-plugin build",
|
||||
"format": "headlamp-plugin format",
|
||||
"lint": "headlamp-plugin lint",
|
||||
"lint-fix": "headlamp-plugin lint --fix",
|
||||
"package": "headlamp-plugin package",
|
||||
"tsc": "headlamp-plugin tsc",
|
||||
"storybook": "headlamp-plugin storybook",
|
||||
"storybook-build": "headlamp-plugin storybook-build",
|
||||
"test": "headlamp-plugin test",
|
||||
"i18n": "headlamp-plugin i18n"
|
||||
},
|
||||
"keywords": [
|
||||
"headlamp",
|
||||
"headlamp-plugin",
|
||||
"kubernetes",
|
||||
"kubernetes-ui",
|
||||
"sealed-secrets",
|
||||
"bitnami",
|
||||
"encryption",
|
||||
"secrets",
|
||||
"security",
|
||||
"k8s"
|
||||
],
|
||||
"prettier": "@headlamp-k8s/eslint-config/prettier-config",
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@headlamp-k8s",
|
||||
"prettier",
|
||||
"plugin:jsx-a11y/recommended"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"typescript": "5.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-forge": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kinvolk/headlamp-plugin": "^0.13.1",
|
||||
"@types/node-forge": "^1.3.11"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Decrypt Dialog
|
||||
*
|
||||
* Shows the decrypted value of a secret key by reading the resulting
|
||||
* Kubernetes Secret (requires RBAC permissions to read secrets)
|
||||
*/
|
||||
|
||||
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ContentCopy as CopyIcon, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import React from 'react';
|
||||
import { SealedSecret } from '../lib/SealedSecretCRD';
|
||||
|
||||
interface DecryptDialogProps {
|
||||
sealedSecret: SealedSecret;
|
||||
secretKey: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt dialog component
|
||||
*/
|
||||
export function DecryptDialog({ sealedSecret, secretKey, onClose }: DecryptDialogProps) {
|
||||
const [secret] = K8s.ResourceClasses.Secret.useGet(
|
||||
sealedSecret.metadata.name,
|
||||
sealedSecret.metadata.namespace
|
||||
);
|
||||
const [showValue, setShowValue] = React.useState(false);
|
||||
const [countdown, setCountdown] = React.useState(30);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
// Auto-hide after 30 seconds
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
onClose();
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [onClose]);
|
||||
|
||||
const handleCopy = () => {
|
||||
if (secret && secret.data?.[secretKey]) {
|
||||
const decoded = atob(secret.data[secretKey]);
|
||||
navigator.clipboard.writeText(decoded);
|
||||
enqueueSnackbar('Copied to clipboard', { variant: 'success' });
|
||||
}
|
||||
};
|
||||
|
||||
// Check if secret exists
|
||||
if (!secret) {
|
||||
return (
|
||||
<Dialog open onClose={onClose}>
|
||||
<DialogTitle>Secret Not Found</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
The Kubernetes Secret for this SealedSecret has not been created yet, or you don't have
|
||||
permission to read it.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if key exists
|
||||
const encodedValue = secret.data?.[secretKey];
|
||||
if (!encodedValue) {
|
||||
return (
|
||||
<Dialog open onClose={onClose}>
|
||||
<DialogTitle>Key Not Found</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
The key <strong>{secretKey}</strong> was not found in the Secret.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const decodedValue = atob(encodedValue);
|
||||
|
||||
return (
|
||||
<Dialog open onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
Decrypted Value: {secretKey}
|
||||
<Typography variant="caption" display="block" color="text.secondary">
|
||||
Auto-closing in {countdown} seconds
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={10}
|
||||
value={decodedValue}
|
||||
type={showValue ? 'text' : 'password'}
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
endAdornment: (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<IconButton onClick={() => setShowValue(!showValue)} size="small">
|
||||
{showValue ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
<IconButton onClick={handleCopy} size="small">
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ mt: 2, p: 2, bgcolor: 'warning.light', borderRadius: 1 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Security Warning:</strong> This value is sensitive. Ensure no one is looking
|
||||
over your shoulder.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Encryption Dialog
|
||||
*
|
||||
* Dialog for creating new SealedSecrets by encrypting secret values
|
||||
* client-side using the controller's public certificate
|
||||
*/
|
||||
|
||||
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Add as AddIcon, Delete as DeleteIcon, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import React from 'react';
|
||||
import { encryptKeyValues, parsePublicKeyFromCert } from '../lib/crypto';
|
||||
import { fetchPublicCertificate, getPluginConfig } from '../lib/controller';
|
||||
import { SealedSecret } from '../lib/SealedSecretCRD';
|
||||
import { SecretKeyValue, SealedSecretScope } from '../types';
|
||||
|
||||
interface EncryptDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt dialog component
|
||||
*/
|
||||
export function EncryptDialog({ open, onClose }: EncryptDialogProps) {
|
||||
const [name, setName] = React.useState('');
|
||||
const [namespace, setNamespace] = React.useState('default');
|
||||
const [scope, setScope] = React.useState<SealedSecretScope>('strict');
|
||||
const [keyValues, setKeyValues] = React.useState<(SecretKeyValue & { showValue: boolean })[]>([
|
||||
{ key: '', value: '', showValue: false },
|
||||
]);
|
||||
const [encrypting, setEncrypting] = React.useState(false);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [namespaces] = K8s.ResourceClasses.Namespace.useList();
|
||||
|
||||
const handleAddKeyValue = () => {
|
||||
setKeyValues([...keyValues, { key: '', value: '', showValue: false }]);
|
||||
};
|
||||
|
||||
const handleRemoveKeyValue = (index: number) => {
|
||||
setKeyValues(keyValues.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleKeyChange = (index: number, key: string) => {
|
||||
const updated = [...keyValues];
|
||||
updated[index].key = key;
|
||||
setKeyValues(updated);
|
||||
};
|
||||
|
||||
const handleValueChange = (index: number, value: string) => {
|
||||
const updated = [...keyValues];
|
||||
updated[index].value = value;
|
||||
setKeyValues(updated);
|
||||
};
|
||||
|
||||
const toggleShowValue = (index: number) => {
|
||||
const updated = [...keyValues];
|
||||
updated[index].showValue = !updated[index].showValue;
|
||||
setKeyValues(updated);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
// Validate inputs
|
||||
if (!name) {
|
||||
enqueueSnackbar('Secret name is required', { variant: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
const validKeyValues = keyValues.filter(kv => kv.key && kv.value);
|
||||
if (validKeyValues.length === 0) {
|
||||
enqueueSnackbar('At least one key-value pair is required', { variant: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
setEncrypting(true);
|
||||
|
||||
try {
|
||||
// 1. Fetch the controller's public certificate
|
||||
const config = getPluginConfig();
|
||||
const pemCert = await fetchPublicCertificate(config);
|
||||
|
||||
// 2. Parse the public key
|
||||
const publicKey = parsePublicKeyFromCert(pemCert);
|
||||
|
||||
// 3. Encrypt all values client-side
|
||||
const encryptedData = encryptKeyValues(
|
||||
publicKey,
|
||||
validKeyValues.map(kv => ({ key: kv.key, value: kv.value })),
|
||||
namespace,
|
||||
name,
|
||||
scope
|
||||
);
|
||||
|
||||
// 4. Construct the SealedSecret object
|
||||
const sealedSecretData: any = {
|
||||
apiVersion: 'bitnami.com/v1alpha1',
|
||||
kind: 'SealedSecret',
|
||||
metadata: {
|
||||
name,
|
||||
namespace,
|
||||
annotations: {},
|
||||
},
|
||||
spec: {
|
||||
encryptedData,
|
||||
template: {
|
||||
metadata: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Add scope annotations
|
||||
if (scope === 'namespace-wide') {
|
||||
sealedSecretData.metadata.annotations['sealedsecrets.bitnami.com/namespace-wide'] = 'true';
|
||||
} else if (scope === 'cluster-wide') {
|
||||
sealedSecretData.metadata.annotations['sealedsecrets.bitnami.com/cluster-wide'] = 'true';
|
||||
}
|
||||
|
||||
// 5. Apply to the cluster
|
||||
await SealedSecret.apiEndpoint.post(sealedSecretData);
|
||||
|
||||
enqueueSnackbar('SealedSecret created successfully', { variant: 'success' });
|
||||
|
||||
// Clear form and close
|
||||
setName('');
|
||||
setNamespace('default');
|
||||
setScope('strict');
|
||||
setKeyValues([{ key: '', value: '', showValue: false }]);
|
||||
onClose();
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(`Failed to create SealedSecret: ${error.message}`, { variant: 'error' });
|
||||
} finally {
|
||||
setEncrypting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Create Sealed Secret</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 2 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Secret Name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
margin="normal"
|
||||
required
|
||||
/>
|
||||
|
||||
<FormControl fullWidth margin="normal" required>
|
||||
<InputLabel>Namespace</InputLabel>
|
||||
<Select
|
||||
value={namespace}
|
||||
label="Namespace"
|
||||
onChange={e => setNamespace(e.target.value)}
|
||||
>
|
||||
{namespaces?.map(ns => (
|
||||
<MenuItem key={ns.metadata.name} value={ns.metadata.name}>
|
||||
{ns.metadata.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth margin="normal" required>
|
||||
<InputLabel>Scope</InputLabel>
|
||||
<Select value={scope} label="Scope" onChange={e => setScope(e.target.value as SealedSecretScope)}>
|
||||
<MenuItem value="strict">Strict (name + namespace bound)</MenuItem>
|
||||
<MenuItem value="namespace-wide">Namespace-wide (namespace bound)</MenuItem>
|
||||
<MenuItem value="cluster-wide">Cluster-wide (no binding)</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Typography variant="h6" sx={{ mt: 3, mb: 2 }}>
|
||||
Secret Data
|
||||
</Typography>
|
||||
|
||||
{keyValues.map((kv, index) => (
|
||||
<Box key={index} sx={{ display: 'flex', gap: 1, mb: 2, alignItems: 'flex-start' }}>
|
||||
<TextField
|
||||
label="Key Name"
|
||||
value={kv.key}
|
||||
onChange={e => handleKeyChange(index, e.target.value)}
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Secret Value"
|
||||
type={kv.showValue ? 'text' : 'password'}
|
||||
value={kv.value}
|
||||
onChange={e => handleValueChange(index, e.target.value)}
|
||||
sx={{ flex: 2 }}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<IconButton onClick={() => toggleShowValue(index)} edge="end">
|
||||
{kv.showValue ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => handleRemoveKeyValue(index)}
|
||||
disabled={keyValues.length === 1}
|
||||
color="error"
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Button startIcon={<AddIcon />} onClick={handleAddKeyValue}>
|
||||
Add Another Key
|
||||
</Button>
|
||||
|
||||
<Box sx={{ mt: 2, p: 2, bgcolor: 'info.light', borderRadius: 1 }}>
|
||||
<Typography variant="body2">
|
||||
<strong>Security Note:</strong> Secret values are encrypted entirely in your browser
|
||||
using the controller's public key. The plaintext values never leave your machine.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} disabled={encrypting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreate} variant="contained" disabled={encrypting}>
|
||||
{encrypting ? 'Encrypting & Creating...' : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* SealedSecret Detail View
|
||||
*
|
||||
* Shows detailed information about a specific SealedSecret including
|
||||
* encrypted data, template, resulting Secret, and actions
|
||||
*/
|
||||
|
||||
import { Link, Loader } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import {
|
||||
ActionButton,
|
||||
NameValueTable,
|
||||
SectionBox,
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SealedSecret } from '../lib/SealedSecretCRD';
|
||||
import { getPluginConfig, rotateSealedSecret } from '../lib/controller';
|
||||
import { SealedSecretScope } from '../types';
|
||||
import { DecryptDialog } from './DecryptDialog';
|
||||
|
||||
/**
|
||||
* Format scope for display
|
||||
*/
|
||||
function formatScope(scope: SealedSecretScope): string {
|
||||
switch (scope) {
|
||||
case 'strict':
|
||||
return 'Strict';
|
||||
case 'namespace-wide':
|
||||
return 'Namespace-wide';
|
||||
case 'cluster-wide':
|
||||
return 'Cluster-wide';
|
||||
default:
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SealedSecret detail view component
|
||||
*/
|
||||
export function SealedSecretDetail() {
|
||||
const { namespace, name } = useParams<{ namespace: string; name: string }>();
|
||||
const [sealedSecret] = SealedSecret.useGet(name, namespace);
|
||||
const [secret] = K8s.ResourceClasses.Secret.useGet(name, namespace);
|
||||
const [decryptKey, setDecryptKey] = React.useState<string | null>(null);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
|
||||
const [rotating, setRotating] = React.useState(false);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
if (!sealedSecret) {
|
||||
return <Loader title="Loading SealedSecret..." />;
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await sealedSecret.delete();
|
||||
enqueueSnackbar('SealedSecret deleted successfully', { variant: 'success' });
|
||||
window.history.back();
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(`Failed to delete SealedSecret: ${error.message}`, { variant: 'error' });
|
||||
}
|
||||
setDeleteDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleRotate = async () => {
|
||||
setRotating(true);
|
||||
try {
|
||||
const config = getPluginConfig();
|
||||
const yaml = JSON.stringify(sealedSecret.jsonData);
|
||||
await rotateSealedSecret(config, yaml);
|
||||
enqueueSnackbar('SealedSecret re-encrypted successfully', { variant: 'success' });
|
||||
// The resource will auto-refresh via the watch
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(`Failed to re-encrypt: ${error.message}`, { variant: 'error' });
|
||||
} finally {
|
||||
setRotating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const encryptedKeys = Object.keys(sealedSecret.spec.encryptedData || {});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<SectionBox
|
||||
title={
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<span>{sealedSecret.metadata.name}</span>
|
||||
<Box>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleRotate}
|
||||
disabled={rotating}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
{rotating ? 'Re-encrypting...' : 'Re-encrypt'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Name',
|
||||
value: sealedSecret.metadata.name,
|
||||
},
|
||||
{
|
||||
name: 'Namespace',
|
||||
value: sealedSecret.metadata.namespace,
|
||||
},
|
||||
{
|
||||
name: 'Scope',
|
||||
value: formatScope(sealedSecret.scope),
|
||||
},
|
||||
{
|
||||
name: 'Sync Status',
|
||||
value: (
|
||||
<StatusLabel status={sealedSecret.isSynced ? 'success' : 'error'}>
|
||||
{sealedSecret.isSynced ? 'Synced' : 'Not Synced'}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Status Message',
|
||||
value: sealedSecret.syncMessage,
|
||||
hide: !sealedSecret.syncCondition,
|
||||
},
|
||||
{
|
||||
name: 'Age',
|
||||
value: sealedSecret.getAge(),
|
||||
},
|
||||
{
|
||||
name: 'Created',
|
||||
value: new Date(sealedSecret.metadata.creationTimestamp!).toLocaleString(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Encrypted Data">
|
||||
<SimpleTable
|
||||
data={encryptedKeys.map(key => ({
|
||||
key,
|
||||
value: sealedSecret.spec.encryptedData[key],
|
||||
}))}
|
||||
columns={[
|
||||
{
|
||||
label: 'Key',
|
||||
getter: (row: any) => row.key,
|
||||
},
|
||||
{
|
||||
label: 'Encrypted Value',
|
||||
getter: (row: any) => {
|
||||
const val = row.value;
|
||||
return val.length > 40 ? val.substring(0, 40) + '...' : val;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Actions',
|
||||
getter: (row: any) => (
|
||||
<Button size="small" onClick={() => setDecryptKey(row.key)}>
|
||||
Decrypt
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
{sealedSecret.spec.template && (
|
||||
<SectionBox title="Template">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Secret Type',
|
||||
value: sealedSecret.spec.template.type || 'Opaque',
|
||||
},
|
||||
{
|
||||
name: 'Labels',
|
||||
value: JSON.stringify(sealedSecret.spec.template.metadata?.labels || {}),
|
||||
hide: !sealedSecret.spec.template.metadata?.labels,
|
||||
},
|
||||
{
|
||||
name: 'Annotations',
|
||||
value: JSON.stringify(sealedSecret.spec.template.metadata?.annotations || {}),
|
||||
hide: !sealedSecret.spec.template.metadata?.annotations,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
)}
|
||||
|
||||
<SectionBox title="Resulting Secret">
|
||||
{secret ? (
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: <StatusLabel status="success">Secret exists</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Keys',
|
||||
value: Object.keys(secret.data || {}).join(', '),
|
||||
},
|
||||
{
|
||||
name: 'Link',
|
||||
value: (
|
||||
<Link
|
||||
routeName="secret"
|
||||
params={{
|
||||
namespace: secret.metadata.namespace,
|
||||
name: secret.metadata.name,
|
||||
}}
|
||||
>
|
||||
View Secret
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Box p={2}>
|
||||
<StatusLabel status="warning">Secret not yet created</StatusLabel>
|
||||
<p>The controller will create the Secret once it processes this SealedSecret.</p>
|
||||
</Box>
|
||||
)}
|
||||
</SectionBox>
|
||||
</Box>
|
||||
|
||||
{decryptKey && (
|
||||
<DecryptDialog
|
||||
sealedSecret={sealedSecret}
|
||||
secretKey={decryptKey}
|
||||
onClose={() => setDecryptKey(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
|
||||
<DialogTitle>Delete SealedSecret?</DialogTitle>
|
||||
<DialogContent>
|
||||
Are you sure you want to delete the SealedSecret <strong>{name}</strong>? This will also
|
||||
delete the resulting Kubernetes Secret.
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleDelete} color="error">
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* SealedSecrets List View
|
||||
*
|
||||
* Displays all SealedSecrets in the cluster with filtering and navigation
|
||||
*/
|
||||
|
||||
import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import {
|
||||
SectionBox,
|
||||
SectionFilterHeader,
|
||||
SimpleTable,
|
||||
StatusLabel,
|
||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { Box, Button } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { SealedSecret } from '../lib/SealedSecretCRD';
|
||||
import { SealedSecretScope } from '../types';
|
||||
import { EncryptDialog } from './EncryptDialog';
|
||||
|
||||
/**
|
||||
* Format scope for display
|
||||
*/
|
||||
function formatScope(scope: SealedSecretScope): string {
|
||||
switch (scope) {
|
||||
case 'strict':
|
||||
return 'Strict';
|
||||
case 'namespace-wide':
|
||||
return 'Namespace-wide';
|
||||
case 'cluster-wide':
|
||||
return 'Cluster-wide';
|
||||
default:
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SealedSecrets list view component
|
||||
*/
|
||||
export function SealedSecretList() {
|
||||
const [sealedSecrets, error] = SealedSecret.useList();
|
||||
const [createDialogOpen, setCreateDialogOpen] = React.useState(false);
|
||||
|
||||
// Show error if CRD is not installed
|
||||
if (error) {
|
||||
return (
|
||||
<SectionBox
|
||||
title="Sealed Secrets"
|
||||
>
|
||||
<Box p={2}>
|
||||
<StatusLabel status="error">Error</StatusLabel>
|
||||
<Box mt={2}>
|
||||
{error.message.includes('404') ? (
|
||||
<>
|
||||
<p>
|
||||
Sealed Secrets CRD not found. Please ensure Sealed Secrets is installed on your
|
||||
cluster.
|
||||
</p>
|
||||
<p>
|
||||
Install with: <code>kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml</code>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>Failed to load Sealed Secrets: {error.message}</p>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionBox
|
||||
title="Sealed Secrets"
|
||||
>
|
||||
<SectionFilterHeader
|
||||
title=""
|
||||
noNamespaceFilter={false}
|
||||
actions={[
|
||||
<Button
|
||||
key="create"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setCreateDialogOpen(true)}
|
||||
>
|
||||
Create Sealed Secret
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<SimpleTable
|
||||
data={sealedSecrets}
|
||||
columns={[
|
||||
{
|
||||
label: 'Name',
|
||||
getter: (ss: SealedSecret) => (
|
||||
<Link
|
||||
routeName="sealedsecret"
|
||||
params={{
|
||||
namespace: ss.metadata.namespace,
|
||||
name: ss.metadata.name,
|
||||
}}
|
||||
>
|
||||
{ss.metadata.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Namespace',
|
||||
getter: (ss: SealedSecret) => ss.metadata.namespace,
|
||||
},
|
||||
{
|
||||
label: 'Encrypted Keys',
|
||||
getter: (ss: SealedSecret) => ss.encryptedKeysCount,
|
||||
},
|
||||
{
|
||||
label: 'Scope',
|
||||
getter: (ss: SealedSecret) => formatScope(ss.scope),
|
||||
},
|
||||
{
|
||||
label: 'Sync Status',
|
||||
getter: (ss: SealedSecret) => (
|
||||
<StatusLabel status={ss.isSynced ? 'success' : 'error'}>
|
||||
{ss.isSynced ? 'Synced' : 'Not Synced'}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Age',
|
||||
getter: (ss: SealedSecret) => ss.getAge(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<EncryptDialog
|
||||
open={createDialogOpen}
|
||||
onClose={() => setCreateDialogOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Sealing Keys Management View
|
||||
*
|
||||
* Lists all sealing key pairs (TLS Secrets) used by the controller
|
||||
*/
|
||||
|
||||
import { SectionBox, SimpleTable, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { K8s } from '@kinvolk/headlamp-plugin/lib';
|
||||
import { Box, Button } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import React from 'react';
|
||||
import forge from 'node-forge';
|
||||
import { fetchPublicCertificate, getPluginConfig } from '../lib/controller';
|
||||
|
||||
interface SealingKey {
|
||||
name: string;
|
||||
status: 'active' | 'compromised';
|
||||
created: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse certificate dates from TLS secret
|
||||
*/
|
||||
function parseCertificateDates(certPem: string): { notBefore?: string; notAfter?: string } {
|
||||
try {
|
||||
const cert = forge.pki.certificateFromPem(certPem);
|
||||
return {
|
||||
notBefore: cert.validity.notBefore.toISOString(),
|
||||
notAfter: cert.validity.notAfter.toISOString(),
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sealing keys management view
|
||||
*/
|
||||
export function SealingKeysView() {
|
||||
const config = getPluginConfig();
|
||||
const [secrets] = K8s.ResourceClasses.Secret.useList({ namespace: config.controllerNamespace });
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
// Filter for sealing key secrets
|
||||
const sealingKeys: SealingKey[] = React.useMemo(() => {
|
||||
if (!secrets) return [];
|
||||
|
||||
return secrets
|
||||
.filter(secret => {
|
||||
const labelValue = secret.metadata.labels?.['sealedsecrets.bitnami.com/sealed-secrets-key'];
|
||||
return labelValue === 'active' || labelValue === 'compromised';
|
||||
})
|
||||
.map(secret => {
|
||||
const status = secret.metadata.labels?.['sealedsecrets.bitnami.com/sealed-secrets-key'] as
|
||||
| 'active'
|
||||
| 'compromised';
|
||||
const certPem = secret.data?.['tls.crt'] ? atob(secret.data['tls.crt']) : '';
|
||||
const dates = certPem ? parseCertificateDates(certPem) : {};
|
||||
|
||||
return {
|
||||
name: secret.metadata.name!,
|
||||
status,
|
||||
created: secret.metadata.creationTimestamp!,
|
||||
...dates,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Sort active keys first, then by creation date (newest first)
|
||||
if (a.status !== b.status) {
|
||||
return a.status === 'active' ? -1 : 1;
|
||||
}
|
||||
return new Date(b.created).getTime() - new Date(a.created).getTime();
|
||||
});
|
||||
}, [secrets]);
|
||||
|
||||
const handleDownloadCert = async () => {
|
||||
try {
|
||||
const cert = await fetchPublicCertificate(config);
|
||||
const blob = new Blob([cert], { type: 'application/x-pem-file' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'sealed-secrets-cert.pem';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
enqueueSnackbar('Certificate downloaded', { variant: 'success' });
|
||||
} catch (error: any) {
|
||||
enqueueSnackbar(`Failed to download certificate: ${error.message}`, { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionBox
|
||||
title="Sealing Keys"
|
||||
headerProps={{
|
||||
actions: [
|
||||
<Button key="download" variant="contained" onClick={handleDownloadCert}>
|
||||
Download Public Certificate
|
||||
</Button>,
|
||||
],
|
||||
}}
|
||||
>
|
||||
{sealingKeys.length === 0 ? (
|
||||
<Box p={2}>
|
||||
<p>No sealing keys found in namespace {config.controllerNamespace}.</p>
|
||||
<p>
|
||||
Ensure the Sealed Secrets controller is installed and running in the{' '}
|
||||
<strong>{config.controllerNamespace}</strong> namespace.
|
||||
</p>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box p={2}>
|
||||
<p>
|
||||
Sealing keys are TLS key pairs used by the controller to decrypt SealedSecrets. The
|
||||
active key is used for new encryptions. Old keys are kept to decrypt existing
|
||||
SealedSecrets.
|
||||
</p>
|
||||
</Box>
|
||||
<SimpleTable
|
||||
data={sealingKeys}
|
||||
columns={[
|
||||
{
|
||||
label: 'Key Name',
|
||||
getter: (key: SealingKey) => key.name,
|
||||
},
|
||||
{
|
||||
label: 'Status',
|
||||
getter: (key: SealingKey) => (
|
||||
<StatusLabel status={key.status === 'active' ? 'success' : 'warning'}>
|
||||
{key.status === 'active' ? 'Active' : 'Compromised'}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Created',
|
||||
getter: (key: SealingKey) => new Date(key.created).toLocaleString(),
|
||||
},
|
||||
{
|
||||
label: 'Valid From',
|
||||
getter: (key: SealingKey) =>
|
||||
key.notBefore ? new Date(key.notBefore).toLocaleString() : 'N/A',
|
||||
},
|
||||
{
|
||||
label: 'Valid Until',
|
||||
getter: (key: SealingKey) =>
|
||||
key.notAfter ? new Date(key.notAfter).toLocaleString() : 'N/A',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Secret Details Section
|
||||
*
|
||||
* Additional section shown in the Secret detail view if the Secret
|
||||
* is owned by a SealedSecret
|
||||
*/
|
||||
|
||||
import { Link } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { NameValueTable, SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import React from 'react';
|
||||
import { SealedSecret } from '../lib/SealedSecretCRD';
|
||||
|
||||
interface SecretDetailsSectionProps {
|
||||
resource: any; // The Secret resource
|
||||
}
|
||||
|
||||
/**
|
||||
* Secret details section component
|
||||
*/
|
||||
export function SecretDetailsSection({ resource }: SecretDetailsSectionProps) {
|
||||
// Check if this Secret is owned by a SealedSecret
|
||||
const ownerRef = resource.metadata?.ownerReferences?.find(
|
||||
(ref: any) => ref.kind === 'SealedSecret' && ref.apiVersion === 'bitnami.com/v1alpha1'
|
||||
);
|
||||
|
||||
if (!ownerRef) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch the parent SealedSecret
|
||||
const [sealedSecret] = SealedSecret.useGet(ownerRef.name, resource.metadata.namespace);
|
||||
|
||||
return (
|
||||
<SectionBox title="Sealed Secret">
|
||||
{sealedSecret ? (
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Parent SealedSecret',
|
||||
value: (
|
||||
<Link
|
||||
routeName="sealedsecret"
|
||||
params={{
|
||||
namespace: sealedSecret.metadata.namespace,
|
||||
name: sealedSecret.metadata.name,
|
||||
}}
|
||||
>
|
||||
{sealedSecret.metadata.name}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Scope',
|
||||
value: sealedSecret.scope,
|
||||
},
|
||||
{
|
||||
name: 'Sync Status',
|
||||
value: (
|
||||
<StatusLabel status={sealedSecret.isSynced ? 'success' : 'error'}>
|
||||
{sealedSecret.isSynced ? 'Synced' : 'Not Synced'}
|
||||
</StatusLabel>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<p>Loading SealedSecret information...</p>
|
||||
)}
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Settings Page
|
||||
*
|
||||
* Configuration page for the Sealed Secrets plugin
|
||||
*/
|
||||
|
||||
import { SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import React from 'react';
|
||||
import { getPluginConfig, savePluginConfig } from '../lib/controller';
|
||||
import { PluginConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Settings page component
|
||||
*/
|
||||
export function SettingsPage() {
|
||||
const [config, setConfig] = React.useState<PluginConfig>(getPluginConfig());
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const handleSave = () => {
|
||||
savePluginConfig(config);
|
||||
enqueueSnackbar('Settings saved successfully', { variant: 'success' });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
const defaultConfig: PluginConfig = {
|
||||
controllerName: 'sealed-secrets-controller',
|
||||
controllerNamespace: 'kube-system',
|
||||
controllerPort: 8080,
|
||||
};
|
||||
setConfig(defaultConfig);
|
||||
};
|
||||
|
||||
return (
|
||||
<SectionBox
|
||||
title="Sealed Secrets Plugin Settings"
|
||||
>
|
||||
<Box p={3}>
|
||||
<Typography variant="body1" paragraph>
|
||||
Configure the connection to your Sealed Secrets controller. These settings are stored in
|
||||
your browser's local storage.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Controller Name"
|
||||
value={config.controllerName}
|
||||
onChange={e => setConfig({ ...config, controllerName: e.target.value })}
|
||||
margin="normal"
|
||||
helperText="Name of the sealed-secrets-controller deployment/service"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Controller Namespace"
|
||||
value={config.controllerNamespace}
|
||||
onChange={e => setConfig({ ...config, controllerNamespace: e.target.value })}
|
||||
margin="normal"
|
||||
helperText="Namespace where the controller is installed"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Controller Port"
|
||||
type="number"
|
||||
value={config.controllerPort}
|
||||
onChange={e => setConfig({ ...config, controllerPort: parseInt(e.target.value, 10) })}
|
||||
margin="normal"
|
||||
helperText="HTTP port of the controller service"
|
||||
/>
|
||||
|
||||
<Box mt={3} display="flex" gap={2}>
|
||||
<Button variant="contained" onClick={handleSave}>
|
||||
Save Settings
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={handleReset}>
|
||||
Reset to Defaults
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box mt={4} p={2} bgcolor="info.light" borderRadius={1}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Default Values
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
<strong>Controller Name:</strong> sealed-secrets-controller
|
||||
<br />
|
||||
<strong>Controller Namespace:</strong> kube-system
|
||||
<br />
|
||||
<strong>Controller Port:</strong> 8080
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</SectionBox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 The Kubernetes Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="@kinvolk/headlamp-plugin" />
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Headlamp Sealed Secrets Plugin
|
||||
*
|
||||
* A comprehensive plugin for managing Bitnami Sealed Secrets in Kubernetes.
|
||||
* Provides UI for viewing, creating, and managing encrypted secrets.
|
||||
*
|
||||
* Features:
|
||||
* - List and detail views for SealedSecrets
|
||||
* - Client-side encryption using controller's public key
|
||||
* - Sealing keys management
|
||||
* - Secret decryption (via K8s Secret access)
|
||||
* - Integration with Headlamp's Secret detail view
|
||||
*
|
||||
* @see https://github.com/bitnami-labs/sealed-secrets
|
||||
*/
|
||||
|
||||
import {
|
||||
registerDetailsViewSection,
|
||||
registerRoute,
|
||||
registerSidebarEntry,
|
||||
} from '@kinvolk/headlamp-plugin/lib';
|
||||
import React from 'react';
|
||||
import { SealedSecretDetail } from './components/SealedSecretDetail';
|
||||
import { SealedSecretList } from './components/SealedSecretList';
|
||||
import { SealingKeysView } from './components/SealingKeysView';
|
||||
import { SecretDetailsSection } from './components/SecretDetailsSection';
|
||||
import { SettingsPage } from './components/SettingsPage';
|
||||
|
||||
/**
|
||||
* Register sidebar navigation
|
||||
*/
|
||||
|
||||
// Main "Sealed Secrets" entry
|
||||
registerSidebarEntry({
|
||||
parent: null,
|
||||
name: 'sealed-secrets',
|
||||
label: 'Sealed Secrets',
|
||||
icon: 'mdi:lock',
|
||||
url: '/sealedsecrets',
|
||||
});
|
||||
|
||||
// "All Sealed Secrets" child entry
|
||||
registerSidebarEntry({
|
||||
parent: 'sealed-secrets',
|
||||
name: 'sealed-secrets-list',
|
||||
label: 'All Sealed Secrets',
|
||||
url: '/sealedsecrets',
|
||||
});
|
||||
|
||||
// "Sealing Keys" child entry
|
||||
registerSidebarEntry({
|
||||
parent: 'sealed-secrets',
|
||||
name: 'sealing-keys',
|
||||
label: 'Sealing Keys',
|
||||
url: '/sealedsecrets/keys',
|
||||
});
|
||||
|
||||
// "Settings" child entry
|
||||
registerSidebarEntry({
|
||||
parent: 'sealed-secrets',
|
||||
name: 'sealed-secrets-settings',
|
||||
label: 'Settings',
|
||||
url: '/sealedsecrets/settings',
|
||||
});
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
|
||||
// List view
|
||||
registerRoute({
|
||||
path: '/sealedsecrets',
|
||||
sidebar: 'sealed-secrets-list',
|
||||
component: () => <SealedSecretList />,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// Detail view
|
||||
registerRoute({
|
||||
path: '/sealedsecrets/:namespace/:name',
|
||||
sidebar: 'sealed-secrets-list',
|
||||
component: () => <SealedSecretDetail />,
|
||||
exact: true,
|
||||
name: 'sealedsecret',
|
||||
});
|
||||
|
||||
// Sealing keys view
|
||||
registerRoute({
|
||||
path: '/sealedsecrets/keys',
|
||||
sidebar: 'sealing-keys',
|
||||
component: () => <SealingKeysView />,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// Settings page
|
||||
registerRoute({
|
||||
path: '/sealedsecrets/settings',
|
||||
sidebar: 'sealed-secrets-settings',
|
||||
component: () => <SettingsPage />,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Register integration with Secret detail view
|
||||
*
|
||||
* Adds a "Sealed Secret" section to Secrets that are owned by SealedSecrets
|
||||
*/
|
||||
registerDetailsViewSection(({ resource }) => {
|
||||
if (resource?.kind === 'Secret') {
|
||||
return <SecretDetailsSection resource={resource} />;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* SealedSecret Custom Resource Definition
|
||||
*/
|
||||
|
||||
import { apiFactoryWithNamespace } from '@kinvolk/headlamp-plugin/lib/lib/k8s/apiProxy';
|
||||
import { KubeObject } from '@kinvolk/headlamp-plugin/lib/lib/k8s/cluster';
|
||||
import {
|
||||
SealedSecretInterface,
|
||||
SealedSecretScope,
|
||||
SealedSecretSpec,
|
||||
SealedSecretStatus,
|
||||
} from '../types';
|
||||
|
||||
/**
|
||||
* SealedSecret CRD class
|
||||
* Represents a Bitnami Sealed Secret resource in the cluster
|
||||
*/
|
||||
export class SealedSecret extends KubeObject<SealedSecretInterface> {
|
||||
/**
|
||||
* API endpoint for SealedSecret resources
|
||||
* bitnami.com/v1alpha1/sealedsecrets
|
||||
*/
|
||||
static apiEndpoint = apiFactoryWithNamespace('bitnami.com', 'v1alpha1', 'sealedsecrets');
|
||||
|
||||
/**
|
||||
* Class name used for Headlamp registration
|
||||
*/
|
||||
static get className(): string {
|
||||
return 'SealedSecret';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SealedSecret spec
|
||||
*/
|
||||
get spec(): SealedSecretSpec {
|
||||
return this.jsonData.spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SealedSecret status
|
||||
*/
|
||||
get status(): SealedSecretStatus | undefined {
|
||||
return this.jsonData.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the scope of this SealedSecret (strict, namespace-wide, or cluster-wide)
|
||||
*/
|
||||
get scope(): SealedSecretScope {
|
||||
const annotations = this.metadata.annotations || {};
|
||||
|
||||
if (annotations['sealedsecrets.bitnami.com/cluster-wide'] === 'true') {
|
||||
return 'cluster-wide';
|
||||
}
|
||||
if (annotations['sealedsecrets.bitnami.com/namespace-wide'] === 'true') {
|
||||
return 'namespace-wide';
|
||||
}
|
||||
return 'strict';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of encrypted keys
|
||||
*/
|
||||
get encryptedKeysCount(): number {
|
||||
return Object.keys(this.spec.encryptedData || {}).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the SealedSecret is synced
|
||||
*/
|
||||
get isSynced(): boolean {
|
||||
const syncCondition = this.status?.conditions?.find(c => c.type === 'Synced');
|
||||
return syncCondition?.status === 'True';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sync status condition
|
||||
*/
|
||||
get syncCondition() {
|
||||
return this.status?.conditions?.find(c => c.type === 'Synced');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sync status message
|
||||
*/
|
||||
get syncMessage(): string {
|
||||
const condition = this.syncCondition;
|
||||
if (!condition) {
|
||||
return 'Unknown';
|
||||
}
|
||||
return condition.message || condition.reason || condition.status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Sealed Secrets Controller API helpers
|
||||
*
|
||||
* Utilities for interacting with the sealed-secrets-controller HTTP API
|
||||
* via the Kubernetes API proxy.
|
||||
*/
|
||||
|
||||
import { request } from '@kinvolk/headlamp-plugin/lib/lib/k8s/apiProxy';
|
||||
import { PluginConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Build the controller proxy URL
|
||||
*/
|
||||
export function getControllerProxyURL(config: PluginConfig, path: string): string {
|
||||
const { controllerNamespace, controllerName, controllerPort } = config;
|
||||
return `/api/v1/namespaces/${controllerNamespace}/services/http:${controllerName}:${controllerPort}/proxy${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the controller's public certificate
|
||||
*
|
||||
* @param config Plugin configuration
|
||||
* @returns PEM-encoded certificate
|
||||
*/
|
||||
export async function fetchPublicCertificate(config: PluginConfig): Promise<string> {
|
||||
const url = getControllerProxyURL(config, '/v1/cert.pem');
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch certificate: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const cert = await response.text();
|
||||
return cert;
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to fetch controller certificate: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a SealedSecret can be decrypted by the controller
|
||||
*
|
||||
* @param config Plugin configuration
|
||||
* @param sealedSecretYaml YAML or JSON of the SealedSecret
|
||||
*/
|
||||
export async function verifySealedSecret(
|
||||
config: PluginConfig,
|
||||
sealedSecretYaml: string
|
||||
): Promise<boolean> {
|
||||
const url = getControllerProxyURL(config, '/v1/verify');
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: sealedSecretYaml,
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Verification failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate (re-encrypt) a SealedSecret with the current active key
|
||||
*
|
||||
* @param config Plugin configuration
|
||||
* @param sealedSecretYaml YAML or JSON of the SealedSecret
|
||||
* @returns The re-encrypted SealedSecret
|
||||
*/
|
||||
export async function rotateSealedSecret(
|
||||
config: PluginConfig,
|
||||
sealedSecretYaml: string
|
||||
): Promise<string> {
|
||||
const url = getControllerProxyURL(config, '/v1/rotate');
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: sealedSecretYaml,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Rotation failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
throw new Error(`Unable to rotate SealedSecret: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plugin configuration from localStorage
|
||||
*/
|
||||
export function getPluginConfig(): PluginConfig {
|
||||
const stored = localStorage.getItem('sealed-secrets-plugin-config');
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored);
|
||||
} catch {
|
||||
// Fall through to default
|
||||
}
|
||||
}
|
||||
|
||||
// Return default config
|
||||
return {
|
||||
controllerName: 'sealed-secrets-controller',
|
||||
controllerNamespace: 'kube-system',
|
||||
controllerPort: 8080,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save plugin configuration to localStorage
|
||||
*/
|
||||
export function savePluginConfig(config: PluginConfig): void {
|
||||
localStorage.setItem('sealed-secrets-plugin-config', JSON.stringify(config));
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Client-side encryption utilities for Sealed Secrets
|
||||
*
|
||||
* This module handles the encryption of secret values using the sealed-secrets
|
||||
* controller's public key. The encryption process matches the kubeseal CLI tool:
|
||||
*
|
||||
* 1. Generate a random AES-256-GCM session key
|
||||
* 2. Encrypt the secret value with the session key
|
||||
* 3. Encrypt the session key with the RSA public key (OAEP + SHA-256)
|
||||
* 4. Construct the payload: 2-byte length prefix + encrypted session key + encrypted data
|
||||
* 5. Base64-encode the result
|
||||
*/
|
||||
|
||||
import forge from 'node-forge';
|
||||
import { SealedSecretScope } from '../types';
|
||||
|
||||
/**
|
||||
* Parse a PEM certificate and extract the RSA public key
|
||||
*/
|
||||
export function parsePublicKeyFromCert(pemCert: string): forge.pki.rsa.PublicKey {
|
||||
try {
|
||||
const cert = forge.pki.certificateFromPem(pemCert);
|
||||
const publicKey = cert.publicKey as forge.pki.rsa.PublicKey;
|
||||
return publicKey;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse certificate: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a secret value using the kubeseal format
|
||||
*
|
||||
* @param publicKey RSA public key from the controller's certificate
|
||||
* @param value The plaintext secret value to encrypt
|
||||
* @param namespace The namespace (for strict/namespace-wide scoping)
|
||||
* @param name The secret name (for strict scoping)
|
||||
* @param key The key name within the secret
|
||||
* @param scope The encryption scope
|
||||
* @returns Base64-encoded encrypted value
|
||||
*/
|
||||
export function encryptValue(
|
||||
publicKey: forge.pki.rsa.PublicKey,
|
||||
value: string,
|
||||
namespace: string,
|
||||
name: string,
|
||||
key: string,
|
||||
scope: SealedSecretScope
|
||||
): string {
|
||||
try {
|
||||
// Generate a random 32-byte (256-bit) AES session key
|
||||
const sessionKey = forge.random.getBytesSync(32);
|
||||
|
||||
// Construct the label for RSA-OAEP based on scope
|
||||
// This binds the encryption to specific namespace/name/key depending on scope
|
||||
let label = '';
|
||||
if (scope === 'strict') {
|
||||
// Strict scope: namespace.name.key
|
||||
label = `${namespace}.${name}.${key}`;
|
||||
} else if (scope === 'namespace-wide') {
|
||||
// Namespace-wide scope: namespace.key
|
||||
label = `${namespace}.${key}`;
|
||||
} else {
|
||||
// Cluster-wide scope: just the key
|
||||
label = key;
|
||||
}
|
||||
|
||||
// Encrypt the session key with RSA-OAEP (SHA-256)
|
||||
const encryptedSessionKey = publicKey.encrypt(sessionKey, 'RSA-OAEP', {
|
||||
md: forge.md.sha256.create(),
|
||||
mgf1: {
|
||||
md: forge.md.sha256.create(),
|
||||
},
|
||||
label: label,
|
||||
});
|
||||
|
||||
// Encrypt the actual secret value with AES-256-GCM
|
||||
const iv = forge.random.getBytesSync(12); // 12 bytes for GCM
|
||||
const cipher = forge.cipher.createCipher('AES-GCM', sessionKey);
|
||||
cipher.start({ iv: iv });
|
||||
cipher.update(forge.util.createBuffer(value, 'utf8'));
|
||||
cipher.finish();
|
||||
|
||||
const encryptedValue = cipher.output.getBytes();
|
||||
const tag = (cipher.mode as any).tag.getBytes();
|
||||
|
||||
// Construct the sealed secret format:
|
||||
// [2-byte length of encrypted session key][encrypted session key][IV][encrypted value][auth tag]
|
||||
const sessionKeyLength = encryptedSessionKey.length;
|
||||
const lengthBytes = String.fromCharCode((sessionKeyLength >> 8) & 0xff) +
|
||||
String.fromCharCode(sessionKeyLength & 0xff);
|
||||
|
||||
const payload = lengthBytes + encryptedSessionKey + iv + encryptedValue + tag;
|
||||
|
||||
// Base64 encode the final payload
|
||||
return forge.util.encode64(payload);
|
||||
} catch (error) {
|
||||
throw new Error(`Encryption failed: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt multiple key-value pairs for a SealedSecret
|
||||
*
|
||||
* @param publicKey RSA public key from the controller's certificate
|
||||
* @param keyValues Array of {key, value} pairs to encrypt
|
||||
* @param namespace The namespace
|
||||
* @param name The secret name
|
||||
* @param scope The encryption scope
|
||||
* @returns Object mapping keys to encrypted values
|
||||
*/
|
||||
export function encryptKeyValues(
|
||||
publicKey: forge.pki.rsa.PublicKey,
|
||||
keyValues: Array<{ key: string; value: string }>,
|
||||
namespace: string,
|
||||
name: string,
|
||||
scope: SealedSecretScope
|
||||
): Record<string, string> {
|
||||
const encryptedData: Record<string, string> = {};
|
||||
|
||||
for (const { key, value } of keyValues) {
|
||||
encryptedData[key] = encryptValue(publicKey, value, namespace, name, key, scope);
|
||||
}
|
||||
|
||||
return encryptedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a PEM certificate
|
||||
*/
|
||||
export function validateCertificate(pemCert: string): boolean {
|
||||
try {
|
||||
parsePublicKeyFromCert(pemCert);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* TypeScript interfaces for Bitnami Sealed Secrets plugin
|
||||
*/
|
||||
|
||||
import { KubeObjectInterface } from '@kinvolk/headlamp-plugin/lib/lib/k8s/cluster';
|
||||
|
||||
/**
|
||||
* Sealed Secret scope types
|
||||
*/
|
||||
export type SealedSecretScope = 'strict' | 'namespace-wide' | 'cluster-wide';
|
||||
|
||||
/**
|
||||
* SealedSecret CRD spec
|
||||
*/
|
||||
export interface SealedSecretSpec {
|
||||
/** Map of key names to encrypted (base64-encoded) values */
|
||||
encryptedData: Record<string, string>;
|
||||
/** Metadata template for the resulting Secret */
|
||||
template?: {
|
||||
metadata?: {
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
};
|
||||
type?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SealedSecret status condition
|
||||
*/
|
||||
export interface SealedSecretCondition {
|
||||
type: string;
|
||||
status: 'True' | 'False' | 'Unknown';
|
||||
lastTransitionTime?: string;
|
||||
lastUpdateTime?: string;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* SealedSecret CRD status
|
||||
*/
|
||||
export interface SealedSecretStatus {
|
||||
conditions?: SealedSecretCondition[];
|
||||
observedGeneration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete SealedSecret CRD interface
|
||||
*/
|
||||
export interface SealedSecretInterface extends KubeObjectInterface {
|
||||
spec: SealedSecretSpec;
|
||||
status?: SealedSecretStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin configuration stored in localStorage
|
||||
*/
|
||||
export interface PluginConfig {
|
||||
/** Controller deployment name */
|
||||
controllerName: string;
|
||||
/** Controller namespace */
|
||||
controllerNamespace: string;
|
||||
/** Controller service port */
|
||||
controllerPort: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default plugin configuration
|
||||
*/
|
||||
export const DEFAULT_CONFIG: PluginConfig = {
|
||||
controllerName: 'sealed-secrets-controller',
|
||||
controllerNamespace: 'kube-system',
|
||||
controllerPort: 8080,
|
||||
};
|
||||
|
||||
/**
|
||||
* Key-value pair for encryption dialog
|
||||
*/
|
||||
export interface SecretKeyValue {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption request parameters
|
||||
*/
|
||||
export interface EncryptionRequest {
|
||||
name: string;
|
||||
namespace: string;
|
||||
scope: SealedSecretScope;
|
||||
keyValues: SecretKeyValue[];
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./node_modules/@kinvolk/headlamp-plugin/config/plugins-tsconfig.json",
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user