DD-0055 — Member SSH access: browser surface, laboratory and delivery
Continuation of DD-0055 — Member SSH access to Icarus-hpc; section numbers are kept so existing references stay valid.
6. Browser API
Routes from #678 are declared in contracts/ssh_access/http_api/ and bundled into
contracts/http/api.openapi.json with x-owner: ssh_access. Commands use API command
bindings (idempotency key, expected version, actor from the session). Reads return
Cache-Control: no-store. Error bodies use the #678 codes, including the
multi-error gated 403. Writes require same-origin, as already enforced by the API.
6.1 Site-scoped resources
The browser surface names its Site on every route, because there the caller is a member session and membership must be authorised per request:
siteId is the Site identifier the context configures ([a-z][a-z0-9-]{0,62}),
never free text typed by the member. GET /sites answers from the SSH Access
context as the intersection of the authenticated actor's member (ActorMembers)
and the Sites where that member has an account (ActorMembers.identity), as
{id, name}. It therefore never lists a Site the caller cannot act at, and the
platform Site catalogue is not used, because it lists Sites where the member has
no account.
One Site serves one process; the list is usually one entry,
and a member with several Sites simply receives several roots. The process
checks the requested Site against its configured one, and any other Site reads
as the same public 404 not_found. That response is identical for a Site the
caller cannot act at, a Site that does not exist and a route without one, so
probing never reveals that a Site exists. A key that belongs to another Site
answers key_not_found from inside this Site's root, with no cross-Site signal.
The context keeps site_not_found internally for audit; the API maps it to the
constant public body.
The /api/v1/settings/hpc/* routes are removed with no alias: the only deployed
consumer is the compat page in development, and it moves with this contract.
These are defaults for review: one Site per process, /sites returning only
id and name, a hard cut of the previous address, and one public 404.
On main the API session is the provisioned local operator; the fixture identity
port maps that actor to a test member. Member login arrives with Beto.
6.1.1 One Site for the whole Site API
The Site API is one listener, so the Site it serves is one setting rather than a
property of one route family: SITE_API_SITE_ID, in the same identifier form as
the context's Site ([a-z][a-z0-9-]{0,62}). The command routes and the SSH access
routes both read it, and the native commands@<site> and ssh-access@<site>
grants are checked against it before any storage read; a grant for another Site
answers the shared site_mismatch. The member-credentials route of the AAI work
reads the same setting instead of introducing a third scope.
SSH_ACCESS_SITE remains the SSH access context's own binding. ssh-access-settings
supplies both values, and a listener whose two Site values disagree does not start.
A listener configured with native grants but no SITE_API_SITE_ID also refuses to
start, because a check that cannot compare is not a check; a listener that serves
only Kubernetes identities may omit it, since a projected ServiceAccount token
names no Site. The 4B qualification lane renders the installed workload without the
ssh-access-settings ConfigMap and supplies SITE_API_SITE_ID as a recorded
lane adaptation.
The browser projection (typed Protobuf, Centrifugo) is available through the minimal live key view. It reads the owner's snapshot after subscription, applies only newer revisions and reconciles once after unrecoverable history. No polling is introduced.
6.2 Realtime per Site
The publication aggregate is already Site-scoped: the root is
root_id(member, site), and the projection payload carries member_id and
site. The channel model does not change: one server-authorised principal
channel per actor, with the Site travelling in the aggregate id and the payload,
so a member with several Sites receives several roots on the same channel.
Per-Site channel names would duplicate the principal model and invite
client-side channel selection, which the realtime rules forbid.
6.3 Known limitation: one request queue for every Site
Every ssh-access process serves one Site, but all of them consume the same
request queue. With a second HPC Site, a member request for the other Site can be
delivered to the process that serves this one, which resolves it against its own
Site and answers the same public 404 not_found; GET /api/v1/ssh-access/sites
then lists only the Site of the Site API that process talks to. Nothing prevents
this today because one Site is configured.
The fix is either a queue per Site, with the API addressing the Site the browser
already sends, or one process for every Site that selects the root by site_id.
It is required before the second Site is configured, and it is tracked in #877.
7. Laboratory on metallo-ikons-1
Icarus-cloud: the dev-ikons dev box (k3s, Argo CD) with the platform,
Waldur 8.1.3 and the local AAI. Development uses make takeover; committed changes
use make reconcile.
Icarus-hpc: the VM host, configured by a new laboratory under
dx/metallo/icarus-hpc/, outside devops/ansible and gitops. It owns the
OpenLDAP with slapo-unique/slapo-constraint, SSSD, Slurm, Munge, MariaDB,
coordinator and job_submit.lua configuration instead of extending the removed
control-plane laboratory. On 21 October 2026 the VM had none of these host services installed.
Additions:
openssh-lpkschema; ACLs forcn=pharos-agent(nosshPublicKeywrite) andcn=pharos-ssh-access(sshPublicKeyand only theldapPublicKeyclass value write, identity attributes read).- SSSD
services = nss, pam, ssh,ldap_user_ssh_public_key = sshPublicKey; sshdAuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys %u,AuthorizedKeysCommandUser nobody,AuthorizedKeysFilelimited to local users. - Site agent as
pharos-agent(pinned upstream release;ldapwithaccount_source: waldurandldapPublicKeyinuser_object_classes;slurmwithroot_account: pharos). - Access agent as
pharos-ssh-access. - Host resolution of
site.<domain>andwaldur.<domain>to the dev box edge, and trust in their CAs. - Waldur bootstrap as an administration job (staff token never in a runtime
process): service provider, HPC Icarus offering with
service_provider_can_create_offering_user,identity_claim, POSIX pool, no offering-user auto-deletion, Projects A and B, Orders, test usersaliceandbobwithsite_usernamefrom the local AAI.
8. Verification
Before hand-off: pnpm verify (Python 3.13), pnpm test:integration,
pnpm check:large-file-review, and an explicit report of skipped environment lanes.
Evidence without credentials goes to .local/icarus-hpc/<target>/evidence.
9. Delivery
Branch from main. Each step is a separate PR below roughly 1,000 changed lines.
10. Open questions
- Context name (
ssh_accessproposed) and AUP source (ADR-0055 open questions). - LDAP transport on Icarus-hpc: the access agent uses
ldapi://with SASL EXTERNAL; the upstream Site agent supports simple bind only and uses loopbackldap://. Confirm the listener and service-user mapping with jimsiak. - Site agent ACL change (jimsiak). No existing entry needs
ldapPublicKey. - Acceptance of the distributed owner's separate Site API hostname route commit, including route timeouts/body limits and equivalent staging hostname/TLS evidence.
- Username consistency once Beto lands: Party Site binding, Waldur offering-user
username and LDAP
uidNumbermust agree before any write.
Page source
Source snapshot · Revision f928834c8.