dotnet format --verify-no-changes exits 2 on main: the async-suffix naming rule fires on every async test method. I reported this gate as clean when finishing the sync engine and it was not. Test names are documentation. Push_WithAStaleVersion_ReportsConflict says what is asserted; adding Async says the same plus an implementation detail nobody reading a failure report needs. The suffix convention exists so callers can spot awaitables, and a test method has no callers -- so the rule is switched off under tests/ rather than the names being changed.
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 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/, starting with
the E2EE trust model.
Repository layout
src/
DodoSSH.Contracts DTOs shared with the client — the real API contract
DodoSSH.Crypto DSH1 envelope, AAD derivation, key wrapping
DodoSSH.Domain entities and invariants, no EF
DodoSSH.Infrastructure DbContext, configurations, migrations
DodoSSH.Api the host
tests/ one test project per source project
docs/adr/ architecture decision records
Building
Requires the .NET SDK pinned in global.json (10.0.x).
dotnet build DodoSSH.slnx
dotnet test DodoSSH.slnx
Run the API locally:
dotnet run --project src/DodoSSH.Api
It listens on http://localhost:5233, serving /healthz/live, /healthz/ready and — in
Development — /openapi/v1.json.
Conventions the build enforces
- Warnings are errors.
dotnet format --verify-no-changesgates CI. - Package versions are centralised in
Directory.Packages.props;packages.lock.jsonis committed and CI restores in locked mode. BannedSymbols.txtbansDateTime.UtcNow(useTimeProvider),Guid.NewGuid(useCreateVersion7), sync-over-async, MD5/SHA1 and PBKDF2.- Public members of
DodoSSH.Contractsmust be declared inPublicAPI.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. Gated on
freezing
DodoSSH.Contractsand the crypto AAD, plus two client spikes (Linux WebView, SSH.NET window-change). - 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
Not yet chosen.