End-to-end encrypted secret sync for developer teams. Stop Slacking your .env.
Warning
Sotto is pre-1.0 and has not had a third-party cryptographic audit. It works end to end, but should not yet be trusted with critical production secrets. See SECURITY.md.
Sotto is built around one Rust crypto implementation shared by the native CLI and the browser client through WebAssembly. The server stores and synchronises encrypted data without ever receiving plaintext secrets or usable keys.
The end-to-end flow works: encrypt locally, sync ciphertext, decrypt on another device or in the browser, and share a single secret via a one-time link. Teams work end to end too: organisations with roles, per-member environment grants, key rotation on member removal, machine tokens for CI, and lost-key account recovery.
| Component | Available now |
|---|---|
| Crypto core | KDF, XChaCha20-Poly1305 AEAD + AAD, key wrapping, X25519 sealed-box grants, the environment vault hierarchy, data-key rewrap (rotation), share-link crypto, and key encoding - with native↔WASM golden vectors |
| CLI | init, local secret management, run-style injection, login/push/pull sync, new-device setup, share; teams: org create/ls/invite/members/remove, grant, clone, rotate, machine token create/ls/revoke (with SOTTO_TOKEN mode for CI), lost-kit reset |
| Server | OAuth login + sessions, account + snapshot sync (versioned writes, ETag), orgs + memberships + roles, per-member vault-key grants, transactional key rotation, machine tokens, account reset, and share links - ciphertext only |
| Web | Login (cookie session), in-browser unlock + vault decryption via your own grant, one-time share create/receive, and a team panel: orgs, members, invite by email, share an environment with a member |
Prebuilt, signed binaries for macOS (Apple Silicon + Intel), Linux (x86_64 + ARM64), and Windows x86_64:
curl -fsSL https://raw.githubusercontent.com/getsotto/sotto/main/install.sh | shirm https://raw.githubusercontent.com/getsotto/sotto/main/install.ps1 | iexThe installer verifies the archive's SHA-256 checksum - and its Sigstore signature, when cosign
is installed - before installing (~/.local/bin on macOS/Linux, %LOCALAPPDATA%\sotto\bin on
Windows). Prefer to look first? Grab an archive from the
releases page and verify it manually per
SECURITY.md, or build from source (see Developing).
For GitHub Actions, use the Sotto Setup action to
install an exact CLI release and verify its checksum and Sigstore bundles before making sotto
available to later steps:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: getsotto/sotto-action@543d1af56ac81d1f1511d88c3d269106e8513a28 # merged v1.1 implementation
with:
sotto-version: v0.4.0
- run: sotto run -- npm test
env:
SOTTO_SERVER: ${{ vars.SOTTO_SERVER }}
SOTTO_TOKEN: ${{ secrets.SOTTO_TOKEN }}The action ref and sotto-version are independent. The example pins the merged v1.1 implementation
by full commit SHA because a numbered action release has not yet been published. Keep
sotto-version as an exact vX.Y.Z release. Set the optional SOTTO_SERVER repository variable
for a self-hosted server. See the action documentation
for matrix, Windows, and reusable-workflow examples.
sotto init # create your identity + first project - SAVE the printed Emergency Kit
sotto set DATABASE_URL # hidden prompt; encrypted locally before it ever touches disk
sotto run -- npm start # inject the environment's secrets into any command
sotto login && sotto push # optional: sync ciphertext via the hosted instance (getsotto.co.uk)
sotto share DATABASE_URL # one-time, burn-after-reading link for a single secretsotto login uses the hosted instance at getsotto.co.uk unless you point
it elsewhere with --server <url> (see Deploying to run your own). Either way
the server only ever stores ciphertext - the web vault at the same address decrypts in your
browser, with keys that never leave your devices.
Working with a team:
sotto org create acme # prints the org id
sotto init --org <org-id> # an org-owned project
sotto org invite <org-id> dev@example.com # invite an existing Sotto user
sotto grant <user-id> # share the active environment (they run `sotto clone`)
sotto token create --name ci # SOTTO_TOKEN: run/export in CI, no password neededCLI (native) ─────┐
├── sotto-core ── versioned encrypted data
Web client (WASM) ┘ │
▼
sync/API server
(ciphertext only)
The workspace contains four crates:
crates/core- shared cryptographic types and, the complete crypto implementation.crates/cli- thesottocommand-line interface and primary native client.crates/server- the Axum-based synchronisation API.crates/wasm-wasm-bindgenbindings that expose the core to web clients.
- Rustup with stable Rust 1.89 or newer
- The
clippyandrustfmtcomponents - The
wasm32-unknown-unknowntarget
The checked-in rust-toolchain.toml asks Rustup to install the required components and
target automatically.
Clone the repository, then build and test the complete workspace:
git clone https://github.com/getsotto/sotto.git
cd sotto
cargo build --workspace
cargo test --workspaceUse the CLI locally (no server required):
cargo run -p sotto-cli -- --help
cargo run -p sotto-cli -- init # create an identity + project; prints your Emergency Kit
cargo run -p sotto-cli -- set DATABASE_URL # hidden prompt
cargo run -p sotto-cli -- run -- your-command # inject secrets as env vars into a subprocessSecrets are encrypted at rest in a local SQLite store; the master key is cached in the OS keychain
with a TTL. Syncing to a server (login/push/pull/setup/share) is optional.
The server needs Postgres (a docker compose up -d brings one up for local use):
DATABASE_URL=postgres://sotto:sotto@localhost:5432/sotto cargo run -p sotto-server
curl http://127.0.0.1:8080/health # → okGitHub OAuth login requires GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET. For any non-local
deployment also set SOTTO_PUBLIC_URL to the server's externally reachable origin - it builds the
GitHub callback URL and must match the OAuth app's registered callback (it otherwise defaults to
http://localhost:8080) - and, for the web client, SOTTO_WEB_ORIGIN. Without OAuth the server
still boots (serving /health and running migrations), but login and every authenticated endpoint
(sync, share creation) are unavailable.
The browser client runs the same crypto core via WebAssembly (web/):
cd web
npm ci
npm run dev # dev server (proxies the API to localhost:8080)
npm run build # production bundle → web/dist (strict CSP + Subresource Integrity)One command brings up a complete hosted instance - Postgres, the server, and Caddy with automatic
HTTPS - from deploy/docker-compose.prod.yml; the runbook is
deploy/README.md. The pieces also work standalone: serve the web app and API
from one origin (so the session cookie and CSP stay same-origin) - the included
Caddyfile serves web/dist and reverse-proxies the API, with security headers;
Dockerfile builds the server image (migrations run on boot).
Run the same core checks used by CI:
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspaceSupply-chain policy is defined in deny.toml and checked in CI with
cargo-deny:
cargo deny checkThe cross-implementation gate proves the native and WASM builds agree - native-produced ciphertext decrypts byte-for-byte in WASM from shared golden vectors:
wasm-pack test --node crates/wasmThe web build and its dependency audit run in CI (.github/workflows/ci.yml).
The server sends one anonymous ping per day (the first 10-20 minutes after boot) to
https://getsotto.co.uk/telemetry/v1/ping, so we can count active instances and see which
versions are in the wild. The response names the latest release, and the server logs a line when
it is running an outdated version. This is the entire payload - the sending code is
crates/server/src/telemetry.rs, and a unit test pins the
payload to exactly these four fields:
{ "instance_id": "0d0972a6-…", "version": "0.2.0", "os": "linux", "arch": "x86_64" }instance_id is a random UUID generated once and stored in your database - derived from nothing,
so it identifies no hardware, host, or account; deleting it makes the instance a fresh anonymous
counter. The ingest side stores no IP addresses and no derived location. There are no org, member,
or secret counts, and no usage events. The CLI, web client, and WASM never send anything.
Opt out with SOTTO_TELEMETRY=off (or the cross-tool
DO_NOT_TRACK=1) - when disabled the task is never started and
no request is ever made. SOTTO_TELEMETRY_URL redirects the ping (e.g. to aggregate a private
fleet), and records idle for 12 months are purged from the hosted census.
Sotto's model is zero-knowledge: plaintext secrets and usable decryption keys stay on client devices, and the server sees only ciphertext plus minimal metadata. This is implemented but not yet independently audited - see SECURITY.md for the model, the honest metadata exposure, how the (re-fetched, weaker) web surface is hardened, and how to verify signed releases. The full adversary model, guarantees, and explicit non-goals are published in THREAT-MODEL.md. Report vulnerabilities privately per SECURITY.md.
Licensed under the Apache License, Version 2.0 - all crates and the web client. You may not use this project except in compliance with the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.