Self-hosted, end-to-end encrypted paste service.
The server stores ciphertext only β it can never read your content.
A modern, privacy-first alternative to Pastebin and PrivateBin.
Documentation Β Β·Β Demo Β Β·Β CLI on PyPI Β Β·Β Self-hosting
Ghostbit encrypts your content in the browser using the Web Crypto API before sending anything to the server. The decryption key lives exclusively in the URL fragment β it is never transmitted over the network.
https://paste.example.com/aB3kZx9m#KEY~DELETE_TOKEN
β
never sent to the server
| Paste type | Key source | Where the key lives |
|---|---|---|
| No password | crypto.subtle.generateKey() |
URL #fragment |
| With password | PBKDF2-SHA256 (600k iter) | User's memory |
- True E2E encryption β AES-256-GCM, server sees ciphertext only
- Burn after read β deleted permanently after the first view
- Max views β auto-deleted after N reads
- Expiration β from 5 minutes to 1 year
- Password protection β client-side key derivation, password never leaves the browser
- Webhook β POST notification on each read
- Language detection β auto-detected from content or file extension
- Markdown preview β rendered in-browser
- CLI β
gbitcommand, pipe anything from your terminal - REST API β full API for automation and integrations
- SQLite / Redis β swap storage backends with a single env var
pip install ghostbit-cli# Paste from stdin
cat main.py | gbit
# Paste a file (language auto-detected)
gbit secrets.env --burn --expires 3600
# Password-protected (secure prompt)
echo "db_pass=s3cr3t" | gbit -p
# Scripting
URL=$(cat deploy.sh | gbit --quiet)
# Point to your instance
gbit config set server https://paste.example.com
# Shell completion (bash / zsh / fish)
eval "$(gbit completion bash)"
eval "$(gbit completion zsh)"
gbit completion fish | sourcedocker pull stackopshq/ghostbit # Docker Hub
docker pull ghcr.io/stackopshq/ghostbit # GHCRgit clone https://github.com/stackopshq/ghostbit
cd ghostbit
cp .env.example .env
docker compose up -dSTORAGE_BACKEND=redis docker compose --profile redis up -d
# With a password
STORAGE_BACKEND=redis REDIS_PASSWORD=mysecret docker compose --profile redis up -dCreate /etc/containers/systemd/ghostbit.container (system-wide) or ~/.config/containers/systemd/ghostbit.container (rootless):
[Unit]
Description=Ghostbit paste service
After=network-online.target
[Container]
Image=ghcr.io/stackopshq/ghostbit:latest
PublishPort=8000:8000
Volume=ghostbit_data:/data
Environment=STORAGE_BACKEND=sqlite
Environment=SQLITE_PATH=/data/ghostbit.db
Environment=MAX_PASTE_SIZE=524288
Environment=PORT=8000
HealthCmd=wget -qO- http://127.0.0.1:8000/healthz || exit 1
HealthInterval=30s
HealthTimeout=5s
HealthStartPeriod=15s
HealthRetries=3
[Service]
Restart=always
[Install]
WantedBy=default.target# Reload systemd and start
systemctl --user daemon-reload
systemctl --user enable --now ghostbitFor Redis, add a ghostbit-redis.container alongside and use After=ghostbit-redis.service + Environment=STORAGE_BACKEND=redis + Environment=REDIS_URL=redis://ghostbit-redis:6379. Podman Quadlet handles the pod networking automatically.
scripts/backup.sh streams python -m app.admin export through age and writes one timestamped .jsonl.age file per run. The plaintext export never touches disk β a stolen backup file is useless without the age private key.
One-shot:
BACKUP_DIR=/var/backups/ghostbit \
AGE_RECIPIENT="age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
scripts/backup.shRecurring via systemd β copy the two unit templates and adjust paths + recipient:
sudo cp scripts/ghostbit-backup.service /etc/systemd/system/
sudo cp scripts/ghostbit-backup.timer /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ghostbit-backup.timer
sudo systemctl list-timers ghostbit-backup.timerRestore:
age --decrypt -i ~/.config/age/keys.txt ghostbit-2026-05-24T03-17-00Z.jsonl.age \
| python -m app.admin importapp.admin import --overwrite replaces existing IDs; without it, conflicts are skipped (safe re-runs).
| Variable | Default | Description |
|---|---|---|
STORAGE_BACKEND |
sqlite |
sqlite or redis |
SQLITE_PATH |
./ghostbit.db |
SQLite file path (Docker overrides to /data/ghostbit.db) |
SQLITE_POOL_SIZE |
5 |
Pooled SQLite connections (WAL enables parallel readers) |
REDIS_URL |
redis://localhost:6379 |
Redis connection URL |
REDIS_PASSWORD |
β | Redis password (injected into REDIS_URL automatically) |
MAX_PASTE_SIZE |
524288 |
Max paste size in bytes (512 KB) |
PORT |
8000 |
Server port |
RATE_LIMIT_CREATE |
30/minute |
Rate limit for paste creation |
RATE_LIMIT_VIEW |
120/minute |
Rate limit for paste viewing |
TRUST_PROXY_HEADERS |
false |
Use rightmost X-Forwarded-For for rate limiting (enable only behind a trusted proxy) |
BASE_URL |
β | Public base URL (e.g. https://paste.example.com) for the absolute links in social-preview meta tags. Derived from the request when unset. |
WEBHOOK_SECRET |
β | HMAC-SHA256 secret for signing webhook payloads |
All content is encrypted client-side β the API only handles ciphertext. Interactive docs are available at /docs (Swagger UI) and /redoc (ReDoc). Operators also get /healthz (liveness β always 200 if the process is alive), /readyz (readiness β 503 if the storage backend doesn't answer) and /metrics (Prometheus exposition).
# Create (content must be pre-encrypted β use the CLI or e2e.js)
curl -X POST https://paste.example.com/api/v1/pastes \
-H "Content-Type: application/json" \
-d '{"content":"<base64 ciphertext>","nonce":"<base64 nonce>","language":"python"}'
# Retrieve (returns ciphertext β client decrypts)
curl https://paste.example.com/api/v1/pastes/{id}
# Delete
curl -X DELETE https://paste.example.com/api/v1/pastes/{id} \
-H "X-Delete-Token: <token>"
# Detect language (plaintext)
curl -X POST https://paste.example.com/api/v1/detect \
-H "Content-Type: application/json" \
-d '{"content":"def hello():\n print(42)"}'Interactive Swagger UI: /docs β ReDoc: /redoc.
git clone https://github.com/stackopshq/ghostbit
cd ghostbit
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
uvicorn app.main:app --reload --port 8000Open http://localhost:8000. SQLite is used by default, no external service required.
pip install -r requirements-dev.txt
pytest tests/ -vInstall once per clone to run ruff, gitleaks and a few hygiene checks on every commit:
pip install pre-commit
pre-commit installThe same checks run in CI β installing the hook just gives you the feedback locally before the push.
Ghostbit follows a zero-knowledge architecture:
| Server sees | Server cannot see | |
|---|---|---|
| Paste content | AES-256-GCM ciphertext | Plaintext |
| Encryption key | Never (stays in URL #fragment) |
β |
| Password | Never (PBKDF2 runs in browser/CLI) | β |
| Delete token | SHA-256 hash only | Plaintext token |
| Metadata | Language, timestamps, view count | β |
- The URL
#fragmentis never sent to the server by any browser. - A compromised server cannot decrypt any paste β past or future.
- SSRF protection blocks webhooks to private/internal networks.
- Rate limiting protects against abuse on all endpoints.
If you discover a security vulnerability, please report it responsibly via GitHub Security Advisories.
- Fork the repository
- Create a feature branch (
git checkout -b feat/my-feature) - Commit with clear messages (
git commit -m "feat: add X") - Push and open a Pull Request
Please ensure:
- All existing tests pass (
pytest tests/ -v) - New features include tests when applicable
- Code follows the existing style (no linter enforced, just be consistent)
MIT
