openapi: 3.1.0 info: title: Metallo SSH Access Site protocol version: 1.0.0 description: >- Owned by SSH Access (DD-0055 section 4). The access agent pulls member key work and acknowledges LDAP results over HTTPS from the Site. It authenticates with a native bearer whose grant names its principal, Site and the ssh-access scope. The Site always comes from the grant, never from a request body. servers: - url: https://site.example.invalid security: - siteBearer: [] paths: /v1/site/ssh-access/pull: post: operationId: pullSshAccessWork description: >- Members with unacknowledged key generations. Pulling records nothing: work stays pending until acknowledged, so the agent keeps no cursor. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/WorkRequest'} responses: '200': description: One page of members with pending work. content: application/json: schema: {$ref: '#/components/schemas/WorkPage'} '401': {$ref: '#/components/responses/Unauthorised'} '403': {$ref: '#/components/responses/Forbidden'} '413': {$ref: '#/components/responses/TooLarge'} '415': {$ref: '#/components/responses/UnsupportedMediaType'} '422': {$ref: '#/components/responses/InvalidRequest'} '503': {$ref: '#/components/responses/Unavailable'} default: {$ref: '#/components/responses/Failure'} /v1/site/ssh-access/snapshot: post: operationId: snapshotSshAccess description: >- The complete desired key state of every member of the Site, each at its own root revision, for drift reconciliation and recovery after a crash between an LDAP change and its acknowledgement. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/WorkRequest'} responses: '200': description: One page of members. content: application/json: schema: {$ref: '#/components/schemas/WorkPage'} '401': {$ref: '#/components/responses/Unauthorised'} '403': {$ref: '#/components/responses/Forbidden'} '413': {$ref: '#/components/responses/TooLarge'} '415': {$ref: '#/components/responses/UnsupportedMediaType'} '422': {$ref: '#/components/responses/InvalidRequest'} '503': {$ref: '#/components/responses/Unavailable'} default: {$ref: '#/components/responses/Failure'} /v1/site/ssh-access/acknowledge: post: operationId: acknowledgeSshAccessResults description: >- Record LDAP results for one member. Each result is fenced by its key generation: an older generation is stale and changes nothing, and a repeated result for the current generation is replayed. requestBody: required: true content: application/json: schema: {$ref: '#/components/schemas/Acknowledgement'} responses: '200': description: One status per result, in request order. content: application/json: schema: {$ref: '#/components/schemas/AcknowledgementResponse'} '401': {$ref: '#/components/responses/Unauthorised'} '403': {$ref: '#/components/responses/Forbidden'} '409': {$ref: '#/components/responses/Conflict'} '413': {$ref: '#/components/responses/TooLarge'} '415': {$ref: '#/components/responses/UnsupportedMediaType'} '422': {$ref: '#/components/responses/InvalidRequest'} '503': {$ref: '#/components/responses/Unavailable'} default: {$ref: '#/components/responses/Failure'} components: securitySchemes: siteBearer: type: http scheme: bearer description: >- A native Site credential, psb_ followed by 43 base64url characters (256 random bits). The Site API keeps only its SHA-256 digest and grant. responses: Unauthorised: description: Missing, malformed, unknown, expired or revoked credential. content: application/json: schema: {$ref: '#/components/schemas/Error'} Forbidden: description: The credential does not cover these routes, or its Site is not configured. content: application/json: schema: {$ref: '#/components/schemas/Error'} Conflict: description: A result contradicts the recorded result or desired presence, or names an unissued generation. content: application/json: schema: {$ref: '#/components/schemas/Error'} TooLarge: description: The request body exceeds 64 KiB. content: application/json: schema: {$ref: '#/components/schemas/Error'} UnsupportedMediaType: description: The request body is not identity-encoded application/json. content: application/json: schema: {$ref: '#/components/schemas/Error'} InvalidRequest: description: The body, page token, member reference or observation time is invalid. content: application/json: schema: {$ref: '#/components/schemas/Error'} Unavailable: description: Credentials or storage cannot be checked now; retry with backoff. content: application/json: schema: {$ref: '#/components/schemas/Error'} Failure: description: An unexpected failure; the body is redacted. content: application/json: schema: {$ref: '#/components/schemas/Error'} schemas: Error: type: object additionalProperties: false required: [code] properties: code: {type: string} WorkRequest: type: object additionalProperties: false properties: limit: {type: integer, minimum: 1, maximum: 100, default: 50} pageToken: {type: string, maxLength: 256} KeyWork: type: object additionalProperties: false required: [keyId, generation, desired, fingerprint] properties: keyId: {type: string, format: uuid} generation: {type: integer, minimum: 1} desired: {type: string, enum: [present, absent]} fingerprint: {type: string, pattern: '^SHA256:[A-Za-z0-9+/]{43}$'} publicKey: type: string description: The exact key line; present only when desired is present. MemberWork: type: object additionalProperties: false required: [memberRef, username, uidNumber, revision, keys] properties: memberRef: type: string format: uuid description: Opaque per-Site reference; not the canonical member ID. username: {type: string} uidNumber: {type: integer, minimum: 1} revision: {type: integer, minimum: 1} keys: type: array items: {$ref: '#/components/schemas/KeyWork'} WorkPage: type: object additionalProperties: false required: [members, nextPageToken] properties: members: type: array items: {$ref: '#/components/schemas/MemberWork'} nextPageToken: {type: [string, 'null']} Result: type: object additionalProperties: false required: [keyId, generation, outcome, observedAt] properties: keyId: {type: string, format: uuid} generation: {type: integer, minimum: 1} outcome: {type: string, enum: [applied, removed, rejected]} reason: {type: string, minLength: 1, maxLength: 500} observedAt: {type: string, format: date-time} Acknowledgement: type: object additionalProperties: false required: [memberRef, revision, results] properties: memberRef: {type: string, format: uuid} revision: {type: integer, minimum: 1} results: type: array minItems: 1 maxItems: 100 items: {$ref: '#/components/schemas/Result'} ResultStatus: type: object additionalProperties: false required: [keyId, status] properties: keyId: {type: string, format: uuid} status: {type: string, enum: [recorded, replayed, stale]} AcknowledgementResponse: type: object additionalProperties: false required: [results] properties: results: type: array items: {$ref: '#/components/schemas/ResultStatus'}