MCP Hub
Back to servers

zKettle

Self-hosted, zero-knowledge encrypted, self-destructing secrets for secure agent-to-agent coordination

glama
Stars
3
Updated
Feb 24, 2026
Validated
Feb 26, 2026

zKettle

Self-hosted zero-knowledge expiring secrets. Encrypt locally, store ciphertext on the server, share a URL with the decryption key in the fragment. The server never sees the plaintext or the key.

Installation

Go install (requires Go 1.25+)

go install github.com/benderterminal/zkettle@latest

This installs to $GOPATH/bin (typically ~/go/bin). Make sure it's in your PATH: export PATH="$HOME/go/bin:$PATH"

Binary download

curl -fsSL https://github.com/benderterminal/zkettle/releases/latest/download/zkettle-$(uname -s | tr A-Z a-z)-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') -o /usr/local/bin/zkettle && chmod +x /usr/local/bin/zkettle

From source

git clone https://github.com/benderterminal/zkettle.git && cd zkettle && make install

Via AI agent

Paste this prompt into Claude Code, Cursor, or any MCP-compatible agent:

I want to set up zKettle — a self-hosted zero-knowledge secret sharing tool. Install it with go install github.com/benderterminal/zkettle@latest, then read the MCP setup instructions in the README at https://github.com/benderterminal/zkettle. When configuring the MCP server, use the absolute path to the installed binary (find it with which zkettle or check ~/go/bin/). Once configured, test the full workflow using the CLI: create a secret, read it back, and revoke it. Note that MCP servers are loaded at startup — the new tools won't be available until the next terminal session.

Quick Start

# Start the server with a Cloudflare tunnel (instant public URL)
zkettle serve --tunnel

# Or start locally
zkettle serve --port 3000

# Create a secret (in another terminal)
echo "my secret password" | zkettle create --views 1 --minutes 60
# → http://localhost:3000/s/abc123#key

# Read a secret
zkettle read "http://localhost:3000/s/abc123#key"
# → my secret password (use -c for clipboard, -o <file> for file output)

# Revoke a secret
zkettle revoke --server http://localhost:3000 --token <delete-token> <id>

Open the URL in a browser to reveal the secret via the web viewer.

Docker Deployment

# Build and run with Docker Compose
docker compose up -d

# Or build and run manually
docker build -t zkettle .
docker run -d -p 3000:3000 -v zkettle-data:/data zkettle

The container listens on port 3000 and stores data in /data. Configure with environment variables (see Configuration Reference).

Production Deployment

With TLS (direct)

zkettle serve --host 0.0.0.0 --tls-cert /path/to/cert.pem --tls-key /path/to/key.pem

With a reverse proxy (recommended)

Run zkettle behind Caddy, Nginx, or Traefik for automatic TLS:

zkettle serve --host 127.0.0.1 --trust-proxy

Enable --trust-proxy so zkettle reads the real client IP from X-Forwarded-For headers.

Systemd

Copy the service template and enable it:

sudo cp contrib/zkettle.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now zkettle

The service template uses DynamicUser=yes with ReadWritePaths=/var/lib/zkettle, so systemd manages the data directory automatically.

Configure via environment file at /etc/zkettle/env:

ZKETTLE_PORT=3000
ZKETTLE_HOST=0.0.0.0
ZKETTLE_ADMIN_TOKEN=your-secret-token
ZKETTLE_TRUST_PROXY=true

Backups

The database is a single SQLite file at <data-dir>/zkettle.db. Back it up with:

sqlite3 /var/lib/zkettle/zkettle.db ".backup /backups/zkettle-$(date +%Y%m%d).db"

Admin API

Enable the admin endpoint by setting an admin token via environment variable:

export ZKETTLE_ADMIN_TOKEN=my-secret-admin-token
zkettle serve

Note: Passing --admin-token on the command line exposes the token in process listings (ps, /proc/*/cmdline). Prefer the environment variable or config file.

List active secrets

# Via CLI
zkettle list --server http://localhost:3000 --admin-token my-secret-admin-token

# Via API
curl -H "Authorization: Bearer my-secret-admin-token" http://localhost:3000/api/admin/secrets

Returns metadata only (ID, views remaining, timestamps). No encrypted content or decryption keys are ever exposed.

GET /api/admin/secrets

Returns 404 when no admin token is configured (endpoint disabled). Requires Authorization: Bearer <token> header.

Response (200):

[
  {
    "id": "abc123...",
    "views_left": 2,
    "expires_at": "2024-01-02T03:04:05Z",
    "created_at": "2024-01-01T00:00:00Z"
  }
]

Metrics

Enable the /metrics endpoint with the --metrics flag:

export ZKETTLE_ADMIN_TOKEN=my-secret-admin-token
zkettle serve --metrics

The /metrics endpoint requires the admin token (Authorization: Bearer <token> header). Returns 404 when no admin token is configured.

Returns JSON metrics at GET /metrics:

{
  "zkettle_secrets_active": 5
}

MCP Setup

zKettle includes an MCP server for use with Claude Desktop, Claude Code, or any MCP-compatible agent.

Important: Use the absolute path to the zkettle binary. Many MCP clients do not inherit your shell's PATH, so a bare zkettle command will silently fail to start.

Add to your MCP client's configuration file:

{
  "mcpServers": {
    "zkettle": {
      "command": "/absolute/path/to/zkettle",
      "args": ["mcp", "--port", "3001", "--tunnel"]
    }
  }
}

Use --tunnel for public shareable URLs via Cloudflare Quick Tunnel (no account required). Omit it for local-only access. Use --base-url https://your-domain.com if you have a custom domain.

Claude Code shortcut:

claude mcp add -s user zkettle -- /absolute/path/to/zkettle mcp --port 3001 --tunnel

The MCP server starts an HTTP backend on the specified port and communicates with the agent over stdio. All encryption and decryption happens locally — in the browser (Web Crypto API), CLI, or MCP server process. The zKettle HTTP server never sees plaintext.

Available tools:

ToolDescription
create_secretEncrypt and store a secret (content or file input), returns an expiring URL
read_secretRetrieve and decrypt a secret (file or clipboard output to avoid context exposure)
list_secretsList active secrets (metadata only)
revoke_secretDelete a secret by ID
generate_secretGenerate a random secret, optionally store it (create=true)

CLI Reference

zkettle serve [options]     Start the HTTP server
  --port 3000               HTTP port
  --host 127.0.0.1          Listen address (use 0.0.0.0 for all interfaces)
  --data ./data             Data directory for SQLite database
  --base-url ""             Base URL for generated links (default: http://localhost:{port})
  --cors-origins ""         Comma-separated allowed CORS origins
  --tunnel                  Expose server via Cloudflare Quick Tunnel
  --trust-proxy             Trust X-Forwarded-For headers (behind a reverse proxy)
  --log-format ""           Log format: json or text (defaults to text)
  --tls-cert ""             TLS certificate file path
  --tls-key ""              TLS private key file path
  --admin-token ""          Admin API bearer token (enables GET /api/admin/secrets)
  --max-secret-size 0       Max encrypted secret size in bytes (0 = 500KB)
  --metrics                 Enable /metrics endpoint

zkettle create [options]    Encrypt and store a secret (reads from stdin)
  --views 1                 Max views before auto-delete
  --minutes 1440            Minutes until expiry (default 24h)
  --server http://localhost:3000   Server URL
  --json                    Output JSON to stdout
  --quiet, -q               Suppress stderr output

zkettle read [options] <url>   Retrieve and decrypt a secret (quote the URL)
  --clipboard, -c             Copy to clipboard instead of printing to stdout
  --file, -o <path>           Write to file (0600 permissions) instead of stdout

zkettle revoke [options] <id>   Delete a secret
  --server http://localhost:3000   Server URL
  --token ""                Delete token (returned by create, or set ZKETTLE_DELETE_TOKEN)

zkettle list [options]      List active secrets (requires admin token)
  --server http://localhost:3000   Server URL
  --admin-token ""          Admin API bearer token
  --json                    Output raw JSON

zkettle generate [options]  Generate a cryptographically random secret
  --length 32               Length in characters
  --charset alphanumeric    Character set: alphanumeric, symbols, hex, base64url

zkettle mcp [options]       Start MCP server on stdio with HTTP backend
  --port 3000               HTTP port for API
  --host 127.0.0.1          Listen address
  --data ./data             Data directory
  --base-url ""             Base URL for generated links
  --tunnel                  Expose server via Cloudflare Quick Tunnel
  --trust-proxy             Trust X-Forwarded-For headers (behind a reverse proxy)
  --log-format ""           Log format: json or text (defaults to text)

zkettle version             Print version

Configuration Reference

Configuration is resolved in order of precedence: flags > env vars > config file > defaults.

Config file

Search order: ./zkettle.toml, $HOME/.config/zkettle/zkettle.toml

port = 3000
host = "127.0.0.1"
data = "./data"
base_url = ""
cors_origins = []
trust_proxy = false
tunnel = false
log_format = "" # defaults to "text"
tls_cert = ""
tls_key = ""
admin_token = ""
max_secret_size = 0 # 0 = 500KB default
metrics = false

Security: If your config file contains admin_token, restrict permissions: chmod 600 zkettle.toml

Environment variables

VariableDescription
ZKETTLE_PORTHTTP port
ZKETTLE_HOSTListen address
ZKETTLE_DATAData directory
ZKETTLE_BASE_URLBase URL for generated links
ZKETTLE_CORS_ORIGINSComma-separated CORS origins
ZKETTLE_TRUST_PROXYTrust proxy headers (true/1/yes)
ZKETTLE_TUNNELEnable Cloudflare tunnel (true/1/yes)
ZKETTLE_LOG_FORMATLog format: json or text
ZKETTLE_TLS_CERTTLS certificate file path
ZKETTLE_TLS_KEYTLS private key file path
ZKETTLE_ADMIN_TOKENAdmin API bearer token
ZKETTLE_MAX_SECRET_SIZEMax encrypted secret size in bytes
ZKETTLE_METRICSEnable metrics endpoint (true/1/yes)
ZKETTLE_DELETE_TOKENDelete token for zkettle revoke (alternative to --token)

API Reference

POST /api/secrets

Create a secret.

Request:

{
  "encrypted": "<base64url ciphertext>",
  "iv": "<base64url 12-byte IV>",
  "views": 1,
  "minutes": 1440
}

Constraints:

  • encrypted — required, base64url-encoded, max 500KB decoded (configurable via --max-secret-size)
  • iv — required, base64url-encoded, must decode to exactly 12 bytes
  • views — 1-100 (default: 1)
  • minutes — 1-43200 (default: 1440)

Response (201):

{
  "id": "abc123",
  "expires_at": "2024-01-02T03:04:05Z",
  "delete_token": "def456"
}

Errors: 400 (validation), 415 (wrong Content-Type), 429 (rate limited)

GET /api/secrets/{id}

Retrieve and consume a view. Returns the encrypted blob:

{
  "encrypted": "<base64url ciphertext>",
  "iv": "<base64url IV>"
}

Errors: 400 (invalid ID format), 404 (expired, consumed, or nonexistent)

GET /api/secrets/{id}/status

Check availability without consuming a view.

Response (200):

{
  "status": "available"
}

Errors: 400 (invalid ID format), 404 (expired, consumed, or nonexistent)

DELETE /api/secrets/{id}

Delete a secret. Requires Authorization: Bearer {delete_token} header.

Response: 204 No Content

Errors: 400 (invalid ID format), 401 (missing token), 404 (not found or wrong token)

GET /health

Health check. Returns 200 with {"status":"ok"}, or 503 with {"status":"error"} if the database is unavailable.

GET /s/{id}

Serves the web viewer HTML. The decryption key is in the URL fragment (#key) and never sent to the server.

Security Model

  • Zero-knowledge: The server stores only AES-256-GCM ciphertext. The decryption key lives in the URL fragment, which browsers never send to the server.
  • Client-side encryption: All encryption and decryption happens locally — in the browser (Web Crypto API), CLI, or MCP server process. The zKettle HTTP server never sees plaintext.
  • Expiring: Secrets auto-delete after the configured number of views or time limit.
  • Composable library: When importing zKettle as a Go library, use ExtraRoutes and Middleware to extend the server with custom routes and middleware for any deployment.
  • CLI/MCP plaintext exposure: When reading secrets via CLI (zkettle read) or MCP (read_secret), the decrypted plaintext appears in terminal output or the MCP tool result (which enters the AI agent's conversation context). To keep secrets out of terminal scrollback and agent logs, use --clipboard / --file (CLI) or the clipboard / file parameters (MCP). When creating secrets via MCP, use the file parameter to read content from a file, or generate_secret with create=true to generate and encrypt without exposing plaintext. In the web viewer, use the "Copy Without Revealing" button to copy a secret to clipboard without rendering it in the page.

Building

make build          # Build for current platform
make build-all      # Build for darwin/linux/windows amd64 + darwin/linux arm64
make install        # Build and install to $GOPATH/bin or /usr/local/bin
make test           # Run all tests
make clean          # Remove build artifacts

License

AGPL-3.0 — see LICENSE.

Reviews

No reviews yet

Sign in to write a review