Public Access
Spelunk: sign each device in through the browser instead of keeping Spelunk API keys in the vault
A Spelunk API key reaches one organisation, so a server elsewhere - oc-linux in Orange Climate - was refused before it reached the agent. Each device now signs in to a Spelunk once, under Settings -> Security -> Spelunk or from a Spelunk host's editor: the OAuth authorization code flow with PKCE for a native app (RFC 8252), with Spelunk as the authorization server. The browser opens Spelunk's /api/app-sign-in/authorize with a loopback redirect (the app's own scheme on Android), the person signs in there however they normally do and allows "DodoSSH on <device>", and the code is redeemed at /api/app-sign-in/token for an app session token. That token opens terminals in every organisation its person is in, with the role they have on each server at the time; tunnel tickets send no organisation. The sign-in stays on the device: sealed in the local cache under the LocalCacheKey (AAD resource type 18, bound to its row), never synced, wiped with the cache, and only ever sent to the Spelunk it came from. A 401 from Spelunk forgets it; SIGN OUT ends it there and here, and so does signing out of DodoSSH. Before every Spelunk connection DodoSSH asks its own identity provider whether its sign-in still stands (a forced refresh, at most once a minute), and an account it refuses is signed out of DodoSSH and of every Spelunk on the device. A route names no key any more: Spelunk hosts are written at host schema 11, which an older build shows read-only and asks to be updated for, and a payload that still names a key credential decodes with the id skipped. The SpelunkKey item type is gone: SyncEntityType 14, ChangeEntityType 14 and AAD type 17 are retired, the server drops spelunk_key and its change-log rows, and the client cache drops its cached items, outbox entries and conflicts of that type. A link still never starts a sign-in: from a Spelunk this device is not signed in to it opens the editor, which offers to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
902761d37b
commit
ef365f462e
62 files changed
+4327
-1945
No files matched your search
@@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
namespace DodoSSH.Client.Domain;
|
||||
|
||||
/// <summary>
|
||||
/// How a host is reached through Spelunk: which Spelunk, which of its servers, and which key opens the way.
|
||||
/// How a host is reached through Spelunk: which Spelunk, and which of its servers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
@@ -15,15 +15,15 @@ namespace DodoSSH.Client.Domain;
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Inside the host's encrypted payload, and nowhere else.</b> Unlike the relay, nothing about this has to
|
||||
/// be readable by the DodoSSH server: the tunnel is opened by this client, against Spelunk, with a key only
|
||||
/// the vault holds. So the concession ADR 0004 argues for the relay's address is not made for this.
|
||||
/// be readable by the DodoSSH server: the tunnel is opened by this client, against Spelunk, as whoever is
|
||||
/// signed in to that Spelunk on this device. So the concession ADR 0004 argues for the relay's address is
|
||||
/// not made for this.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The API key is referenced, not embedded.</b> <see cref="ApiKeyCredentialId"/> names the keychain item
|
||||
/// holding it, for the reason <see cref="HostSecret.CredentialId"/> gives: one Spelunk key is the key for
|
||||
/// every server in its organisation, and a copy per host is twenty places to rotate it and one to forget.
|
||||
/// That item is a <see cref="SpelunkKeySecret"/>, which also says the one address the key may go to, or —
|
||||
/// for a host set up before those existed — an ordinary credential whose password is the key.
|
||||
/// <b>No credential travels with the host.</b> Each device signs in to a Spelunk once, through the browser,
|
||||
/// and keeps what that gives it to itself (see <c>SpelunkSignIns</c>): the same host opens on every device
|
||||
/// whose person is signed in to its Spelunk, as that person, in whichever of their organisations the server
|
||||
/// is in. Nothing about who may connect is synced.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed record SpelunkRoute
|
||||
@@ -50,32 +50,13 @@ public sealed record SpelunkRoute
|
||||
/// <summary>The server's id in Spelunk.</summary>
|
||||
public required Guid ServerId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The keychain item holding the Spelunk API key: a <see cref="SpelunkKeySecret"/>, or a credential whose
|
||||
/// password is the key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Named for the credential it always was, and kept so on the wire: item ids are unique across kinds, so
|
||||
/// the one id says which, and a build that predates Spelunk keys reads a host naming one as a host whose
|
||||
/// key credential has gone — refused at connect, and kept as it is by an edit — rather than as damage.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Required rather than nullable. A route with no key can never be opened, and a host saved that way
|
||||
/// would refuse every connection with a sentence about something the editor let through. The reference
|
||||
/// can still dangle — the credential may be deleted on another machine — and that is refused where it
|
||||
/// is noticed, at connect, exactly as a missing SSH credential is.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public required Guid ApiKeyCredentialId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether Spelunk signs the user in, so the host needs no SSH key, credential or password of its own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For each connection this client makes a throwaway key pair in memory and sends the public half with
|
||||
/// the ticket request, which the API key authorises. The agent then runs a one-connection SSH server on
|
||||
/// the ticket request, which this device's sign-in to Spelunk authorises. The agent then runs a one-connection SSH server on
|
||||
/// the machine whose only authorised key is that one, and says which account it logs in as. It is real
|
||||
/// SSH to the server's own host key, so shells, file transfer and port forwards work as for any host,
|
||||
/// and the key is gone when the connection is.
|
||||
@@ -121,12 +102,6 @@ public sealed record SpelunkRoute
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ApiKeyCredentialId == Guid.Empty)
|
||||
{
|
||||
reason = "A host reached through Spelunk needs its Spelunk API key chosen.";
|
||||
return false;
|
||||
}
|
||||
|
||||
reason = null;
|
||||
return true;
|
||||
}
|
||||
@@ -136,8 +111,8 @@ public sealed record SpelunkRoute
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <b>HTTPS, or plain HTTP to this machine only.</b> The API key travels in a header on every tunnel, so
|
||||
/// an address that sends it in clear across a network is refused rather than stored. A Spelunk running
|
||||
/// <b>HTTPS, or plain HTTP to this machine only.</b> The sign-in's token travels in a header on every
|
||||
/// tunnel, so an address that sends it in clear across a network is refused rather than stored. A Spelunk running
|
||||
/// on the developer's own box is the one place plain HTTP is harmless, and it is allowed there so the
|
||||
/// feature can be tried against one.
|
||||
/// </para>
|
||||
@@ -175,7 +150,7 @@ public sealed record SpelunkRoute
|
||||
|
||||
if (string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.Ordinal) && !uri.IsLoopback)
|
||||
{
|
||||
reason = "The Spelunk API address must use https, because the API key is sent with every connection.";
|
||||
reason = "The Spelunk API address must use https, because your sign-in is sent with every connection.";
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user