For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /reference/docs/dd/0055-hpc-ssh-key-provisioning-browser-and-operations.md.

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:

MethodPathOperation
GET/api/v1/ssh-access/siteslistHpcSites
GET/api/v1/ssh-access/sites/{siteId}/accessgetHpcAccess
GET/POST/api/v1/ssh-access/sites/{siteId}/keyslistHpcKeys, submitHpcKey
POST/api/v1/ssh-access/sites/{siteId}/keys/validatevalidateHpcKey
GET/DELETE/api/v1/ssh-access/sites/{siteId}/keys/{keyId}getHpcKey, deleteHpcKey
POST/api/v1/ssh-access/sites/{siteId}/keys/{keyId}/revokerevokeHpcKey
POST/api/v1/ssh-access/sites/{siteId}/keys/{keyId}/resubmitresubmitHpcKey
GET/api/v1/ssh-access/sites/{siteId}/projectiongetHpcProjection

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-lpk schema; ACLs for cn=pharos-agent (no sshPublicKey write) and cn=pharos-ssh-access (sshPublicKey and only the ldapPublicKey class value write, identity attributes read).
  • SSSD services = nss, pam, ssh, ldap_user_ssh_public_key = sshPublicKey; sshd AuthorizedKeysCommand /usr/bin/sss_ssh_authorizedkeys %u, AuthorizedKeysCommandUser nobody, AuthorizedKeysFile limited to local users.
  • Site agent as pharos-agent (pinned upstream release; ldap with account_source: waldur and ldapPublicKey in user_object_classes; slurm with root_account: pharos).
  • Access agent as pharos-ssh-access.
  • Host resolution of site.<domain> and waldur.<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 users alice and bob with site_username from the local AAI.

8. Verification

LaneCovers
Unit#678 transition table, gate order, key validation cases, generation fencing, propagation window
PostgreSQL integrationConcurrent submissions, duplicates in every state, lost response retry, resubmit keeping key_id, stale and replayed Site results, AUP bulk revocation, restricted role
API contractEvery #678 route and error code, validate/submit parity, gated 403, origin and content type
Site APIGrants: access-agent credential only on SSH routes, Runtime credential refused there, expired and revoked credentials, Site derived from credential
Access agent + OpenLDAP containerACL enforcement in both directions, missing entry, uidNumber mismatch, first/last key and auxiliary class changes, unmanaged values preserved, crash after modify before acknowledgement
Lab e2e (metallo-ikons-1)Waldur membership → LDAP entry → id; key before and after provisioning; ssh succeeds; revoke → ssh fails after the window; leaving one of two Projects keeps keys; leaving all keeps login but sbatch fails; agent restart; measured propagation

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.

#ScopeDone when
1ssh_access domain, application, PostgreSQL adaptor, migrations, registrationUnit and PostgreSQL lanes pass
2Browser API contract and bindings, fixture portsAPI contract lane passes
3Site routes, native credential verifier, edge routeSite API lane passes
4Access agent and systemd packagingOpenLDAP container lane passes
5Icarus-hpc laboratory and Waldur bootstrap (parallel to 1–4)Provisioning scenario passes on metallo-ikons-1
6E2E laneAll lab scenarios pass, propagation measured
7Browser projection and pageRealtime recovery lane passes
8Beto adaptors after the Beto branch mergesFixtures removed from non-development configuration

10. Open questions

  • Context name (ssh_access proposed) 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 loopback ldap://. 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 uidNumber must agree before any write.
Page source

Source snapshot · Revision f928834c8.