diff --git a/.gitignore b/.gitignore index e43b0f9..65f1211 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +downloads/* +!downloads/.gitkeep diff --git a/README.md b/README.md index 391c1d3..b4a7eea 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ It's plain static files with nothing to build, and it loads nothing from other s - Keep `/support.html` and `/privacy.html` at these addresses: the App Store, Google Play, Chrome and Firefox listings point to them. - To release a platform, edit its card in `index.html`: set the store link, then remove the `soon` class and the badge. The main button picks it up. -- For a new Windows version, change the version in the Windows card's link. +- For a new Windows version, put the installer in `downloads/` and change the version in the Windows card's link. +- `host/` holds the server setup files that `host.html` tells people to download. Refresh them from `kestrove-server/selfhost` when those change. -To host it, serve this folder with any web server. `deploy/Caddyfile.snippet` is a Caddy example with the security headers. +Installers are not kept in git. Copy them to `downloads/` on the server. -To preview it locally, run `python3 -m http.server 8090` here. +To host it, copy this folder to the server and run `docker compose up -d` in `deploy/`. To preview it locally, run `python3 -m http.server 8090` here. diff --git a/deploy/Caddyfile.snippet b/deploy/Caddyfile similarity index 80% rename from deploy/Caddyfile.snippet rename to deploy/Caddyfile index 88f6554..16cebd2 100644 --- a/deploy/Caddyfile.snippet +++ b/deploy/Caddyfile @@ -1,7 +1,6 @@ -# The Kestrove website: static files only. Replace DOMAIN and the root path. -DOMAIN { +kestrove.samialabri.com { encode zstd gzip - root * /srv/kestrove-website + root * /srv/site header { Strict-Transport-Security "max-age=63072000" X-Content-Type-Options nosniff diff --git a/deploy/compose.yaml b/deploy/compose.yaml new file mode 100644 index 0000000..54a88dc --- /dev/null +++ b/deploy/compose.yaml @@ -0,0 +1,19 @@ +# The Kestrove website. Run from this folder: docker compose up -d +name: kestrove-website +services: + web: + image: caddy:2.8-alpine + restart: unless-stopped + read_only: true + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ..:/srv/site:ro + - caddy-data:/data + - caddy-config:/config +volumes: + caddy-data: + caddy-config: diff --git a/downloads/.gitkeep b/downloads/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/host.html b/host.html new file mode 100644 index 0000000..82f859d --- /dev/null +++ b/host.html @@ -0,0 +1,124 @@ + + + + + +Host Kestrove + + + + + +
+

Kestrove home

+

Host your own Kestrove

+

Your vault is encrypted on your devices before it is synced, so your server only ever stores data it can't read. Setup takes about five minutes.

+ +
+

What you need

+ +
+ +
+

1. Point your domain at the server

+

At your DNS provider, create an A record for your chosen name with your server's IPv4 address. Add an AAAA record too if the server has IPv6.

+

Using Cloudflare? Set the record to DNS only (grey cloud), not Proxied. Kestrove gets its own certificate and pins it in the apps, and Cloudflare's proxy would break both.

+
+ +
+

2. Install Docker

+

Skip this if docker compose version already works.

+
curl -fsSL https://get.docker.com | sudo sh
+
+ +
+

3. Get the files and run setup

+
mkdir kestrove && cd kestrove
+curl -fsSLO https://kestrove.samialabri.com/host/compose.yaml
+curl -fsSLO https://kestrove.samialabri.com/host/setup.sh
+curl -fsSL https://kestrove.samialabri.com/host/env.example -o .env.example
+chmod +x setup.sh
+sudo ./setup.sh
+

The setup asks for your domain and an email address (for certificate notices only), creates a private .env file with a random setup code, checks your DNS, starts Kestrove and waits for HTTPS. At the end it prints your address and the setup code.

+
+Prefer to do it by hand? +
cp .env.example .env
+nano .env        # DOMAIN, ACME_EMAIL, and a KV_SETUP_CODE of at least 16 characters
+chmod 600 .env
+docker compose up -d
+
+
+ +
+

4. Create your account

+
    +
  1. Open https://your-domain in a browser.
  2. +
  3. Choose Create account and enter the setup code from step 3.
  4. +
  5. Pick a strong master password. Kestrove then gives you a Secret Key.
  6. +
  7. Save the Emergency Kit it offers, a PDF with your Secret Key and server address. Print it or store it somewhere safe. Without your master password and Secret Key, nobody can open the vault, including you. There is no reset.
  8. +
+

The setup code stops working as soon as your account exists. That first account is the server's admin.

+
+ +
+

5. Add your devices

+

In the web vault go to Devices → Add a device. Then in the Kestrove app on your phone or computer choose Scan setup QR and scan it. Each new device needs your master password once.

+

Get the apps from the download section.

+
+ +
+

Inviting other people

+

One server can hold accounts for your family or team. Each person has their own vault, master password and Secret Key. As the admin you manage accounts, but you can't see, export or reset anyone's passwords.

+
    +
  1. In the web vault open Admin → Invite someone. You can enter their email, so that only that address can use the invite, and choose how long it's valid (1, 7 or 30 days).
  2. +
  3. Send them the invite link or show the QR code. It works once.
  4. +
  5. They open the link, or choose Create a new account in any Kestrove app and enter the code with their email. Then they pick their own master password.
  6. +
+

On the same page you can switch Sign-up to Open, so anyone who can reach the server may create an account, optionally only with certain email domains. You can also disable, delete or promote accounts. Someone who forgets their master password can't be recovered: delete their account and invite them again.

+

The web vault's code comes from your server, so whoever runs the server could in principle tamper with it. People who would rather not rely on that should use the Kestrove apps, which don't load code from the server.

+
+ +
+

Updating

+
cd kestrove
+docker compose pull && docker compose up -d
+

KESTROVE_VERSION=latest in .env follows new releases. Set a version number instead, for example 1.1.0, to update only when you change it. Back up before a big update.

+
+ +
+

Backups

+

Your data lives in the Docker volume kestrove_data. Take a backup any time, even while Kestrove runs:

+
docker compose run --rm -T backup > kestrove-$(date +%F).db
+

Your vault inside the file is still end-to-end encrypted, so it's safe to keep on another machine or in cloud storage. For a nightly backup, run mkdir backups, then add this with crontab -e, changing the path to yours:

+
30 3 * * * cd /home/you/kestrove && docker compose run --rm -T backup > backups/kestrove-$(date +\%F).db && find backups -name 'kestrove-*.db' -mtime +30 -delete
+

To restore a backup onto a fresh install, after step 3, change the file name below to yours:

+
docker compose stop server
+docker run --rm -v kestrove_data:/data -v "$PWD":/backup alpine \
+  sh -c 'cp /backup/kestrove-2026-01-31.db /data/kestrove.db && rm -f /data/kestrove.db-wal /data/kestrove.db-shm && chown 65532:65532 /data/kestrove.db'
+docker compose start server
+

Anything changed on the server since the backup is gone from the server. Devices with newer data send it again on their next sync.

+
+ +
+

Troubleshooting

+

Setup warns the domain points elsewhere. Check the A record's IP, and "DNS only" on Cloudflare. DNS changes can take a few minutes.

+

The browser shows a certificate error. Run docker compose logs web. Ports 80 and 443 must be open in both your cloud firewall and ufw or firewalld.

+

Apps say they can't reach the server. Open https://your-domain on that device. If the server's IP changed, update the DNS record.

+

You lost the setup code. It's KV_SETUP_CODE in .env. If that was empty, run docker compose logs server | grep SETUP_TOKEN.

+

You want to start over. docker compose down -v deletes everything, then run ./setup.sh again.

+

To see what's happening, use docker compose logs -f for logs and docker compose ps for status.

+
+ +
+

What runs

+

server is the sync API. It stores encrypted vault data, device keys and a login verifier that is not your password. It runs as a non-root user on a read-only filesystem.

+

web is Caddy. It handles HTTPS certificates from Let's Encrypt, the security headers and the web vault, and it passes sync traffic to the server.

+

Nothing else is contacted: no telemetry, and no outside services besides Let's Encrypt.

+
+
+ + diff --git a/host/compose.yaml b/host/compose.yaml new file mode 100644 index 0000000..4838c48 --- /dev/null +++ b/host/compose.yaml @@ -0,0 +1,69 @@ +# Kestrove: your own end-to-end encrypted password manager server. +# +# ./setup.sh asks two questions, writes .env and starts everything +# docker compose up -d (after filling in .env yourself) +# +# Two containers: `server` (the sync API; it only ever stores encrypted data) and `web` (Caddy with the +# web vault: automatic HTTPS from Let's Encrypt, strict security headers, and /v1 proxied to `server`). +# Full guide: https://kestrove.samialabri.com/host.html +name: kestrove + +x-server-image: &server-image ${KESTROVE_REGISTRY:-gitea.samialabri.com/kestrove}/kestrove-server:${KESTROVE_VERSION:-latest} + +services: + server: + image: *server-image + restart: unless-stopped + read_only: true + tmpfs: [/tmp] + cap_drop: [ALL] + security_opt: ["no-new-privileges:true"] + volumes: + - data:/data + environment: + RUST_LOG: info + KV_PUBLIC_URL: https://${DOMAIN:?Set DOMAIN in .env (run ./setup.sh)} + KV_TRUST_PROXY: "1" + # One-time code for creating your account in the web vault. Empty: a random one is printed in + # `docker compose logs server` on first start. + KV_SETUP_CODE: ${KV_SETUP_CODE:-} + networks: [internal] + + web: + image: ${KESTROVE_REGISTRY:-gitea.samialabri.com/kestrove}/kestrove-web:${KESTROVE_VERSION:-latest} + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + environment: + DOMAIN: ${DOMAIN:?Set DOMAIN in .env (run ./setup.sh)} + ACME_EMAIL: ${ACME_EMAIL:?Set ACME_EMAIL in .env (run ./setup.sh)} + volumes: + - caddy-data:/data + - caddy-config:/config + networks: [internal] + depends_on: [server] + + # docker compose run --rm -T backup > kestrove-$(date +%F).db + # A consistent, integrity-checked copy of the database, taken while the server runs. Your vault inside it + # is still end-to-end encrypted: restoring it needs your master password and Secret Key. + backup: + image: *server-image + profiles: [tools] + command: ["backup"] + read_only: true + tmpfs: [/tmp] + cap_drop: [ALL] + security_opt: ["no-new-privileges:true"] + network_mode: none + volumes: + - data:/data + +volumes: + data: + caddy-data: + caddy-config: + +networks: + internal: diff --git a/host/env.example b/host/env.example new file mode 100644 index 0000000..2ee19ed --- /dev/null +++ b/host/env.example @@ -0,0 +1,17 @@ +# Copy to .env (./setup.sh does this for you), then: docker compose up -d + +# The address of your Kestrove, e.g. vault.example.com. Create a DNS A record (and AAAA for IPv6) pointing at +# this server first. Using Cloudflare? Set the record to "DNS only" (grey cloud). +DOMAIN= + +# Let's Encrypt sends certificate expiry warnings here. Nothing else. +ACME_EMAIL= + +# One-time code you enter in the web vault to create your account. At least 16 characters; keep it private. +# It stops working as soon as your account exists. Leave empty to have one printed in the server log. +KV_SETUP_CODE= + +# Where the images come from, and which version. "latest" follows releases; pin a version (e.g. 1.1.0) +# to update only when you choose. +KESTROVE_REGISTRY=gitea.samialabri.com/kestrove +KESTROVE_VERSION=latest diff --git a/host/setup.sh b/host/setup.sh new file mode 100755 index 0000000..ca5448d --- /dev/null +++ b/host/setup.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# Kestrove self-host setup: asks for your domain and email, writes .env, checks DNS and starts Kestrove. +# Safe to run again: it keeps an existing .env unless you choose to replace it. +set -eu +cd "$(dirname "$0")" + +say() { printf '%s\n' "$*"; } +ask() { # prompt default -> REPLY + if [ -n "$2" ]; then printf '%s [%s]: ' "$1" "$2"; else printf '%s: ' "$1"; fi + read -r REPLY || REPLY="" + [ -n "$REPLY" ] || REPLY=$2 +} +die() { say "setup: $*" >&2; exit 1; } + +command -v docker >/dev/null 2>&1 || die "Docker is not installed. See https://docs.docker.com/engine/install/" +docker compose version >/dev/null 2>&1 || die "Docker Compose v2 is missing (the 'docker compose' command)." +docker info >/dev/null 2>&1 || die "Docker is not running, or this user can't use it (try sudo)." + +if [ -f .env ]; then + ask "A .env already exists. Keep it? (Y/n)" "Y" + case "$REPLY" in [nN]*) ;; *) KEEP=1 ;; esac +fi + +if [ -z "${KEEP:-}" ]; then + say "" + say "Kestrove needs a domain name, e.g. vault.example.com, with a DNS record pointing at this server." + ask "Domain" "" + DOMAIN=$(printf '%s' "$REPLY" | tr 'A-Z' 'a-z' | sed -e 's#^https\{0,1\}://##' -e 's#/.*$##') + printf '%s' "$DOMAIN" | grep -Eq '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$' || die "'$DOMAIN' is not a domain name" + ask "Email for Let's Encrypt certificate notices" "" + ACME_EMAIL=$REPLY + printf '%s' "$ACME_EMAIL" | grep -Eq '^[^@ ]+@[^@ ]+\.[^@ ]+$' || die "'$ACME_EMAIL' is not an email address" + # 24 characters from an unambiguous alphabet (no 0/O, 1/I/L), in groups of four. + CODE=$(LC_ALL=C tr -dc 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' .env + say "Wrote .env (readable only by you)." +fi + +# shellcheck disable=SC1091 +. ./.env +[ -n "${DOMAIN:-}" ] || die ".env has no DOMAIN" + +# DNS check: the domain should resolve to one of this machine's public addresses (warn only; NAT and +# proxies make this check imperfect). +PUBLIC_IP=$(curl -fsS -4 --max-time 5 https://api.ipify.org 2>/dev/null || true) +RESOLVED=$( (getent ahostsv4 "$DOMAIN" 2>/dev/null || true) | awk '{print $1; exit}') +[ -n "$RESOLVED" ] || RESOLVED=$( (nslookup "$DOMAIN" 2>/dev/null || true) | awk '/^Address: /{print $2}' | grep -v ':' | tail -1) +if [ -z "$RESOLVED" ]; then + say "Warning: $DOMAIN does not resolve yet. Create its DNS A record, wait a minute, then run this again." + ask "Start anyway? (y/N)" "N" + case "$REPLY" in [yY]*) ;; *) exit 1 ;; esac +elif [ -n "$PUBLIC_IP" ] && [ "$RESOLVED" != "$PUBLIC_IP" ]; then + say "Warning: $DOMAIN points to $RESOLVED, but this server's public address is $PUBLIC_IP." + say " If you use Cloudflare, set the record to \"DNS only\" (grey cloud). HTTPS setup fails otherwise." + ask "Start anyway? (y/N)" "N" + case "$REPLY" in [yY]*) ;; *) exit 1 ;; esac +fi + +say "Starting Kestrove…" +docker compose pull -q +docker compose up -d + +say "" +say "Waiting for the HTTPS certificate (up to a minute)…" +i=0 +until curl -fsS -o /dev/null --max-time 5 "https://$DOMAIN/" 2>/dev/null; do + i=$((i + 1)) + if [ "$i" -ge 24 ]; then + say "Not reachable over HTTPS yet. Check: docker compose logs web" + say "Ports 80 and 443 must be open to the internet for Let's Encrypt." + break + fi + sleep 5 +done + +say "" +say "Kestrove is running at https://$DOMAIN" +say "" +say "Next: open https://$DOMAIN, choose Create account, and enter this setup code:" +say "" +if [ -n "${KV_SETUP_CODE:-}" ]; then + say " $KV_SETUP_CODE" +else + say " (printed in: docker compose logs server | grep SETUP_TOKEN)" +fi +say "" +say "Save your Emergency Kit when the web vault offers it. Then add your phone and computers from the" +say "web vault (Devices > Add a device) by scanning the QR code in the Kestrove apps." diff --git a/index.html b/index.html index 67e0737..507346c 100644 --- a/index.html +++ b/index.html @@ -37,7 +37,7 @@

Your passwords and passkeys are encrypted on your own devices, then synced through a server you run. That server only ever stores data it can't read.

See all downloads - Run your own server + View source code

For iPhone, iPad, Mac, Android, Windows and Linux.

@@ -75,7 +75,7 @@

Your server, in about five minutes

Kestrove's server runs anywhere Docker does. It comes with a web vault and an admin page for inviting people. The admin can manage accounts but can't read anyone's passwords.

-

Read the hosting guide

+

Read the hosting guide

  1. Start your serverPoint a domain at it and run the setup.
  2. @@ -87,8 +87,9 @@
    -

    Get Kestrove

    -

    Install it on every device you use.

    +

    Get Kestrove

    +

    Install it on every device you use.

    + View source code
    - +

    Linux

    Snap Store

    Coming soon
    sudo snap install kestrove

    Open a terminal, run this command, then start Kestrove from your applications.

    diff --git a/legal.css b/legal.css index 4ad9db1..99f75d2 100644 --- a/legal.css +++ b/legal.css @@ -16,3 +16,7 @@ a { color: var(--primary); } section { background: var(--surface); border: 1px solid var(--border); border-radius: 16px; padding: 4px 20px 16px; margin-top: 16px; } code { font-family: ui-monospace, "JetBrains Mono", monospace; font-size: 0.92em; } .home { margin: 0 0 16px; font-size: 15px; } +pre { background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; overflow-x: auto; font-size: 14px; line-height: 1.5; } +pre code { font-size: inherit; } +details { margin: 8px 0; } +summary { cursor: pointer; font-weight: 600; } diff --git a/site.css b/site.css index 5871c5e..c7e1648 100644 --- a/site.css +++ b/site.css @@ -106,3 +106,4 @@ footer { padding: 32px 0; } html { scroll-behavior: auto; } .btn { transition: none; } } +.dl-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; } diff --git a/sitemap.xml b/sitemap.xml index 6fc95bb..5d22f32 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1,6 +1,7 @@ https://kestrove.samialabri.com/ +https://kestrove.samialabri.com/host.html https://kestrove.samialabri.com/support.html https://kestrove.samialabri.com/privacy.html diff --git a/support.html b/support.html index 1031338..dc46943 100644 --- a/support.html +++ b/support.html @@ -16,7 +16,7 @@

    Getting started

      -
    1. Set up a Kestrove server: self-hosting guide (about five minutes with Docker).
    2. +
    3. Set up a Kestrove server: hosting guide (about five minutes with Docker).
    4. Open your server's web address, create your account with the setup code, and save your Emergency Kit.
    5. In the web vault choose Devices → Add a device, then scan the QR code with the Kestrove app on your phone or computer.
    @@ -32,7 +32,7 @@

    Report a problem

    -

    Open an issue on the Kestrove issue tracker. Never include passwords, your Secret Key or your Emergency Kit.

    +

    Email REPLACE_WITH_SUPPORT_EMAIL. Never include passwords, your Secret Key or your Emergency Kit.

    Privacy Policy