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:
106
docs/api.md
Normal file
106
docs/api.md
Normal 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
374
docs/cli.md
Normal 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
267
docs/docker.md
Normal 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
178
docs/e2e.md
Normal 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
123
docs/encryption.md
Normal 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
186
docs/env.md
Normal 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
95
docs/health.md
Normal 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
141
docs/helm-oauth.md
Normal 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
205
docs/helm.md
Normal 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
299
docs/managed.md
Normal 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
85
docs/metrics.md
Normal 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
77
docs/sdk.md
Normal 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
147
docs/secret-request.md
Normal 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
451
docs/social-login.md
Normal 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
33
docs/upgrade.md
Normal 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
230
docs/webhook.md
Normal 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
|
||||
Reference in New Issue
Block a user