# DodoSSH A self-hosted, team-oriented SSH client with an end-to-end encrypted vault. Manage hosts, credentials and keys in a desktop app; sync them across your devices and share them with teammates through a server you run yourself. **The server stores ciphertext and never holds a key** — the operator cannot read the credentials it stores. > Status: early development. See [the milestone plan](#milestones) for what exists today. ## Why Teams either scatter SSH credentials across individual `~/.ssh` directories with no sharing story, or pay per-seat for a hosted product that holds their infrastructure credentials. DodoSSH keeps the convenience of a synced, shareable vault while remaining self-hostable and zero-knowledge. ## Architecture | Component | Choice | | --- | --- | | Backend | ASP.NET Core on .NET 10, PostgreSQL + EF Core | | Client | Avalonia (C#) for Windows/Linux/macOS; terminal pane is a WebView running xterm.js | | Auth | OIDC, provider-agnostic (Entra ID, Keycloak, Auth0, Authentik) | | Vault | End-to-end encrypted; X25519 + Ed25519 + XChaCha20-Poly1305, Argon2id unlock | | Connections | Client-direct SSH by default, with an optional raw-TCP server relay | Two consequences worth knowing before you read further: - **Revocation is not retroactive.** A removed member keeps what they already downloaded. The real remediation is rotating the SSH credential, so offboarding is built around a rotation checklist rather than a button that implies more than it delivers. - **No session recording in relay mode.** The relay forwards SSH ciphertext, so it cannot see commands. That is the cost of the relay not being able to read your traffic. The reasoning behind each major decision is recorded in [`docs/adr/`](docs/adr/), starting with [the E2EE trust model](docs/adr/0001-e2ee-trust-model.md). ## Repository layout ``` src/ DodoSSH.Contracts DTOs shared with the client — the real API contract DodoSSH.Crypto DSH1 envelope, AAD derivation, the key hierarchy DodoSSH.Domain entities and invariants, no EF DodoSSH.Infrastructure DbContext, configurations, migrations DodoSSH.Api the server DodoSSH.Client.Auth OIDC code+PKCE on a loopback redirect, and the key binding DodoSSH.Client.Api the typed server client, and client-side enrollment DodoSSH.Client.Domain the decrypted item model and the three-way merge — no I/O at all DodoSSH.Client.Storage the local cache: ciphertext mirror, outbox, offline unlock material DodoSSH.Client.Sync the pull/apply/push loop and the conflict policy DodoSSH.Client.Session where a profile lives, unlocking it, and getting one in the first place DodoSSH.Client.Ssh connections, PTY shells, host key trust DodoSSH.Client.Terminal the loopback data plane and credit-based flow control DodoSSH.Client.App Avalonia; the only project that knows about a UI toolkit tests/ one test project per source project docs/adr/ architecture decision records ``` Everything under `src/DodoSSH.Client.*` except `App` is deliberately free of Avalonia. That is the seam that lets the SSH layer, the terminal's flow control and the OIDC flow be tested without a UI toolkit or a browser engine — which is most of why they are testable at all. ## Building Requires the .NET SDK pinned in [`global.json`](global.json) (10.0.x). ```bash dotnet build DodoSSH.slnx ``` ```bash dotnet test DodoSSH.slnx ``` Run the API locally: ```bash dotnet run --project src/DodoSSH.Api ``` It listens on `http://localhost:5233`, serving `/healthz/live`, `/healthz/ready` and — in Development — `/openapi/v1.json`. ### End-to-end verification One suite runs against a real server rather than a stub, and it is opt-in because it needs the stack: ```bash docker compose -f deploy/docker-compose.dev.yml up -d ``` ```bash dotnet ef database update --project src/DodoSSH.Infrastructure ``` ```bash dotnet run --project src/DodoSSH.Api ``` ```bash DODOSSH_E2E=1 dotnet test tests/DodoSSH.SystemTests ``` It signs in through a real Keycloak, enrolls, unlocks, creates a host, syncs it, reads it back on a second simulated machine, unlocks again with no network, and opens a shell on a real `sshd`. Skipped otherwise, with a message naming the commands above. It is worth its weight: on its first run it found a loopback redirect URI the realm registered in a form Keycloak rejects, and a JSON configuration gap that made the whole sync surface unreachable from the real client while every other test passed. Both are the same class of bug — two sides of a stub agreeing with each other about something the specification never said. Development and testing are currently **Windows-only**. Anything known or suspected to differ on Linux and macOS is tracked in [`docs/platform-flags.md`](docs/platform-flags.md), along with the deployment gotchas that have already cost time once. Read it before assuming something works off-Windows. ### Conventions the build enforces - Warnings are errors. `dotnet format --verify-no-changes` gates CI. - Package versions are centralised in `Directory.Packages.props`; `packages.lock.json` is committed and CI restores in locked mode. - [`BannedSymbols.txt`](BannedSymbols.txt) bans `DateTime.UtcNow` (use `TimeProvider`), `Guid.NewGuid` (use `CreateVersion7`), sync-over-async, MD5/SHA1 and PBKDF2. - Public members of `DodoSSH.Contracts` must be declared in `PublicAPI.Unshipped.txt`, so a contract change is a build error rather than a client-side surprise. ## Milestones - **M0 — foundation.** Repo structure, build conventions, CI, ADRs. *Done.* - **M1 — vertical slice.** OIDC login → enroll → create a host → open a shell. *Server done:* the DSH1 crypto core, the data model, sync push/pull for hosts, `/me`, and enrollment with the identity-provider key binding. *Client done:* the key hierarchy, the OIDC flow with the key binding, SSH connections with host key trust, the terminal data plane, the encrypted local cache with the sync client — offline unlock, an outbox and a field-level three-way merge, conflict matrix green — and an Avalonia shell that is vault-backed: server URL → browser sign-in → enroll → unlock → host list → terminal. The shell's whole path is covered by tests against an in-memory server, so the states that matter most (the recovery code that cannot be skipped, the unlock that needs no network) are checked rather than remembered. *Verified end to end:* `tests/DodoSSH.SystemTests` drives the whole slice against a real Keycloak, a real API, a real PostgreSQL and a real `sshd` — sign-in, the identity-provider key binding, enrollment, offline unlock, a host through the vault to a second machine, and an interactive shell. See [End-to-end verification](#end-to-end-verification). Known gaps in the client, stated rather than implied by the interface: credentials are not a synced entity type yet, so a connection still asks for a password; known host keys live in memory for one session instead of in the vault; and no device key is registered, so the passphrase is needed on every launch until the OS keystore is wired. - **M2 — full personal vault**, robust sync, relay. - **M3 — teams**, sharing, ACLs. - **M4 — hardening and ops**, packaging, self-hosting guide. - **M5 — multi-provider OIDC**, key rotation, per-item content keys. ## Licence [MIT](LICENSE).