Skip to content

stackopshq/ghostbit

Repository files navigation

Ghostbit

Ghostbit

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

Python FastAPI License PyPI


How it works

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

Features

  • 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 β€” gbit command, pipe anything from your terminal
  • REST API β€” full API for automation and integrations
  • SQLite / Redis β€” swap storage backends with a single env var

CLI

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 | source

Self-hosting

Docker

docker pull stackopshq/ghostbit          # Docker Hub
docker pull ghcr.io/stackopshq/ghostbit  # GHCR
git clone https://github.com/stackopshq/ghostbit
cd ghostbit
cp .env.example .env
docker compose up -d

With Redis

STORAGE_BACKEND=redis docker compose --profile redis up -d

# With a password
STORAGE_BACKEND=redis REDIS_PASSWORD=mysecret docker compose --profile redis up -d

Podman Quadlet

Create /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 ghostbit

For 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.


Encrypted backups

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.sh

Recurring 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.timer

Restore:

age --decrypt -i ~/.config/age/keys.txt ghostbit-2026-05-24T03-17-00Z.jsonl.age \
  | python -m app.admin import

app.admin import --overwrite replaces existing IDs; without it, conflicts are skipped (safe re-runs).


Environment variables

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

API

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.


Local development

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 8000

Open http://localhost:8000. SQLite is used by default, no external service required.

Running tests

pip install -r requirements-dev.txt
pytest tests/ -v

Pre-commit hooks

Install once per clone to run ruff, gitleaks and a few hygiene checks on every commit:

pip install pre-commit
pre-commit install

The same checks run in CI β€” installing the hook just gives you the feedback locally before the push.


Security

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 #fragment is 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.


Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Commit with clear messages (git commit -m "feat: add X")
  4. 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)

License

MIT

About

Ghostbit β€” Secure, ephemeral, and encrypted code sharing for developers. Share snippets that vanish after use. πŸ‘» πŸ’»

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors