Options, JWT bearer validation, the /meta and .well-known endpoints, and a dev compose stack with Keycloak. Verified end to end: compose up, migrate, run, both discovery endpoints return correct payloads, and readiness reports the schema current. Configuration: - Strongly-typed options for Server, Oidc, Relay and Sync, all ValidateOnStart. A self-hosted server that boots half-configured and fails later per-request is far harder to diagnose than one that refuses to start and names the bad setting. - Cross-field validation the annotations cannot express: relay needs a WebSocketUrl when enabled, idle timeout must be under max session duration, item payload cap under batch cap. - Startup warnings for combinations that are individually valid but dangerous together: RequireHttpsMetadata false outside Development, and AllowEmailLinking (which turns any token bearing a victim's email into account takeover, hence default false). Auth: - JwtBearer with ClockSkew cut to 30s from the 5-minute default; five minutes of slack on a credential granting vault ciphertext access is more than any clock needs. - IncludeErrorDetails off, and a FallbackPolicy so an endpoint without an explicit policy still requires a caller rather than silently being public. Discovery, per ADR 0002: - /api/v1/meta reports versions, features and push caps. - /.well-known/dodossh-configuration is the onboarding story: the user types one server URL and the client discovers OIDC authority, client id, scopes and relay endpoint. Two environment problems found by actually running the stack: - PostgreSQL 18 changed its data mount point. Mounting /var/lib/postgresql/data — correct through 17 — makes the image refuse to start; 18+ wants a single mount at /var/lib/postgresql with the cluster in a subdirectory. - Keycloak moved to host port 18080. An unrelated Apache Tomcat on this machine holds 127.0.0.1:8080, and a loopback-specific bind beats Docker's 0.0.0.0 publish for "localhost". It presents as Keycloak 404ing every realm while its own log says the import succeeded, which is a genuinely misleading failure. Also: CA1848 is enforced, not advisory — warnings are errors, so the .editorconfig comment claiming otherwise was wrong. Startup and health logging now uses [LoggerMessage]. And a clean rebuild is back to zero warnings; the incremental build had been hiding 40 in test projects (banned Guid.NewGuid, an obsolete Testcontainers constructor, and two analyzer families that are genuinely noise under a test host). Verified: 0 warnings on a clean rebuild, 122 tests pass, format clean.
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.