feat: rebrand Hemmelig to paste.es for cloudhost.es

- Set Spanish as default language with ephemeral/encrypted privacy focus
- Translate all user-facing strings and legal pages to Spanish
- Replace Norwegian flag with Spanish flag in footer
- Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship
- Rewrite PrivacyPage: zero data collection, ephemeral design emphasis
- Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references
- Update PWA manifest, HTML meta tags, package.json branding
- Rename webhook headers to X-Paste-Event / X-Paste-Signature
- Update API docs title and contact to paste.es / cloudhost.es

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:30:19 +01:00
commit bc9f96cbd4
268 changed files with 45773 additions and 0 deletions

106
docs/api.md Normal file
View File

@@ -0,0 +1,106 @@
# API Documentation
Hemmelig provides a REST API for programmatic access to secret sharing functionality.
## Interactive Documentation
The API is documented using OpenAPI 3.0 specification with an interactive Swagger UI:
- **Swagger UI:** `/api/docs` - Interactive API explorer
- **OpenAPI Spec:** `/api/openapi.json` - Raw OpenAPI specification
## Authentication
### Session Authentication
For browser-based access, authenticate through the `/auth` endpoints provided by better-auth. Session cookies are automatically managed.
### API Key Authentication
For programmatic access, create an API key in your account settings under the **Developer** tab.
Use the API key as a Bearer token in the `Authorization` header:
```bash
curl -H "Authorization: Bearer hemmelig_your_api_key_here" \
https://your-instance.com/api/secrets
```
**Important:**
- API keys are shown only once upon creation - store them securely
- Maximum 5 API keys per user
- Keys can optionally expire after 30, 90, or 365 days
- Revoke compromised keys immediately from your account settings
For endpoints requiring admin access, the authenticated user must have the `admin` role.
## Quick Reference
### Public Endpoints
| Method | Endpoint | Description |
| ------ | ------------------------------- | ------------------------------------------------- |
| GET | `/api/healthz` | Health check |
| POST | `/api/secrets` | Create a new secret |
| POST | `/api/secrets/:id` | Retrieve a secret (password in body if protected) |
| GET | `/api/secrets/:id/check` | Check if secret exists and requires password |
| POST | `/api/files` | Upload a file |
| GET | `/api/files/:id` | Download a file |
| GET | `/api/instance/settings/public` | Get public instance settings |
| GET | `/api/setup/status` | Check if initial setup is needed |
| POST | `/api/setup/complete` | Complete initial setup |
### Authenticated Endpoints
| Method | Endpoint | Description |
| ------ | ----------------------- | ------------------- |
| GET | `/api/secrets` | List user's secrets |
| DELETE | `/api/secrets/:id` | Delete a secret |
| GET | `/api/account` | Get account info |
| PUT | `/api/account` | Update account info |
| PUT | `/api/account/password` | Update password |
| DELETE | `/api/account` | Delete account |
| GET | `/api/api-keys` | List API keys |
| POST | `/api/api-keys` | Create API key |
| DELETE | `/api/api-keys/:id` | Delete API key |
### Admin Endpoints
| Method | Endpoint | Description |
| ------ | ------------------------------- | ------------------------- |
| GET | `/api/instance/settings` | Get all instance settings |
| PUT | `/api/instance/settings` | Update instance settings |
| GET | `/api/analytics` | Get secret analytics |
| GET | `/api/analytics/visitors/daily` | Get daily visitor stats |
| GET | `/api/invites` | List invite codes |
| POST | `/api/invites` | Create invite code |
| DELETE | `/api/invites/:id` | Deactivate invite code |
| PUT | `/api/user/:id` | Update user |
## Example: Create a Secret
```bash
curl -X POST https://your-instance.com/api/secrets \
-H "Content-Type: application/json" \
-d '{
"secret": "BASE64_ENCRYPTED_CONTENT",
"salt": "ENCRYPTION_SALT",
"expiresAt": 3600,
"views": 1
}'
```
Response:
```json
{
"id": "abc123xyz"
}
```
## Important Notes
- **Client-side encryption:** All secret content should be encrypted client-side before sending to the API. The server only stores encrypted data.
- **Decryption keys:** Never send decryption keys to the server. They should be passed via URL fragments (`#key=...`) which are not transmitted to the server.
- **Rate limiting:** API requests may be rate-limited based on instance settings.

374
docs/cli.md Normal file
View File

@@ -0,0 +1,374 @@
# Hemmelig CLI
The Hemmelig CLI allows you to create encrypted secrets directly from the command line, making it ideal for automation, CI/CD pipelines, and scripting.
```
_ _ _ _
| | | | ___ _ __ ___ _ __ ___ ___| (_) __ _
| |_| |/ _ \ '_ ` _ \| '_ ` _ \ / _ \ | |/ _` |
| _ | __/ | | | | | | | | | | __/ | | (_| |
|_| |_|\___|_| |_| |_|_| |_| |_|\___|_|_|\__, |
|___/
```
## Installation
### Binary (Recommended for CI/CD)
Download the pre-built binary for your platform from the [CLI releases](https://github.com/HemmeligOrg/Hemmelig.app/releases?q=cli-v&expanded=true).
Replace `VERSION` below with the desired version (e.g., `1.0.0`):
#### Linux (amd64)
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/hemmelig-linux-amd64 -o hemmelig
chmod +x hemmelig
sudo mv hemmelig /usr/local/bin/
```
#### Linux (arm64)
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/hemmelig-linux-arm64 -o hemmelig
chmod +x hemmelig
sudo mv hemmelig /usr/local/bin/
```
#### macOS (Apple Silicon)
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/hemmelig-darwin-arm64 -o hemmelig
chmod +x hemmelig
sudo mv hemmelig /usr/local/bin/
```
#### macOS (Intel)
```bash
VERSION=1.0.1
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/hemmelig-darwin-amd64 -o hemmelig
chmod +x hemmelig
sudo mv hemmelig /usr/local/bin/
```
#### Windows
Download `hemmelig-windows-amd64.exe` from the [CLI releases](https://github.com/HemmeligOrg/Hemmelig.app/releases?q=cli-v&expanded=true) and add it to your PATH.
#### Verify Download
```bash
VERSION=1.0.1
# Download checksums
curl -L https://github.com/HemmeligOrg/Hemmelig.app/releases/download/cli-v${VERSION}/checksums.txt -o checksums.txt
# Verify integrity
sha256sum -c checksums.txt --ignore-missing
```
### npm
```bash
# Install globally
npm install -g hemmelig
# Or use with npx (no installation required)
npx hemmelig "my secret"
```
## Usage
```bash
hemmelig <secret> [options]
```
Or pipe content from stdin:
```bash
echo "my secret" | hemmelig [options]
cat file.txt | hemmelig [options]
```
## Options
| Option | Description |
| ----------------------- | --------------------------------------------------- |
| `-t, --title <title>` | Set a title for the secret |
| `-p, --password <pass>` | Protect with a password (if not set, key is in URL) |
| `-e, --expires <time>` | Expiration time (default: 1d) |
| `-v, --views <number>` | Max views before deletion (default: 1, max: 9999) |
| `-b, --burnable` | Burn after first view (default: true) |
| `--no-burnable` | Don't burn after first view |
| `-u, --url <url>` | Base URL (default: https://hemmelig.app) |
| `-h, --help, /?` | Show help message |
### Expiration Times
Valid expiration values: `5m`, `30m`, `1h`, `4h`, `12h`, `1d`, `3d`, `7d`, `14d`, `28d`
## Examples
### Basic Usage
```bash
# Create a simple secret (expires in 1 day, 1 view)
hemmelig "my secret message"
# Create a secret with a title
hemmelig "database_password=secret123" -t "Database Credentials"
# Set custom expiration and view count
hemmelig "temporary token" -e 1h -v 3
```
### Password Protection
```bash
# Create a password-protected secret
hemmelig "sensitive data" -p "mypassword123"
```
When password-protected, the recipient must enter the password to decrypt the secret. The URL will not contain the decryption key.
### Self-Hosted Instances
```bash
# Use your own Hemmelig instance
hemmelig "internal secret" -u https://secrets.company.com
```
## CI/CD Integration
The CLI is designed for automation. It outputs only the secret URL to stdout, making it easy to capture and use in scripts.
### GitHub Actions
Share secrets securely between workflow jobs or with external parties:
```yaml
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Share deployment credentials
run: |
SECRET_URL=$(npx hemmelig "${{ secrets.DEPLOY_KEY }}" \
-t "Deployment Key" \
-e 1h \
-v 1 \
-u https://secrets.company.com)
echo "Secure link: $SECRET_URL"
# Send to Slack, email, etc.
```
### GitLab CI
```yaml
share-credentials:
stage: deploy
script:
- |
SECRET_URL=$(npx hemmelig "$DB_PASSWORD" \
-t "Database Password" \
-e 4h \
-u https://secrets.company.com)
echo "Secret URL: $SECRET_URL"
```
### Jenkins Pipeline
```groovy
pipeline {
agent any
stages {
stage('Share Secret') {
steps {
script {
def secretUrl = sh(
script: '''
npx hemmelig "${API_KEY}" \
-t "API Key for deployment" \
-e 1h \
-u https://secrets.company.com
''',
returnStdout: true
).trim()
echo "Secret available at: ${secretUrl}"
}
}
}
}
}
```
## Automation Use Cases
### Secure Credential Handoff
When onboarding new team members or sharing credentials with contractors:
```bash
#!/bin/bash
# generate-access.sh
DB_CREDS="host: db.internal.com
user: app_user
password: $(openssl rand -base64 32)"
SECRET_URL=$(echo "$DB_CREDS" | hemmelig \
-t "Database Access - $(date +%Y-%m-%d)" \
-e 24h \
-v 1)
echo "Send this link to the new team member: $SECRET_URL"
```
### Automated Secret Rotation
Share rotated secrets with dependent services:
```bash
#!/bin/bash
# rotate-and-share.sh
NEW_PASSWORD=$(openssl rand -base64 24)
# Update the password in your system
update_service_password "$NEW_PASSWORD"
# Share with the dependent team
SECRET_URL=$(hemmelig "$NEW_PASSWORD" \
-t "Rotated Service Password" \
-e 1h \
-v 1 \
-u https://secrets.company.com)
# Notify via Slack
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\": \"Password rotated. New credentials: $SECRET_URL\"}"
```
### Sharing Build Artifacts Securely
```bash
#!/bin/bash
# share-artifact.sh
# Generate a signed URL or token for the artifact
ARTIFACT_TOKEN=$(generate_artifact_token)
SECRET_URL=$(hemmelig "$ARTIFACT_TOKEN" \
-t "Build Artifact Access Token" \
-e 4h \
-v 5)
echo "Artifact access link: $SECRET_URL"
```
### Emergency Access Credentials
Create break-glass credentials that self-destruct:
```bash
#!/bin/bash
# emergency-access.sh
EMERGENCY_CREDS=$(cat << EOF
Emergency Admin Access
======================
URL: https://admin.company.com
Username: emergency_admin
Password: $(openssl rand -base64 32)
MFA Backup: $(generate_mfa_backup)
This access expires in 1 hour.
EOF
)
SECRET_URL=$(echo "$EMERGENCY_CREDS" | hemmelig \
-t "Emergency Access Credentials" \
-e 1h \
-v 1 \
-p "emergency-$(date +%s)")
echo "Emergency access: $SECRET_URL"
echo "Password hint: emergency-[unix timestamp]"
```
## Programmatic Usage
The CLI can also be used as a library in your Node.js projects:
```typescript
import { createSecret } from 'hemmelig';
const result = await createSecret({
secret: 'my secret message',
title: 'API Key',
expiresIn: '1h',
views: 1,
burnable: true,
baseUrl: 'https://hemmelig.app', // optional
});
console.log(result.url); // https://hemmelig.app/secret/abc123#decryptionKey=...
console.log(result.id); // abc123
```
### API Reference
#### `createSecret(options: SecretOptions): Promise<CreateSecretResult>`
| Option | Type | Default | Description |
| ----------- | --------------- | ------------------------ | ----------------------------- |
| `secret` | `string` | required | The secret content to encrypt |
| `title` | `string` | - | Optional title |
| `password` | `string` | - | Password protection |
| `expiresIn` | `ExpirationKey` | `'1d'` | Expiration time |
| `views` | `number` | `1` | Max views (1-9999) |
| `burnable` | `boolean` | `true` | Burn on first view |
| `baseUrl` | `string` | `'https://hemmelig.app'` | Server URL |
**Returns:**
| Property | Type | Description |
| ----------- | -------- | ----------------------------- |
| `url` | `string` | Full URL to access the secret |
| `id` | `string` | The secret ID |
| `expiresIn` | `string` | The expiration time set |
## Security Notes
- **Client-side encryption**: All encryption happens locally before data is sent to the server
- **Zero-knowledge**: The server never sees your plaintext secrets or encryption keys
- **URL fragments**: When not using a password, the decryption key is in the URL fragment (`#decryptionKey=...`), which is never sent to the server
- **Self-destructing**: Secrets are automatically deleted after the specified views or expiration time
## Troubleshooting
### Secret Creation Fails
If you're using a self-hosted instance and secret creation fails, ensure:
1. The instance URL is correct and accessible
2. The server is running and healthy
3. CORS is configured to allow requests from the CLI origin
### Piped Content Issues
When piping content, the CLI preserves all internal newlines and formatting. Only trailing whitespace is trimmed.
```bash
# This preserves the JSON formatting
cat config.json | hemmelig -t "Config"
```

267
docs/docker.md Normal file
View File

@@ -0,0 +1,267 @@
# Docker Deployment
Complete guide for deploying Hemmelig using Docker.
## Architecture Support
Hemmelig Docker images are built for multiple architectures:
| Architecture | Supported | Use Case |
| ------------- | --------- | -------------------------------------------- |
| `linux/amd64` | Yes | Intel/AMD servers, most cloud providers |
| `linux/arm64` | Yes | Apple Silicon, AWS Graviton, Raspberry Pi 4+ |
Docker will automatically pull the correct image for your platform.
## Quick Start
```bash
docker run -d \
--name hemmelig \
-p 3000:3000 \
-v hemmelig-data:/app/database \
-v hemmelig-uploads:/app/uploads \
-e DATABASE_URL="file:/app/database/hemmelig.db" \
-e BETTER_AUTH_SECRET="your-secret-key-min-32-chars" \
-e BETTER_AUTH_URL="https://your-domain.com" \
hemmeligapp/hemmelig:v7
```
## Docker Compose
The repository includes a ready-to-use `docker-compose.yml`:
```bash
# Clone the repository
git clone https://github.com/HemmeligOrg/Hemmelig.app.git
cd Hemmelig.app
# Edit environment variables
nano docker-compose.yml
# Start the application
docker compose up -d
```
### Configuration
The included `docker-compose.yml` uses SQLite:
```yaml
services:
hemmelig:
image: hemmeligapp/hemmelig:v7
container_name: hemmelig
restart: unless-stopped
volumes:
- ./database:/app/database
- ./uploads:/app/uploads
environment:
- DATABASE_URL=file:/app/database/hemmelig.db
- BETTER_AUTH_SECRET=change-this-to-a-secure-secret-min-32-chars
- BETTER_AUTH_URL=https://secrets.example.com
- NODE_ENV=production
- HEMMELIG_BASE_URL=https://secrets.example.com
ports:
- '3000:3000'
healthcheck:
test:
[
'CMD',
'wget',
'--no-verbose',
'--tries=1',
'--spider',
'http://localhost:3000/api/health/ready',
]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
```
**Important:** Before starting, update the following:
- `BETTER_AUTH_SECRET` - Generate with `openssl rand -base64 32`
- `HEMMELIG_BASE_URL` - Your public domain URL
## Volume Mounts
| Container Path | Purpose | Required |
| --------------- | ----------------------- | -------- |
| `/app/database` | SQLite database storage | Yes |
| `/app/uploads` | File upload storage | Yes |
## Environment Variables
See [Environment Variables](./env.md) for a complete reference.
### Required Variables
| Variable | Description |
| -------------------- | -------------------------------------------------------- |
| `DATABASE_URL` | Database connection string |
| `BETTER_AUTH_SECRET` | Authentication secret (min 32 characters) |
| `BETTER_AUTH_URL` | Public URL of your instance (for proper cookie handling) |
### Common Variables
| Variable | Description | Default |
| ------------------- | ---------------------------------------------- | ------------- |
| `NODE_ENV` | Set to `production` for production deployments | `development` |
| `HEMMELIG_BASE_URL` | Public URL of your instance | - |
| `HEMMELIG_PORT` | Internal port (usually leave as default) | `3000` |
## Troubleshooting
### Database Permission Errors
If you see errors like:
```
Error: Migration engine error:
SQLite database error
unable to open database file: /app/database/hemmelig.db
```
This means the container cannot write to the mounted volume. Fix by setting correct ownership on the host:
```bash
# Find your user ID
id -u
# Create directories and set ownership
sudo mkdir -p ./database ./uploads
sudo chown -R $(id -u):$(id -g) ./database ./uploads
```
Or use Docker named volumes instead of bind mounts:
```yaml
volumes:
- hemmelig-data:/app/database
- hemmelig-uploads:/app/uploads
```
### File Upload Permission Errors
If file uploads fail, ensure the uploads directory has correct permissions:
```bash
sudo chown -R $(id -u):$(id -g) ./uploads
chmod 755 ./uploads
```
### Container User
The Hemmelig container runs as user `bun` (non-root) for security. When using bind mounts, ensure the host directories are writable by UID 1000 (the default `bun` user in the container).
## Building from Source
To build the Docker image locally:
```bash
git clone https://github.com/HemmeligOrg/Hemmelig.app.git
cd Hemmelig.app
docker build -t hemmelig .
```
### Building for ARM64
To build for ARM64 (e.g., for Apple Silicon or AWS Graviton):
```bash
# Set up Docker buildx with multi-architecture support
docker buildx create --name multiarch --driver docker-container --use
# Build for ARM64
docker buildx build --platform linux/arm64 -t hemmelig:arm64 --load .
# Build for both architectures
docker buildx build --platform linux/amd64,linux/arm64 -t hemmelig:latest --push .
```
The Dockerfile uses a cross-compilation strategy where Prisma client generation runs on the build host's native architecture to avoid QEMU emulation issues.
## Reverse Proxy
### Nginx
1. Create the Nginx configuration file:
```bash
sudo nano /etc/nginx/sites-available/hemmelig
```
2. Add the following configuration (HTTP only, for initial setup):
```nginx
server {
listen 80;
server_name your_domain.com; # Replace with your domain or IP
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
3. Enable the site:
```bash
sudo ln -s /etc/nginx/sites-available/hemmelig /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
```
4. Install Certbot and obtain SSL certificate:
```bash
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d your_domain.com
```
Certbot will automatically modify your Nginx configuration to use HTTPS.
````
## Health Checks
The container exposes a health endpoint at `/api/health/ready`. The built-in healthcheck uses `wget` to verify the application is responding and all dependencies (database, storage) are healthy.
To manually check:
```bash
curl http://localhost:3000/api/health/ready
# Returns: JSON with status and component health details
````
## Updating
```bash
# Pull latest image
docker pull hemmeligapp/hemmelig:v7
# Recreate container
docker compose down
docker compose up -d
```
Database migrations run automatically on startup.
## Security Notes
1. **Always use HTTPS** in production with a reverse proxy
2. **Generate secure secrets**: `openssl rand -base64 32`
3. **Keep the image updated** for security patches
4. **Back up your data** regularly, especially the database

178
docs/e2e.md Normal file
View File

@@ -0,0 +1,178 @@
# End-to-End Testing with Playwright
Hemmelig uses [Playwright](https://playwright.dev/) for end-to-end integration testing.
## Setup
Playwright and browsers are installed as dev dependencies. If you need to install browser dependencies on your system:
```bash
sudo npx playwright install-deps
```
### Test Database
The e2e tests automatically use a **separate test database** (`database/hemmelig-test.db`) that is:
1. Created fresh before each test run
2. Migrated with the latest schema
3. Seeded with a test user
4. Deleted after tests complete
This ensures tests don't affect your development database.
**Test User Credentials** (created automatically):
- Email: `e2e-test@hemmelig.local`
- Username: `e2etestuser`
- Password: `E2ETestPassword123!`
## Running Tests
```bash
# Run all e2e tests
npm run test:e2e
# Run tests with interactive UI
npm run test:e2e:ui
# Run tests in debug mode
npm run test:e2e:debug
# Run a specific test file
npx playwright test tests/e2e/secret.spec.ts
# Run tests in headed mode (see the browser)
npx playwright test --headed
```
## Test Structure
Tests are located in `tests/e2e/`:
| File | Description |
| -------------------- | ------------------------------------------------- |
| `auth.spec.ts` | Authentication tests (setup, login, registration) |
| `home.spec.ts` | Homepage and secret form tests |
| `secret.spec.ts` | Secret creation, viewing, and deletion flows |
| `navigation.spec.ts` | Navigation and routing tests |
| `fixtures.ts` | Shared test fixtures (`authenticatedPage`) |
| `global-setup.ts` | Creates test database and user before tests |
| `global-teardown.ts` | Cleans up test database after tests |
## Configuration
The Playwright configuration is in `playwright.config.ts`:
- **Test directory**: `tests/e2e/`
- **Base URL**: `http://localhost:5173`
- **Browser**: Chromium (Desktop Chrome)
- **Web server**: Automatically starts Vite with test database
- **Global setup**: Creates fresh test database and test user
- **Global teardown**: Deletes test database
## Writing Tests
### Basic Test Structure
```typescript
import { expect, test } from '@playwright/test';
test.describe('Feature Name', () => {
test('should do something', async ({ page }) => {
await page.goto('/');
// Interact with elements
await page.locator('.ProseMirror').fill('My secret');
await page.getByRole('button', { name: /create/i }).click();
// Assert results
await expect(page.getByText(/success/i)).toBeVisible();
});
});
```
### Using Authenticated Page Fixture
For tests that require authentication:
```typescript
import { expect, test } from './fixtures';
test('should create a secret when logged in', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/');
// authenticatedPage is already logged in with test user
await expect(authenticatedPage.locator('.ProseMirror')).toBeVisible();
});
```
### Common Patterns
**Interacting with the secret editor:**
```typescript
const editor = page.locator('.ProseMirror');
await editor.click();
await editor.fill('Secret content');
```
**Creating and viewing a secret:**
```typescript
// Create
await page.goto('/');
await page.locator('.ProseMirror').fill('My secret');
await page
.getByRole('button', { name: /create/i })
.first()
.click();
// Get the URL
const urlInput = page.locator('input[readonly]').first();
const secretUrl = await urlInput.inputValue();
// View
await page.goto(secretUrl);
await page.getByRole('button', { name: /unlock/i }).click();
```
## CI Integration
Tests run in CI with these settings (from `playwright.config.ts`):
- `forbidOnly: true` - Fails if `.only` is left in tests
- `retries: 2` - Retries failed tests twice
- `workers: 1` - Single worker to prevent conflicts
### GitHub Actions Example
```yaml
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium
- name: Run E2E Tests
run: npm run test:e2e
```
## Viewing Test Reports
After running tests, view the HTML report:
```bash
npx playwright show-report
```
## Debugging Failed Tests
1. **Run in debug mode**: `npm run test:e2e:debug`
2. **Run with UI**: `npm run test:e2e:ui`
3. **View traces**: Failed tests generate traces in `test-results/`
4. **Screenshots**: Failed tests save screenshots automatically
## Best Practices
1. **Use data-testid for stable selectors** when possible
2. **Prefer user-facing selectors** like `getByRole`, `getByText`, `getByPlaceholder`
3. **Add appropriate timeouts** for async operations
4. **Keep tests independent** - each test should work in isolation
5. **Use `.first()` when multiple elements match** to avoid strict mode violations

123
docs/encryption.md Normal file
View File

@@ -0,0 +1,123 @@
# Encryption
Hemmelig uses a **zero-knowledge architecture** where all encryption and decryption happens entirely in your browser. The server never sees your plaintext secrets or encryption keys.
## How It Works
1. **Secret Creation**: When you create a secret, it's encrypted in your browser before being sent to the server
2. **Key Transmission**: The decryption key is passed via URL fragment (`#decryptionKey=...`), which is never sent to the server
3. **Secret Retrieval**: When viewing a secret, the encrypted data is fetched and decrypted locally in your browser
## Why URL Fragments?
The decryption key is placed in the URL fragment (the part after `#`) for a critical security reason:
**URL fragments are never transmitted to servers.**
When you visit a URL like `https://example.com/secret/abc123#decryptionKey=xyz`:
- The browser sends a request to `https://example.com/secret/abc123`
- The fragment (`#decryptionKey=xyz`) stays in your browser
- Server logs, proxies, load balancers, and CDNs never see the fragment
- The key exists only in the browser's address bar and JavaScript
This is defined in [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3.5) and is a fundamental behavior of all web browsers.
### What This Means
| Component | Sees the Key? |
| ----------------------------- | ------------- |
| Your browser | ✅ Yes |
| Hemmelig server | ❌ No |
| Reverse proxies (nginx, etc.) | ❌ No |
| CDNs (Cloudflare, etc.) | ❌ No |
| Server access logs | ❌ No |
| Network monitoring tools | ❌ No |
**Note**: Be aware that browser history and bookmarks store the full URL including fragments.
## Technical Details
### Why This Encryption?
Hemmelig uses **AES-256-GCM** via the **Web Crypto API** for several important reasons:
- **Browser-native**: The Web Crypto API is built into all modern browsers. No external libraries required.
- **Hardware-accelerated**: AES is supported by dedicated instructions (AES-NI) in most modern CPUs (Intel, AMD, ARM), making encryption and decryption fast.
- **Battle-tested**: AES-256 is a NIST-approved standard.
- **Authenticated encryption**: GCM mode provides both confidentiality and integrity, detecting any tampering with the ciphertext.
- **No dependencies**: By using native browser APIs, we avoid supply chain risks from third-party cryptography libraries.
### Algorithm
- **Encryption**: AES-256-GCM (Galois/Counter Mode)
- **Key Derivation**: PBKDF2 with SHA-256
- **Implementation**: Web Crypto API (browser-native)
### Parameters
| Parameter | Value | Description |
| ----------------- | ------------------ | -------------------------------------------------------- |
| Algorithm | AES-GCM | Authenticated encryption with associated data |
| Key Length | 256 bits | Maximum AES key size |
| IV Length | 96 bits (12 bytes) | Initialization vector, randomly generated per encryption |
| Salt Length | 32 characters | Unique per secret, stored server-side |
| PBKDF2 Iterations | 1,300,000 | Key derivation iterations |
| PBKDF2 Hash | SHA-256 | Hash function for key derivation |
### Encryption Process
1. **Key Generation**: A 32-character random key is generated using `nanoid`, or a user-provided password is used directly
2. **Key Derivation**: PBKDF2 derives a 256-bit AES key from the password/key and a unique salt
3. **Encryption**: AES-256-GCM encrypts the plaintext with a random 96-bit IV
4. **Output Format**: `IV (12 bytes) || Ciphertext`
## Password Protection
When you set a password on a secret:
- The password is used directly as the encryption key instead of a randomly generated key
- The URL does **not** include the `#decryptionKey=...` fragment
- The recipient must enter the password manually to decrypt the secret
- This allows you to share the URL and password through separate channels for additional security
### Decryption Process
1. **Parse**: Extract the 12-byte IV from the beginning of the encrypted data
2. **Key Derivation**: PBKDF2 derives the same AES key using the password/key and salt
3. **Decryption**: AES-GCM decrypts and authenticates the ciphertext
## Security Properties
- **Confidentiality**: AES-256 provides strong encryption
- **Integrity**: GCM mode provides authenticated encryption, detecting any tampering
- **Key Strength**: PBKDF2 with 1,300,000 iterations provides resistance against brute-force attacks
- **Forward Secrecy**: Each secret uses a unique salt and random IV
## File Encryption
Files are encrypted using the same AES-256-GCM scheme. The file buffer is encrypted directly, and the output format is identical: `IV || Ciphertext`.
## What the Server Stores
- Encrypted secret (ciphertext)
- Salt (used for key derivation)
- Metadata (expiration, view count, etc.)
## What the Server Never Sees
- Plaintext secrets
- Encryption keys or passwords
- Decryption keys (passed via URL fragment)
## References
- [MDN Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) - Browser-native cryptography documentation
- [MDN AES-GCM](https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams) - AES-GCM algorithm parameters
- [MDN PBKDF2](https://developer.mozilla.org/en-US/docs/Web/API/Pbkdf2Params) - PBKDF2 key derivation parameters
- [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html) - Best practices for cryptographic storage
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) - Key derivation recommendations
- [NIST SP 800-132](https://csrc.nist.gov/pubs/sp/800/132/final) - Password-Based Key Derivation (current version)
- [NIST SP 800-132 Revision Proposal](https://csrc.nist.gov/News/2023/proposal-to-revise-nist-sp-800-132-pbkdf) - Upcoming revision with memory-hard functions
- [NIST AES Specification](https://csrc.nist.gov/publications/detail/fips/197/final) - Official AES standard (FIPS 197)
- [Crypto 101](https://www.crypto101.io/) - Free introductory course on cryptography

186
docs/env.md Normal file
View File

@@ -0,0 +1,186 @@
# Environment Variables
Complete reference for all environment variables supported by Hemmelig.
## Required Variables
| Variable | Description | Default |
| -------------------- | -------------------------------------------------------- | ------------------------- |
| `DATABASE_URL` | SQLite connection string | `file:./data/hemmelig.db` |
| `BETTER_AUTH_SECRET` | Secret key for authentication sessions | - |
| `BETTER_AUTH_URL` | Public URL of your instance (for proper cookie handling) | - |
## Server Configuration
| Variable | Description | Default |
| ------------------------- | ------------------------------------------------ | ------------- |
| `NODE_ENV` | Environment mode (`production` or `development`) | `development` |
| `HEMMELIG_PORT` | Port the server listens on | `3000` |
| `HEMMELIG_BASE_URL` | Public URL of your instance (required for OAuth) | - |
| `HEMMELIG_TRUSTED_ORIGIN` | Additional trusted origin for CORS | - |
## General Settings
| Variable | Description | Default |
| ------------------------------- | --------------------------------------------- | ------- |
| `HEMMELIG_INSTANCE_NAME` | Custom name for your instance | - |
| `HEMMELIG_INSTANCE_DESCRIPTION` | Custom description for your instance | - |
| `HEMMELIG_ALLOW_REGISTRATION` | Allow new user registrations (`true`/`false`) | `true` |
## Security Settings
| Variable | Description | Default |
| ---------------------------------------- | ------------------------------------------------------------- | ------- |
| `HEMMELIG_ALLOW_PASSWORD_PROTECTION` | Allow password-protected secrets | `true` |
| `HEMMELIG_ALLOW_IP_RESTRICTION` | Allow IP range restrictions on secrets | `true` |
| `HEMMELIG_ALLOW_FILE_UPLOADS` | Allow users to attach files to secrets | `true` |
| `HEMMELIG_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password registration (social login only) | `false` |
| `HEMMELIG_MAX_ENCRYPTED_PAYLOAD_SIZE` | Hard ceiling for encrypted payloads in KB (parsed at startup) | `1024` |
## Analytics
| Variable | Description | Default |
| -------------------------------- | --------------------------------------- | -------------- |
| `HEMMELIG_ANALYTICS_ENABLED` | Enable privacy-focused analytics | `true` |
| `HEMMELIG_ANALYTICS_HMAC_SECRET` | HMAC secret for anonymizing visitor IDs | auto-generated |
## Social Login Providers
See [Social Login Documentation](./social-login.md) for detailed setup instructions.
### GitHub
| Variable | Description |
| ----------------------------- | ------------------------------ |
| `HEMMELIG_AUTH_GITHUB_ID` | GitHub OAuth App Client ID |
| `HEMMELIG_AUTH_GITHUB_SECRET` | GitHub OAuth App Client Secret |
### Google
| Variable | Description |
| ----------------------------- | -------------------------- |
| `HEMMELIG_AUTH_GOOGLE_ID` | Google OAuth Client ID |
| `HEMMELIG_AUTH_GOOGLE_SECRET` | Google OAuth Client Secret |
### Microsoft (Azure AD)
| Variable | Description |
| ----------------------------------- | --------------------------------------------------- |
| `HEMMELIG_AUTH_MICROSOFT_ID` | Microsoft Application (client) ID |
| `HEMMELIG_AUTH_MICROSOFT_SECRET` | Microsoft Client Secret |
| `HEMMELIG_AUTH_MICROSOFT_TENANT_ID` | Azure AD Tenant ID (optional, defaults to "common") |
### Discord
| Variable | Description |
| ------------------------------ | --------------------------------- |
| `HEMMELIG_AUTH_DISCORD_ID` | Discord Application Client ID |
| `HEMMELIG_AUTH_DISCORD_SECRET` | Discord Application Client Secret |
### GitLab
| Variable | Description |
| ----------------------------- | ------------------------- |
| `HEMMELIG_AUTH_GITLAB_ID` | GitLab Application ID |
| `HEMMELIG_AUTH_GITLAB_SECRET` | GitLab Application Secret |
### Apple
| Variable | Description |
| ---------------------------- | ------------------- |
| `HEMMELIG_AUTH_APPLE_ID` | Apple Services ID |
| `HEMMELIG_AUTH_APPLE_SECRET` | Apple Client Secret |
### Twitter/X
| Variable | Description |
| ------------------------------ | ------------------------------- |
| `HEMMELIG_AUTH_TWITTER_ID` | Twitter OAuth 2.0 Client ID |
| `HEMMELIG_AUTH_TWITTER_SECRET` | Twitter OAuth 2.0 Client Secret |
### Generic OAuth
Hemmelig supports any OAuth 2.0 / OpenID Connect provider through generic OAuth configuration.
| Variable | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `HEMMELIG_AUTH_GENERIC_OAUTH` | JSON array of generic OAuth provider configurations. See [Social Login docs](./social-login.md) for details. |
**Example**:
```bash
HEMMELIG_AUTH_GENERIC_OAUTH='[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/.well-known/openid-configuration","clientId":"client-id","clientSecret":"secret","scopes":["openid","profile","email"]}]'
```
Supported generic providers include: Authentik, Authelia, Keycloak, Zitadel, Ory Hydra, and any OAuth 2.0 / OIDC-compatible identity provider.
## Example Configuration
### Minimal Setup
```bash
# Required
DATABASE_URL=file:./data/hemmelig.db
BETTER_AUTH_SECRET=your-secret-key-min-32-chars-long
BETTER_AUTH_URL=https://secrets.example.com
```
### Production Setup
```bash
# Required
DATABASE_URL=file:./data/hemmelig.db
BETTER_AUTH_SECRET=your-very-secure-secret-key-here
BETTER_AUTH_URL=https://secrets.example.com
# Server
NODE_ENV=production
HEMMELIG_PORT=3000
HEMMELIG_TRUSTED_ORIGIN=https://secrets.example.com
# Instance
HEMMELIG_INSTANCE_NAME=Company Secrets
HEMMELIG_INSTANCE_DESCRIPTION=Secure secret sharing for our team
# Security
HEMMELIG_ENABLE_RATE_LIMITING=true
# Analytics
HEMMELIG_ANALYTICS_ENABLED=true
HEMMELIG_ANALYTICS_HMAC_SECRET=your-analytics-hmac-secret
# Social Login (optional)
HEMMELIG_AUTH_GITHUB_ID=your-github-client-id
HEMMELIG_AUTH_GITHUB_SECRET=your-github-client-secret
```
### Docker Compose Example
```yaml
version: '3.8'
services:
hemmelig:
image: hemmelig/hemmelig:latest
ports:
- '3000:3000'
environment:
- DATABASE_URL=file:/data/hemmelig.db
- BETTER_AUTH_SECRET=change-this-to-a-secure-secret
- BETTER_AUTH_URL=https://secrets.example.com
- NODE_ENV=production
- HEMMELIG_PORT=3000
- HEMMELIG_ANALYTICS_ENABLED=true
volumes:
- hemmelig_data:/data
volumes:
hemmelig_data:
```
## Notes
- Boolean values accept `true` or `false` (case-insensitive)
- All `HEMMELIG_AUTH_*` variables require both `_ID` and `_SECRET` to enable a provider
- `BETTER_AUTH_URL` is required when using social login providers
- Generate secure secrets using: `openssl rand -base64 32`

95
docs/health.md Normal file
View File

@@ -0,0 +1,95 @@
# Health Check Endpoints
Hemmelig provides health check endpoints for monitoring and container orchestration.
## Endpoints
### Liveness Probe
```
GET /api/health/live
```
Simple check confirming the process is running. Use for Kubernetes liveness probes.
**Response:** `200 OK`
```json
{
"status": "healthy",
"timestamp": "2024-01-15T10:30:00.000Z"
}
```
### Readiness Probe
```
GET /api/health/ready
```
Comprehensive check verifying all dependencies are operational. Use for Kubernetes readiness probes.
**Checks performed:**
| Check | Description |
| ------------ | ---------------------------------------- |
| **Database** | Executes `SELECT 1`, measures latency |
| **Storage** | Verifies uploads directory is read/write |
| **Memory** | Checks RSS is below 1GB threshold |
**Response:** `200 OK` (all healthy) or `503 Service Unavailable` (one or more failed)
```json
{
"status": "healthy",
"timestamp": "2024-01-15T10:30:00.000Z",
"checks": {
"database": { "status": "healthy", "latency_ms": 2 },
"storage": { "status": "healthy" },
"memory": {
"status": "healthy",
"heap_used_mb": 128,
"heap_total_mb": 256,
"rss_mb": 312,
"rss_threshold_mb": 1024
}
}
}
```
### Legacy Endpoint
```
GET /api/healthz
```
Kept for backwards compatibility. Consider using `/api/health/live` instead.
## Kubernetes Configuration
```yaml
livenessProbe:
httpGet:
path: /api/health/live
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/health/ready
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
```
## Docker Compose
```yaml
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/api/health/ready']
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
```

141
docs/helm-oauth.md Normal file
View File

@@ -0,0 +1,141 @@
# Hemmelig Helm Chart - OAuth Configuration Examples
This document demonstrates how to configure OAuth providers with the Hemmelig Helm Chart.
## Using Default Secret Management
The chart can automatically create secrets with your OAuth configuration.
The example below contains all providers supported by the Helm Chart:
```yaml
# values.yaml
config:
betterAuthSecret: "your-auth-secret-here"
betterAuthUrl: "https://secrets.example.com"
baseUrl: "https://secrets.example.com" # Required for OAuth callbacks
oauth:
github:
enabled: true
clientId: "your-github-client-id"
clientSecret: "your-github-client-secret"
google:
enabled: true
clientId: "your-google-client-id"
clientSecret: "your-google-client-secret"
microsoft:
enabled: true
clientId: "your-microsoft-client-id"
clientSecret: "your-microsoft-client-secret"
tenantId: "your-tenant-id" # Optional
discord:
enabled: true
clientId: "your-discord-client-id"
clientSecret: "your-discord-client-secret"
gitlab:
enabled: true
clientId: "your-gitlab-client-id"
clientSecret: "your-gitlab-client-secret"
issuer: "https://gitlab.example.com" # Optional, for self-hosted GitLab
apple:
enabled: true
clientId: "your-apple-client-id"
clientSecret: "your-apple-client-secret"
twitter:
enabled: true
clientId: "your-twitter-client-id"
clientSecret: "your-twitter-client-secret"
generic: '[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/.well-known/openid-configuration","clientId":"client-id","clientSecret":"secret","scopes":["openid","profile","email"]}]'
```
## Using Existing Secret
If you prefer to manage secrets yourself, reference an existing secret
and enable your desired providers:
```yaml
# values.yaml
existingSecret: "hemmelig-secrets"
oauth:
github:
enabled: true
google:
enabled: true
microsoft:
enabled: true
discord:
enabled: true
gitlab:
enabled: true
apple:
enabled: true
twitter:
enabled: true
generic: '[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/.well-known/openid-configuration","clientId":"client-id","clientSecret":"secret","scopes":["openid","profile","email"]}]'
```
Your referenced secret should contain the relevant keys for the providers enabled:
```yaml
apiVersion: v1
kind: Secret
metadata:
name: hemmelig-secrets
type: Opaque
stringData:
BETTER_AUTH_SECRET: "your-auth-secret"
# GitHub
HEMMELIG_AUTH_GITHUB_ID: "github-client-id"
HEMMELIG_AUTH_GITHUB_SECRET: "github-client-secret"
# Google
HEMMELIG_AUTH_GOOGLE_ID: "google-client-id"
HEMMELIG_AUTH_GOOGLE_SECRET: "google-client-secret"
# Microsoft (Azure AD)
HEMMELIG_AUTH_MICROSOFT_ID: "microsoft-client-id"
HEMMELIG_AUTH_MICROSOFT_SECRET: "microsoft-client-secret"
HEMMELIG_AUTH_MICROSOFT_TENANT_ID: "tenant-id" # Optional
# Discord
HEMMELIG_AUTH_DISCORD_ID: "discord-client-id"
HEMMELIG_AUTH_DISCORD_SECRET: "discord-client-secret"
# GitLab
HEMMELIG_AUTH_GITLAB_ID: "gitlab-client-id"
HEMMELIG_AUTH_GITLAB_SECRET: "gitlab-client-secret"
HEMMELIG_AUTH_GITLAB_ISSUER: "https://gitlab.example.com" # Optional
# Apple
HEMMELIG_AUTH_APPLE_ID: "apple-client-id"
HEMMELIG_AUTH_APPLE_SECRET: "apple-client-secret"
# Twitter/X
HEMMELIG_AUTH_TWITTER_ID: "twitter-client-id"
HEMMELIG_AUTH_TWITTER_SECRET: "twitter-client-secret"
# Generic OAuth (JSON array - supports any OAuth 2.0 / OIDC provider)
HEMMELIG_AUTH_GENERIC_OAUTH: "[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/.well-known/openid-configuration","clientId":"client-id","clientSecret":"client-secret","scopes":["openid","profile","email"]}]"
```
## Notes
- All `HEMMELIG_AUTH_*` variables require both `_ID` and `_SECRET`
to enable a provider, except the "Generic" type.
If you enable a provider and not include the required environment variables for it,
the pod will fail to start with CreateContainerConfigError, with an event
similar to the one below:
```
Error: couldn't find key HEMMELIG_AUTH_<missing_env> in Secret default/hemmelig
```
- All OAuth environment variables will be automatically injected into
the deployment, sourced either from the chart-generated secret
or your existing secret.
- If the `existingSecret` value is provided, the `clientId`, `clientSecret`, etc.
values are ignored from the `values.yaml`

205
docs/helm.md Normal file
View File

@@ -0,0 +1,205 @@
# Helm Deployment
Deploy Hemmelig on Kubernetes using Helm.
## Prerequisites
- Kubernetes 1.19+
- Helm 3.0+
- PV provisioner support (for persistence)
## Quick Start
```bash
# Add the chart from local directory
cd Hemmelig.app
# Install with default values
helm install hemmelig ./helm/hemmelig \
--set config.betterAuthSecret="$(openssl rand -base64 32)" \
--set config.betterAuthUrl="https://hemmelig.example.com"
```
## Installation
### From Local Chart
```bash
# Clone the repository
git clone https://github.com/HemmeligOrg/Hemmelig.app.git
cd Hemmelig.app
# Install the chart
helm install hemmelig ./helm/hemmelig -f my-values.yaml
```
### Example values.yaml
```yaml
# my-values.yaml
config:
betterAuthSecret: 'your-secret-key-min-32-chars'
betterAuthUrl: 'https://hemmelig.example.com'
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: hemmelig.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: hemmelig-tls
hosts:
- hemmelig.example.com
persistence:
data:
enabled: true
size: 1Gi
uploads:
enabled: true
size: 10Gi
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
```
## Configuration
### Required Values
| Parameter | Description |
| ------------------------- | ---------------------------------------------------------------------------------- |
| `config.betterAuthSecret` | Authentication secret (min 32 characters). Generate with `openssl rand -base64 32` |
| `config.betterAuthUrl` | Public URL of your instance (required for OAuth and cookie handling) |
### Common Values
| Parameter | Description | Default |
| ----------------------------- | ------------------------------- | ------------------- |
| `replicaCount` | Number of replicas | `1` |
| `image.repository` | Image repository | `hemmelig/hemmelig` |
| `image.tag` | Image tag | `v7` |
| `service.type` | Kubernetes service type | `ClusterIP` |
| `service.port` | Service port | `3000` |
| `ingress.enabled` | Enable ingress | `false` |
| `persistence.data.enabled` | Enable persistence for database | `true` |
| `persistence.data.size` | Database PVC size | `1Gi` |
| `persistence.uploads.enabled` | Enable persistence for uploads | `true` |
| `persistence.uploads.size` | Uploads PVC size | `5Gi` |
### Using Existing Secrets
Instead of setting `config.betterAuthSecret` directly, use an existing Kubernetes secret:
```yaml
existingSecret: my-hemmelig-secret
```
Create the secret:
```bash
kubectl create secret generic my-hemmelig-secret \
--from-literal=BETTER_AUTH_SECRET="$(openssl rand -base64 32)"
```
### Additional Environment Variables
```yaml
env:
- name: HEMMELIG_ANALYTICS_ENABLED
value: 'true'
```
## OAuth Configuration
The Hemmelig Helm Chart supports comprehensive OAuth provider configuration. For detailed setup instructions and examples, see:
**[OAuth Configuration with Helm](helm-oauth.md)**
This guide covers:
- All supported OAuth providers (GitHub, Google, Microsoft, Discord, GitLab, Apple, Twitter/X)
- Generic OAuth providers (Authentik, Authelia, Keycloak, etc.)
- Default secret vs existing secret management
- Required configuration for OAuth callbacks
## Ingress Examples
### Nginx Ingress
```yaml
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: '50m'
hosts:
- host: hemmelig.example.com
paths:
- path: /
pathType: Prefix
```
### Traefik Ingress
```yaml
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.tls: 'true'
hosts:
- host: hemmelig.example.com
paths:
- path: /
pathType: Prefix
```
## Upgrading
```bash
helm upgrade hemmelig ./helm/hemmelig -f my-values.yaml
```
## Uninstalling
```bash
helm uninstall hemmelig
```
**Note:** PersistentVolumeClaims are not deleted automatically. To remove all data:
```bash
kubectl delete pvc -l app.kubernetes.io/name=hemmelig
```
## Troubleshooting
### Check Pod Status
```bash
kubectl get pods -l app.kubernetes.io/name=hemmelig
kubectl logs -l app.kubernetes.io/name=hemmelig
```
### Check PVC Status
```bash
kubectl get pvc -l app.kubernetes.io/name=hemmelig
```
### Port Forward for Testing
```bash
kubectl port-forward svc/hemmelig 3000:3000
# Visit http://localhost:3000
```

299
docs/managed.md Normal file
View File

@@ -0,0 +1,299 @@
# Managed Mode
Run Hemmelig with all instance settings controlled via environment variables. Perfect for containerized deployments, GitOps workflows, and infrastructure-as-code setups where you want configuration locked down and version-controlled.
## Overview
When managed mode is enabled, all instance settings are read from environment variables instead of the database. The admin dashboard becomes read-only, preventing any runtime modifications.
**Key benefits:**
- **Immutable configuration** - Settings can't be changed through the UI
- **GitOps-friendly** - Version control your configuration
- **Reproducible deployments** - Same config across all environments
- **Security hardening** - No accidental configuration changes
## Quick Start
Enable managed mode by setting:
```bash
HEMMELIG_MANAGED=true
```
Then configure your settings via environment variables:
```bash
# Enable managed mode
HEMMELIG_MANAGED=true
# Instance branding
HEMMELIG_INSTANCE_NAME="Company Secrets"
HEMMELIG_INSTANCE_DESCRIPTION="Secure secret sharing for our team"
# Security
HEMMELIG_ALLOW_PASSWORD_PROTECTION=true
HEMMELIG_ALLOW_IP_RESTRICTION=true
HEMMELIG_ENABLE_RATE_LIMITING=true
# Organization
HEMMELIG_REQUIRE_REGISTERED_USER=true
HEMMELIG_ALLOWED_EMAIL_DOMAINS="company.com,partner.com"
```
## Environment Variables
### Core
| Variable | Description | Default |
| ------------------ | ------------------- | ------- |
| `HEMMELIG_MANAGED` | Enable managed mode | `false` |
### General Settings
| Variable | Description | Default |
| ------------------------------------- | ------------------------------------- | ------- |
| `HEMMELIG_INSTANCE_NAME` | Display name for your instance | `""` |
| `HEMMELIG_INSTANCE_DESCRIPTION` | Description shown on the homepage | `""` |
| `HEMMELIG_INSTANCE_LOGO` | Base64-encoded logo image (max 512KB) | `""` |
| `HEMMELIG_ALLOW_REGISTRATION` | Allow new user signups | `true` |
| `HEMMELIG_REQUIRE_EMAIL_VERIFICATION` | Require email verification | `false` |
| `HEMMELIG_DEFAULT_SECRET_EXPIRATION` | Default expiration in hours | `72` |
| `HEMMELIG_MAX_SECRET_SIZE` | Max secret size in KB | `1024` |
| `HEMMELIG_IMPORTANT_MESSAGE` | Alert banner shown to all users | `""` |
### Security Settings
| Variable | Description | Default |
| ------------------------------------ | -------------------------------- | ------- |
| `HEMMELIG_ALLOW_PASSWORD_PROTECTION` | Allow password-protected secrets | `true` |
| `HEMMELIG_ALLOW_IP_RESTRICTION` | Allow IP range restrictions | `true` |
| `HEMMELIG_ALLOW_FILE_UPLOADS` | Allow users to attach files | `true` |
| `HEMMELIG_ENABLE_RATE_LIMITING` | Enable API rate limiting | `true` |
| `HEMMELIG_RATE_LIMIT_REQUESTS` | Max requests per window | `100` |
| `HEMMELIG_RATE_LIMIT_WINDOW` | Rate limit window in seconds | `60` |
### Organization Settings
| Variable | Description | Default |
| ---------------------------------------- | ------------------------------------------------- | ------- |
| `HEMMELIG_REQUIRE_INVITE_CODE` | Require invite code for registration | `false` |
| `HEMMELIG_ALLOWED_EMAIL_DOMAINS` | Comma-separated list of allowed domains | `""` |
| `HEMMELIG_REQUIRE_REGISTERED_USER` | Only registered users can create and read secrets | `false` |
| `HEMMELIG_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password registration (social only) | `false` |
### Webhook Settings
| Variable | Description | Default |
| -------------------------- | ---------------------------------- | ------- |
| `HEMMELIG_WEBHOOK_ENABLED` | Enable webhook notifications | `false` |
| `HEMMELIG_WEBHOOK_URL` | Webhook endpoint URL | `""` |
| `HEMMELIG_WEBHOOK_SECRET` | HMAC secret for webhook signatures | `""` |
| `HEMMELIG_WEBHOOK_ON_VIEW` | Send webhook when secret is viewed | `true` |
| `HEMMELIG_WEBHOOK_ON_BURN` | Send webhook when secret is burned | `true` |
### Metrics Settings
| Variable | Description | Default |
| -------------------------- | ---------------------------------- | ------- |
| `HEMMELIG_METRICS_ENABLED` | Enable Prometheus metrics endpoint | `false` |
| `HEMMELIG_METRICS_SECRET` | Bearer token for `/api/metrics` | `""` |
## Docker Compose Example
```yaml
services:
hemmelig:
image: hemmeligapp/hemmelig:v7
restart: unless-stopped
ports:
- '3000:3000'
volumes:
- ./database:/app/database
- ./uploads:/app/uploads
environment:
# Required
- DATABASE_URL=file:/app/database/hemmelig.db
- BETTER_AUTH_SECRET=your-secret-key-min-32-chars
- BETTER_AUTH_URL=https://secrets.example.com
- NODE_ENV=production
# Enable managed mode
- HEMMELIG_MANAGED=true
# General
- HEMMELIG_INSTANCE_NAME=ACME Secrets
- HEMMELIG_INSTANCE_DESCRIPTION=Internal secret sharing
- HEMMELIG_ALLOW_REGISTRATION=true
- HEMMELIG_DEFAULT_SECRET_EXPIRATION=24
- HEMMELIG_MAX_SECRET_SIZE=2048
# Security
- HEMMELIG_ALLOW_PASSWORD_PROTECTION=true
- HEMMELIG_ALLOW_IP_RESTRICTION=false
- HEMMELIG_ENABLE_RATE_LIMITING=true
- HEMMELIG_RATE_LIMIT_REQUESTS=50
- HEMMELIG_RATE_LIMIT_WINDOW=60
# Organization
- HEMMELIG_REQUIRE_REGISTERED_USER=true
- HEMMELIG_ALLOWED_EMAIL_DOMAINS=acme.com
# Metrics
- HEMMELIG_METRICS_ENABLED=true
- HEMMELIG_METRICS_SECRET=prometheus-scrape-token
```
## Kubernetes Example
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: hemmelig-config
data:
HEMMELIG_MANAGED: 'true'
HEMMELIG_INSTANCE_NAME: 'ACME Secrets'
HEMMELIG_ALLOW_REGISTRATION: 'true'
HEMMELIG_REQUIRE_REGISTERED_USER: 'true'
HEMMELIG_ALLOWED_EMAIL_DOMAINS: 'acme.com'
HEMMELIG_ENABLE_RATE_LIMITING: 'true'
HEMMELIG_METRICS_ENABLED: 'true'
---
apiVersion: v1
kind: Secret
metadata:
name: hemmelig-secrets
type: Opaque
stringData:
BETTER_AUTH_SECRET: 'your-secret-key-min-32-chars'
HEMMELIG_WEBHOOK_SECRET: 'webhook-signing-secret'
HEMMELIG_METRICS_SECRET: 'prometheus-token'
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hemmelig
spec:
replicas: 1
selector:
matchLabels:
app: hemmelig
template:
metadata:
labels:
app: hemmelig
spec:
containers:
- name: hemmelig
image: hemmeligapp/hemmelig:v7
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: hemmelig-config
- secretRef:
name: hemmelig-secrets
env:
- name: DATABASE_URL
value: 'file:/app/database/hemmelig.db'
- name: BETTER_AUTH_URL
value: 'https://secrets.example.com'
- name: NODE_ENV
value: 'production'
```
## Admin Dashboard Behavior
When managed mode is enabled:
1. **Settings are read-only** - All form inputs are disabled
2. **Save buttons are hidden** - No option to modify settings
3. **Banner is displayed** - Admins see a clear "Managed Mode" indicator
4. **API rejects updates** - `PUT /api/instance/settings` returns `403 Forbidden`
Admins can still **view** all settings, making it easy to verify the configuration.
## API Behavior
### Check Managed Mode Status
```bash
curl https://secrets.example.com/api/instance/managed
```
Response:
```json
{
"managed": true
}
```
### Settings Update (Blocked)
When managed mode is enabled, attempting to update settings returns:
```bash
curl -X PUT https://secrets.example.com/api/instance/settings \
-H "Content-Type: application/json" \
-d '{"instanceName": "New Name"}'
```
Response:
```json
{
"error": "Instance is in managed mode. Settings cannot be modified."
}
```
## Migration Guide
### From Database Settings to Managed Mode
1. **Export current settings** - Note your current configuration from the admin dashboard
2. **Create environment configuration** - Translate settings to environment variables
3. **Enable managed mode** - Set `HEMMELIG_MANAGED=true`
4. **Deploy** - Restart with new configuration
Your database settings will be ignored once managed mode is active. The database is still used for secrets, users, and other data - only instance settings come from environment variables.
### Reverting to Database Settings
Simply remove or set `HEMMELIG_MANAGED=false`. Settings will be read from the database again, and the admin dashboard becomes editable.
## Best Practices
1. **Use secrets management** - Store sensitive values like `HEMMELIG_WEBHOOK_SECRET` and `HEMMELIG_METRICS_SECRET` in a secrets manager (Vault, AWS Secrets Manager, etc.)
2. **Version control your config** - Keep your docker-compose or Kubernetes manifests in git
3. **Use CI/CD for changes** - Deploy configuration changes through your pipeline, not manual edits
4. **Document your settings** - Add comments in your configuration files explaining each setting
5. **Test in staging first** - Validate configuration changes in a non-production environment
## Troubleshooting
### Settings Not Applying
- Verify `HEMMELIG_MANAGED=true` is set (case-insensitive)
- Check environment variables are being passed to the container
- Restart the application after changing environment variables
### Dashboard Still Editable
- Clear your browser cache
- Verify the `/api/instance/managed` endpoint returns `{"managed": true}`
- Check server logs for configuration errors
### Rate Limiting Not Working
- Ensure `HEMMELIG_ENABLE_RATE_LIMITING=true`
- Verify `HEMMELIG_RATE_LIMIT_REQUESTS` and `HEMMELIG_RATE_LIMIT_WINDOW` are set
- Rate limiting applies per IP address

85
docs/metrics.md Normal file
View File

@@ -0,0 +1,85 @@
# Prometheus Metrics
Hemmelig provides a Prometheus-compatible metrics endpoint for monitoring your instance.
## Enabling Metrics
1. Go to **Dashboard > Instance > Metrics** tab
2. Enable the **Enable Prometheus Metrics** toggle
3. Optionally, set a **Metrics Secret** for authentication
4. Save the settings
## Endpoint
```
GET /api/metrics
```
## Authentication
If a metrics secret is configured, you must include it as a Bearer token in the `Authorization` header:
```bash
curl -H "Authorization: Bearer YOUR_METRICS_SECRET" https://your-instance.com/api/metrics
```
If no secret is configured, the endpoint is accessible without authentication (not recommended for production).
## Available Metrics
### Application Metrics
| Metric | Type | Description |
| ---------------------------------------- | --------- | -------------------------------------------- |
| `hemmelig_secrets_active_count` | Gauge | Current number of active (unexpired) secrets |
| `hemmelig_users_total` | Gauge | Total number of registered users |
| `hemmelig_visitors_unique_30d` | Gauge | Unique visitors in the last 30 days |
| `hemmelig_visitors_views_30d` | Gauge | Total page views in the last 30 days |
| `hemmelig_http_request_duration_seconds` | Histogram | Duration of HTTP requests in seconds |
### Default Node.js Metrics
The endpoint also exposes default Node.js runtime metrics including:
- `nodejs_heap_size_total_bytes` - Process heap size
- `nodejs_heap_size_used_bytes` - Process heap size used
- `nodejs_external_memory_bytes` - Node.js external memory
- `nodejs_eventloop_lag_seconds` - Event loop lag
- `nodejs_active_handles_total` - Number of active handles
- `nodejs_active_requests_total` - Number of active requests
- `process_cpu_user_seconds_total` - User CPU time spent
- `process_cpu_system_seconds_total` - System CPU time spent
- `process_start_time_seconds` - Process start time
- `process_resident_memory_bytes` - Resident memory size
## Prometheus Configuration
Add the following job to your `prometheus.yml`:
```yaml
scrape_configs:
- job_name: 'hemmelig'
scrape_interval: 30s
static_configs:
- targets: ['your-instance.com']
metrics_path: '/api/metrics'
scheme: https
# If using authentication:
authorization:
type: Bearer
credentials: 'YOUR_METRICS_SECRET'
```
## Grafana Dashboard
You can create a Grafana dashboard to visualize these metrics. Here's an example panel query for active secrets:
```promql
hemmelig_secrets_active_count
```
## Security Considerations
- Always use a strong, randomly generated secret for the metrics endpoint in production
- Consider using network-level restrictions (firewall, VPN) to limit access to the metrics endpoint
- The metrics endpoint does not expose any sensitive data (secret contents, user data, etc.)

77
docs/sdk.md Normal file
View File

@@ -0,0 +1,77 @@
# SDK Generation
> **Disclaimer:** The Hemmelig API is subject to change without notice. Generated SDKs may break with future updates. Use at your own risk.
Hemmelig exposes an OpenAPI 3.0 specification that can be used to generate client SDKs in various programming languages.
## OpenAPI Specification
The OpenAPI spec is available at:
- **Swagger UI:** `/api/docs` - Interactive API explorer
- **OpenAPI JSON:** `/api/openapi.json` - Raw specification
## Generating an SDK
We recommend using [OpenAPI Generator](https://openapi-generator.tech/) which supports 50+ languages.
### Installation
```bash
npm install -g @openapitools/openapi-generator-cli
```
### Generate SDK
```bash
# TypeScript
openapi-generator-cli generate \
-i https://your-instance.com/api/openapi.json \
-g typescript-axios \
-o ./hemmelig-sdk
# Python
openapi-generator-cli generate \
-i https://your-instance.com/api/openapi.json \
-g python \
-o ./hemmelig-sdk
# Go
openapi-generator-cli generate \
-i https://your-instance.com/api/openapi.json \
-g go \
-o ./hemmelig-sdk
```
View all available generators:
```bash
openapi-generator-cli list
```
## Authentication
The API supports two authentication methods:
### Bearer Token (API Key)
```typescript
const client = new HemmeligApi({
baseURL: 'https://your-instance.com/api',
headers: {
Authorization: 'Bearer hemmelig_your_api_key_here',
},
});
```
### Session Cookie
For browser-based applications, session cookies are automatically handled after authentication via `/auth` endpoints.
## Important: Client-Side Encryption
Generated SDKs handle API communication only. **You must implement client-side encryption** before sending secrets to the API.
Hemmelig uses AES-256-GCM encryption. See the [encryption documentation](./encryption.md) for implementation details.
The decryption key should be passed via URL fragments (`#decryptionKey=...`) which are never sent to the server.

147
docs/secret-request.md Normal file
View File

@@ -0,0 +1,147 @@
# Secret Requests
Secret Requests allow you to request secrets from others securely. Instead of asking someone to create a secret and send you the link, you create a request link that they can use to submit a secret directly to you.
## How It Works
1. **Create a Request** - You configure the secret settings (expiration, max views, etc.) and get a unique request link
2. **Share the Link** - Send the request link to the person who has the secret
3. **They Submit** - They enter their secret, which gets encrypted in their browser. They receive a decryption key which they must send back to you.
4. **View the Secret** - You use the secret URL from your dashboard combined with the decryption key to view the secret.
```
┌─────────────┐ ┌─────────────┐
│ Requester │ │ Creator │
│ (You) │ │ (Them) │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1. Create request │
│───────────────────> │
│ │
│ 2. Share request link │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ > │
│ │
│ │ 3. Submit secret
│ │ (encrypted)
│ │
│ 4. They send you the │
│ decryption key │
│< ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
│ 5. View secret from │
│ dashboard with key │
│ │
```
## Creating a Request
Navigate to **Dashboard → Secret Requests → Create New Request**.
### Required Fields
- **Title** - A descriptive title shown to the creator (e.g., "API credentials for Project X")
### Optional Fields
- **Description** - Additional context for the creator
- **Link Validity** - How long the request link remains active (1 hour to 30 days)
- **Secret Expiration** - How long the submitted secret lives (5 minutes to 28 days)
- **Max Views** - Number of times the secret can be viewed (1-9999)
- **IP Restriction** - Limit secret access to specific IP/CIDR
- **Prevent Burn** - Keep secret even after max views reached
- **Webhook URL** - Get notified when the secret is submitted
## Webhooks
When a secret is submitted, Hemmelig sends a POST request to your webhook URL:
```json
{
"event": "secret_request.fulfilled",
"timestamp": "2024-12-14T10:30:00.000Z",
"request": {
"id": "uuid",
"title": "API credentials",
"createdAt": "2024-12-14T10:00:00.000Z",
"fulfilledAt": "2024-12-14T10:30:00.000Z"
},
"secret": {
"id": "secret-uuid",
"maxViews": 1,
"expiresAt": "2024-12-15T10:30:00.000Z"
}
}
```
### Webhook Security
Webhooks are signed using HMAC-SHA256. Verify the signature to ensure authenticity:
```javascript
const crypto = require('crypto');
function verifyWebhook(payload, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedSig = crypto.createHmac('sha256', secret).update(signedPayload).digest('hex');
return `sha256=${expectedSig}` === signature;
}
// Headers to check:
// X-Hemmelig-Signature: sha256=<hex>
// X-Hemmelig-Timestamp: <unix-timestamp>
```
**Note:** The webhook secret is shown only once when creating the request. Store it securely.
## Security
- **Client-side encryption** - Secrets are encrypted in the creator's browser before transmission
- **Decryption key in URL fragment** - The `#decryptionKey=...` never reaches the server
- **Single-use tokens** - Request links use 256-bit cryptographically secure tokens
- **Timing-safe validation** - Prevents timing attacks on token verification
## API Usage
### Create a Request
```bash
curl -X POST https://your-instance/api/secret-requests \
-H "Authorization: Bearer hemmelig_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"title": "Database credentials",
"description": "Need the prod DB password",
"maxViews": 1,
"expiresIn": 86400,
"validFor": 604800
}'
```
### List Your Requests
```bash
curl https://your-instance/api/secret-requests \
-H "Authorization: Bearer hemmelig_your_api_key"
```
### Get Request Details
```bash
curl https://your-instance/api/secret-requests/{id} \
-H "Authorization: Bearer hemmelig_your_api_key"
```
### Cancel a Request
```bash
curl -X DELETE https://your-instance/api/secret-requests/{id} \
-H "Authorization: Bearer hemmelig_your_api_key"
```
## Limits
- **Secret size**: 1 MB maximum
- **Title size**: 1 KB maximum
- **Request validity**: 1 hour to 30 days
- **Secret expiration**: 5 minutes to 28 days

451
docs/social-login.md Normal file
View File

@@ -0,0 +1,451 @@
# Social Login Configuration
Hemmelig supports multiple social login providers. Users can configure any combination of providers via environment variables. Only providers with valid credentials will be shown on the login and registration pages.
## Required Configuration
Before setting up any social provider, you must set your base URL:
```bash
HEMMELIG_BASE_URL=https://your-domain.com
```
This is used to generate the correct OAuth callback URLs.
## Supported Providers
### Standard Providers
| Provider | Environment Variables |
| --------- | -------------------------------------------------------------------------------------------------------------- |
| GitHub | `HEMMELIG_AUTH_GITHUB_ID`, `HEMMELIG_AUTH_GITHUB_SECRET` |
| Google | `HEMMELIG_AUTH_GOOGLE_ID`, `HEMMELIG_AUTH_GOOGLE_SECRET` |
| Microsoft | `HEMMELIG_AUTH_MICROSOFT_ID`, `HEMMELIG_AUTH_MICROSOFT_SECRET`, `HEMMELIG_AUTH_MICROSOFT_TENANT_ID` (optional) |
| Discord | `HEMMELIG_AUTH_DISCORD_ID`, `HEMMELIG_AUTH_DISCORD_SECRET` |
| GitLab | `HEMMELIG_AUTH_GITLAB_ID`, `HEMMELIG_AUTH_GITLAB_SECRET`, `HEMMELIG_AUTH_GITLAB_ISSUER` (optional) |
| Apple | `HEMMELIG_AUTH_APPLE_ID`, `HEMMELIG_AUTH_APPLE_SECRET` |
| Twitter/X | `HEMMELIG_AUTH_TWITTER_ID`, `HEMMELIG_AUTH_TWITTER_SECRET` |
### Generic OAuth Providers
Hemmelig now supports any OAuth 2.0 / OpenID Connect provider through the generic OAuth configuration. This allows you to integrate with identity providers like:
- **Authentik**
- **Authelia**
- **Keycloak**
- **Zitadel**
- **Ory Hydra**
- **Auth0** (if not using the built-in provider)
- **Okta**
- Any other OAuth 2.0 / OpenID Connect compatible provider
Use the `HEMMELIG_AUTH_GENERIC_OAUTH` environment variable with a JSON array of provider configurations.
**Environment Variable**: `HEMMELIG_AUTH_GENERIC_OAUTH`
#### Generic OAuth Configuration Format
Each provider in the array must include:
| Field | Required | Description |
| ------------------ | -------- | -------------------------------------------------------------------------- |
| `providerId` | Yes | Unique identifier for the provider (used in callback URLs) |
| `clientId` | Yes | OAuth client ID from your identity provider |
| `clientSecret` | Yes | OAuth client secret from your identity provider |
| `discoveryUrl` | No\* | OpenID Connect discovery URL (e.g., `/.well-known/openid-configuration`) |
| `authorizationUrl` | No\* | OAuth authorization endpoint (required if no `discoveryUrl`) |
| `tokenUrl` | No\* | OAuth token endpoint (required if no `discoveryUrl`) |
| `userInfoUrl` | No\* | OAuth user info endpoint (required if no `discoveryUrl`) |
| `scopes` | No | Array of OAuth scopes (default: `["openid", "profile", "email"]`) |
| `pkce` | No | Enable PKCE (Proof Key for Code Exchange) - recommended for public clients |
\*You must provide either `discoveryUrl` OR all three of (`authorizationUrl`, `tokenUrl`, `userInfoUrl`).
#### Example: Authentik
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/application/o/hemmelig/.well-known/openid-configuration","clientId":"your-client-id","clientSecret":"your-client-secret","scopes":["openid","profile","email"]}]
```
#### Example: Authelia
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"authelia","discoveryUrl":"https://auth.example.com/.well-known/openid-configuration","clientId":"hemmelig","clientSecret":"your-client-secret","scopes":["openid","profile","email","groups"]}]
```
#### Example: Keycloak
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"keycloak","discoveryUrl":"https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration","clientId":"hemmelig","clientSecret":"your-client-secret","scopes":["openid","profile","email"]}]
```
#### Example: Multiple Generic Providers
You can configure multiple generic OAuth providers in the same JSON array:
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[
{
"providerId": "authentik",
"discoveryUrl": "https://auth.example.com/application/o/hemmelig/.well-known/openid-configuration",
"clientId": "client-id-1",
"clientSecret": "secret-1",
"scopes": ["openid", "profile", "email"]
},
{
"providerId": "keycloak",
"discoveryUrl": "https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration",
"clientId": "client-id-2",
"clientSecret": "secret-2"
}
]
```
#### Example: Manual URLs (without discovery)
If your provider doesn't support OpenID Connect discovery:
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"custom","authorizationUrl":"https://oauth.example.com/authorize","tokenUrl":"https://oauth.example.com/token","userInfoUrl":"https://oauth.example.com/userinfo","clientId":"your-client-id","clientSecret":"your-client-secret","scopes":["openid","profile","email"]}]
```
## Callback URLs
When configuring your OAuth applications, use these callback URLs:
### Standard Providers
| Provider | Callback URL |
| --------- | ----------------------------------------------------- |
| GitHub | `https://your-domain.com/api/auth/callback/github` |
| Google | `https://your-domain.com/api/auth/callback/google` |
| Microsoft | `https://your-domain.com/api/auth/callback/microsoft` |
| Discord | `https://your-domain.com/api/auth/callback/discord` |
| GitLab | `https://your-domain.com/api/auth/callback/gitlab` |
| Apple | `https://your-domain.com/api/auth/callback/apple` |
| Twitter/X | `https://your-domain.com/api/auth/callback/twitter` |
### Generic OAuth Providers
For generic OAuth providers, the callback URL format is:
```
https://your-domain.com/api/auth/oauth2/callback/{providerId}
```
Where `{providerId}` is the value you specified in the `providerId` field.
**Examples:**
- Authentik: `https://your-domain.com/api/auth/oauth2/callback/authentik`
- Authelia: `https://your-domain.com/api/auth/oauth2/callback/authelia`
- Keycloak: `https://your-domain.com/api/auth/oauth2/callback/keycloak`
## Configuration
Add the environment variables for the providers you want to enable. Both `_ID` and `_SECRET` must be set for a standard provider to be enabled.
### Example: Docker Compose
```yaml
services:
hemmelig:
image: hemmelig/hemmelig:latest
environment:
# Required: Base URL for OAuth callbacks
- HEMMELIG_BASE_URL=https://your-domain.com
# Standard providers (GitHub example)
- HEMMELIG_AUTH_GITHUB_ID=your-github-client-id
- HEMMELIG_AUTH_GITHUB_SECRET=your-github-client-secret
# Generic OAuth provider (Authentik example)
- HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/application/o/hemmelig/.well-known/openid-configuration","clientId":"your-client-id","clientSecret":"your-client-secret","scopes":["openid","profile","email"]}]
```
### Example: Environment File (.env)
```bash
# Required: Base URL for OAuth callbacks
HEMMELIG_BASE_URL=https://your-domain.com
# GitHub OAuth
HEMMELIG_AUTH_GITHUB_ID=your-github-client-id
HEMMELIG_AUTH_GITHUB_SECRET=your-github-client-secret
# Google OAuth
HEMMELIG_AUTH_GOOGLE_ID=your-google-client-id
HEMMELIG_AUTH_GOOGLE_SECRET=your-google-client-secret
# Microsoft OAuth (Azure AD)
HEMMELIG_AUTH_MICROSOFT_ID=your-microsoft-client-id
HEMMELIG_AUTH_MICROSOFT_SECRET=your-microsoft-client-secret
HEMMELIG_AUTH_MICROSOFT_TENANT_ID=your-tenant-id # Optional, defaults to "common"
# Discord OAuth
HEMMELIG_AUTH_DISCORD_ID=your-discord-client-id
HEMMELIG_AUTH_DISCORD_SECRET=your-discord-client-secret
# GitLab OAuth
HEMMELIG_AUTH_GITLAB_ID=your-gitlab-client-id
HEMMELIG_AUTH_GITLAB_SECRET=your-gitlab-client-secret
HEMMELIG_AUTH_GITLAB_ISSUER=https://gitlab.example.com # Optional, for self-hosted GitLab
# Apple OAuth
HEMMELIG_AUTH_APPLE_ID=your-apple-client-id
HEMMELIG_AUTH_APPLE_SECRET=your-apple-client-secret
# Twitter/X OAuth
HEMMELIG_AUTH_TWITTER_ID=your-twitter-client-id
HEMMELIG_AUTH_TWITTER_SECRET=your-twitter-client-secret
# Generic OAuth (supports Authentik, Authelia, Keycloak, etc.)
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/application/o/hemmelig/.well-known/openid-configuration","clientId":"your-client-id","clientSecret":"your-client-secret","scopes":["openid","profile","email"]}]
```
## Setting Up OAuth Applications
### Standard Providers
#### GitHub
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
2. Click "New OAuth App"
3. Set the callback URL to: `https://your-domain.com/api/auth/callback/github`
4. Copy the Client ID and Client Secret
#### Google
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
2. Create a new OAuth 2.0 Client ID
3. Set the authorized redirect URI to: `https://your-domain.com/api/auth/callback/google`
4. Copy the Client ID and Client Secret
#### Microsoft (Azure AD)
1. Go to [Azure Portal](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade)
2. Register a new application
3. Add a redirect URI: `https://your-domain.com/api/auth/callback/microsoft`
4. Create a client secret under "Certificates & secrets"
5. Copy the Application (client) ID and the client secret value
6. Optionally set the Tenant ID for single-tenant apps
#### Discord
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
2. Create a new application
3. Go to OAuth2 settings
4. Add redirect URL: `https://your-domain.com/api/auth/callback/discord`
5. Copy the Client ID and Client Secret
#### GitLab
1. Go to GitLab User Settings > Applications
2. Create a new application
3. Set the redirect URI to: `https://your-domain.com/api/auth/callback/gitlab`
4. Select the `read_user` scope
5. Copy the Application ID and Secret
**Self-hosted GitLab:** If you're using a self-hosted GitLab instance, set the `HEMMELIG_AUTH_GITLAB_ISSUER` environment variable to your GitLab instance URL (e.g., `https://gitlab.example.com`). Without this, GitLab.com is used by default.
#### Apple
1. Go to [Apple Developer Portal](https://developer.apple.com/account/resources/identifiers/list/serviceId)
2. Create a Services ID
3. Configure Sign in with Apple, add your domain and return URL: `https://your-domain.com/api/auth/callback/apple`
4. Create a key for Sign in with Apple
5. Use the Services ID as Client ID and generate the client secret from the key
#### Twitter/X
1. Go to [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard)
2. Create a new project and app
3. Enable OAuth 2.0
4. Set the callback URL to: `https://your-domain.com/api/auth/callback/twitter`
5. Copy the Client ID and Client Secret
### Generic OAuth Providers
#### Authentik
1. Log into your Authentik instance as an admin
2. Go to **Applications** > **Providers** > **Create**
3. Select **OAuth2/OpenID Provider**
4. Configure the provider:
- **Name**: Hemmelig
- **Authorization flow**: Select your flow (e.g., default-authentication-flow)
- **Redirect URIs**: `https://your-domain.com/api/auth/oauth2/callback/authentik`
- **Client type**: Confidential
- **Scopes**: `openid`, `profile`, `email`
5. Save and copy the **Client ID** and **Client Secret**
6. Create an application and bind it to this provider
7. Find your discovery URL (usually `https://auth.example.com/application/o/hemmelig/.well-known/openid-configuration`)
Example environment variable:
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/application/o/hemmelig/.well-known/openid-configuration","clientId":"<client-id>","clientSecret":"<client-secret>","scopes":["openid","profile","email"]}]
```
#### Authelia
1. Edit your Authelia configuration file (`configuration.yml`)
2. Add Hemmelig as a client under `identity_providers.oidc.clients`:
```yaml
clients:
- id: hemmelig
description: Hemmelig Secret Sharing
secret: <generate-a-secure-secret>
redirect_uris:
- https://your-domain.com/api/auth/oauth2/callback/authelia
scopes:
- openid
- profile
- email
- groups
```
3. Restart Authelia
4. Your discovery URL will be: `https://auth.example.com/.well-known/openid-configuration`
Example environment variable:
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"authelia","discoveryUrl":"https://auth.example.com/.well-known/openid-configuration","clientId":"hemmelig","clientSecret":"<client-secret>","scopes":["openid","profile","email"]}]
```
#### Keycloak
1. Log into your Keycloak admin console
2. Select your realm (or create a new one)
3. Go to **Clients** > **Create client**
4. Configure the client:
- **Client type**: OpenID Connect
- **Client ID**: `hemmelig`
5. On the next screen:
- **Client authentication**: ON
- **Valid redirect URIs**: `https://your-domain.com/api/auth/oauth2/callback/keycloak`
- **Web origins**: `https://your-domain.com`
6. Go to the **Credentials** tab and copy the **Client Secret**
7. Your discovery URL will be: `https://keycloak.example.com/realms/{realm-name}/.well-known/openid-configuration`
Example environment variable:
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"keycloak","discoveryUrl":"https://keycloak.example.com/realms/myrealm/.well-known/openid-configuration","clientId":"hemmelig","clientSecret":"<client-secret>"}]
```
#### Zitadel
1. Log into your Zitadel instance
2. Go to your project (or create a new one)
3. Create a new application:
- **Type**: Web
- **Authentication method**: PKCE or Code
4. Configure:
- **Redirect URIs**: `https://your-domain.com/api/auth/oauth2/callback/zitadel`
- **Post logout redirect URIs**: `https://your-domain.com`
5. Copy the **Client ID** and **Client Secret** (if using Code flow)
6. Your discovery URL: `https://<instance>.zitadel.cloud/.well-known/openid-configuration`
Example environment variable:
```bash
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"zitadel","discoveryUrl":"https://instance.zitadel.cloud/.well-known/openid-configuration","clientId":"<client-id>","clientSecret":"<client-secret>","scopes":["openid","profile","email"],"pkce":true}]
```
## How It Works
1. On server startup, the application reads all `HEMMELIG_AUTH_*` environment variables
2. Only providers with both `_ID` and `_SECRET` set are enabled
3. The frontend fetches the list of enabled providers from `/api/config/social-providers`
4. Login and registration pages dynamically show buttons only for enabled providers
5. Each provider button uses the correct branded icon and colors
6. The callback URL is built using `HEMMELIG_BASE_URL` + `/api/auth/callback/{provider}`
## All Environment Variables
```bash
# Required for OAuth
HEMMELIG_BASE_URL=https://your-domain.com
# GitHub
HEMMELIG_AUTH_GITHUB_ID=
HEMMELIG_AUTH_GITHUB_SECRET=
# Google
HEMMELIG_AUTH_GOOGLE_ID=
HEMMELIG_AUTH_GOOGLE_SECRET=
# Microsoft (Azure AD)
HEMMELIG_AUTH_MICROSOFT_ID=
HEMMELIG_AUTH_MICROSOFT_SECRET=
HEMMELIG_AUTH_MICROSOFT_TENANT_ID= # Optional
# Discord
HEMMELIG_AUTH_DISCORD_ID=
HEMMELIG_AUTH_DISCORD_SECRET=
# GitLab
HEMMELIG_AUTH_GITLAB_ID=
HEMMELIG_AUTH_GITLAB_SECRET=
HEMMELIG_AUTH_GITLAB_ISSUER= # Optional, for self-hosted GitLab (e.g., https://gitlab.example.com)
# Apple
HEMMELIG_AUTH_APPLE_ID=
HEMMELIG_AUTH_APPLE_SECRET=
# Twitter/X
HEMMELIG_AUTH_TWITTER_ID=
HEMMELIG_AUTH_TWITTER_SECRET=
# Generic OAuth (JSON array - supports any OAuth 2.0 / OIDC provider)
HEMMELIG_AUTH_GENERIC_OAUTH=[{"providerId":"authentik","discoveryUrl":"https://auth.example.com/.well-known/openid-configuration","clientId":"client-id","clientSecret":"client-secret","scopes":["openid","profile","email"]}]
```
## Troubleshooting
### Provider not showing up
**Standard providers:**
- Ensure both `_ID` and `_SECRET` environment variables are set
- Restart the server after adding environment variables
- Check server logs for any configuration errors
**Generic OAuth providers:**
- Verify the JSON in `HEMMELIG_AUTH_GENERIC_OAUTH` is valid
- Check that each provider has `providerId`, `clientId`, and `clientSecret`
- Verify you have either `discoveryUrl` OR all three URLs (`authorizationUrl`, `tokenUrl`, `userInfoUrl`)
- Check server logs for parsing errors
### OAuth callback errors
**Standard providers:**
- Verify the callback URL in your OAuth app settings matches exactly
- Format: `https://your-domain.com/api/auth/callback/{provider}`
**Generic OAuth providers:**
- Callback URL format: `https://your-domain.com/api/auth/oauth2/callback/{providerId}`
- Ensure the `providerId` in your config matches the one in your identity provider settings
**Common issues:**
- Ensure `HEMMELIG_BASE_URL` is set correctly (no trailing slash)
- Ensure your domain is using HTTPS in production
- Check that the client ID and secret are correct (no extra spaces)
### "Access Denied" errors
- Verify the OAuth app has the correct permissions/scopes
- For Microsoft, ensure the app is configured for the correct account types
- For Apple, ensure the Services ID is correctly configured
- For generic OAuth: Check that the requested scopes are allowed by your provider
### Discovery URL errors (Generic OAuth)
- Verify the discovery URL is accessible: `curl https://your-auth-provider/.well-known/openid-configuration`
- Ensure your Hemmelig instance can reach the discovery URL (check firewall rules)
- Try using manual URLs (`authorizationUrl`, `tokenUrl`, `userInfoUrl`) instead if discovery is not supported

33
docs/upgrade.md Normal file
View File

@@ -0,0 +1,33 @@
# Upgrading from v6 to v7
## ⚠️ Breaking Changes - Fresh Start Required
**v7 is a complete rewrite and is NOT backwards compatible with v6.** Due to fundamental changes in the encryption and password hashing algorithms, migration of existing data is not possible.
### What Changed
| Component | v6 | v7 |
| -------------------- | ------------------- | ----------------------- |
| Encryption Algorithm | Different algorithm | AES-256-GCM with PBKDF2 |
| Password Hashing | Different algorithm | Updated secure hashing |
| Database Schema | Previous schema | New schema structure |
### Why Migration Is Not Possible
1. **Encryption Algorithm Change:** Secrets encrypted with the v6 algorithm cannot be decrypted with v7's implementation. Since the server never stores decryption keys (zero-knowledge architecture), there is no way to re-encrypt existing secrets.
2. **Password Algorithm Change:** User passwords are hashed differently in v7. Existing password hashes from v6 cannot be verified or converted.
### Upgrade Steps
1. **Backup v6 data** (for reference only - it cannot be migrated)
2. **Stop your v6 instance**
3. **Deploy v7 with a fresh database** - See [Docker Guide](docker.md) for deployment instructions
4. **Re-create user accounts**
5. **Inform users** that existing secrets are no longer accessible
### Important Notes
- No migration scripts are provided or planned
- Users must register new accounts in v7
- Consider running v6 in read-only mode temporarily to allow users to retrieve unexpired secrets before shutdown

230
docs/webhook.md Normal file
View File

@@ -0,0 +1,230 @@
# Webhook Notifications
Hemmelig can send HTTP POST requests to your webhook URL when secrets are viewed or burned. This allows you to integrate with external services like Slack, Discord, monitoring systems, or custom applications.
## Configuration
Configure webhooks in the admin dashboard under **Instance Settings → Webhooks**.
| Setting | Description |
| ------------------- | ------------------------------------------ |
| **Enable Webhooks** | Turn webhook notifications on/off |
| **Webhook URL** | The endpoint where payloads are sent |
| **Webhook Secret** | Secret key for HMAC-SHA256 payload signing |
| **Secret Viewed** | Send webhook when a secret is viewed |
| **Secret Burned** | Send webhook when a secret is deleted |
## Webhook Payload
Webhooks are sent as HTTP POST requests with a JSON body:
### Secret Events
```json
{
"event": "secret.viewed",
"timestamp": "2024-12-04T10:30:00.000Z",
"data": {
"secretId": "abc123-def456",
"hasPassword": true,
"hasIpRestriction": false,
"viewsRemaining": 2
}
}
```
### API Key Events
```json
{
"event": "apikey.created",
"timestamp": "2024-12-04T10:30:00.000Z",
"data": {
"apiKeyId": "key-uuid-here",
"name": "My Integration",
"expiresAt": "2025-12-04T10:30:00.000Z",
"userId": "user-uuid-here"
}
}
```
### Event Types
| Event | Description |
| ---------------- | -------------------------------------------------- |
| `secret.viewed` | A secret was successfully viewed |
| `secret.burned` | A secret was deleted (manually or after last view) |
| `apikey.created` | A new API key was created |
### Headers
| Header | Description |
| ---------------------- | ------------------------------------------------------------------ |
| `Content-Type` | `application/json` |
| `X-Hemmelig-Event` | Event type (`secret.viewed`, `secret.burned`, or `apikey.created`) |
| `X-Hemmelig-Signature` | HMAC-SHA256 signature (if secret configured) |
## Verifying Webhook Signatures
If you configure a webhook secret, Hemmelig signs each payload using HMAC-SHA256. The signature is sent in the `X-Hemmelig-Signature` header as `sha256=<hex>`.
**Always verify signatures** to ensure webhooks are authentic and haven't been tampered with.
### Node.js Example
```javascript
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Express.js middleware
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-hemmelig-signature'];
const payload = req.body.toString();
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
console.log(`Received ${event.event} for secret ${event.data.secretId}`);
res.status(200).send('OK');
});
```
### Python Example
```python
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Hemmelig-Signature')
payload = request.get_data()
if not verify_webhook(payload, signature, os.environ['WEBHOOK_SECRET']):
return 'Invalid signature', 401
event = request.get_json()
print(f"Received {event['event']} for secret {event['data']['secretId']}")
return 'OK', 200
```
### Go Example
```go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhook(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-Hemmelig-Signature")
payload, _ := io.ReadAll(r.Body)
if !verifyWebhook(payload, signature, os.Getenv("WEBHOOK_SECRET")) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook...
w.WriteHeader(http.StatusOK)
}
```
## Integration Examples
### Slack Notification
Send a message to Slack when a secret is viewed:
```javascript
app.post('/webhook', async (req, res) => {
const event = req.body;
if (event.event === 'secret.viewed') {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🔓 Secret ${event.data.secretId} was viewed. ${event.data.viewsRemaining} views remaining.`,
}),
});
}
res.status(200).send('OK');
});
```
### Discord Notification
```javascript
if (event.event === 'secret.burned') {
await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
embeds: [
{
title: '🔥 Secret Burned',
description: `Secret \`${event.data.secretId}\` has been permanently deleted.`,
color: 0xff6b6b,
timestamp: event.timestamp,
},
],
}),
});
}
```
## Best Practices
1. **Always use HTTPS** for your webhook endpoint
2. **Always verify signatures** to prevent spoofed requests
3. **Respond quickly** (< 5 seconds) to avoid timeouts
4. **Use a queue** for heavy processing to avoid blocking
5. **Log webhook events** for debugging and audit trails
6. **Handle retries** gracefully (webhooks are fire-and-forget, no retries)
## Troubleshooting
**Webhooks not being received?**
- Check that webhooks are enabled in Instance Settings
- Verify your webhook URL is accessible from the Hemmelig server
- Check server logs for any error messages
**Invalid signature errors?**
- Ensure you're using the raw request body (not parsed JSON) for verification
- Check that your webhook secret matches exactly
- Make sure you're comparing the full signature including the `sha256=` prefix