Skip to main content
Version: v5.6.1

Transport providers, grants and bindings

packs:rpc

Transport providers broker remote-host access for the rpc action pack (packs-rpc): instead of SSH keys resting in node properties or step inputs, the platform holds provider records (connection facts plus a credential reference), mints a one-shot session per admitted step, and refuses everything else by name. Three record kinds make up the component, and this page covers all three:

  • a transport provider — how to reach hosts: a kind, connection facts and the credential reference (the fourth provider family, beside identity, secrets and model providers);
  • a transport grant — who may use which provider, where, for what: approval-shaped authority every dispatched session is checked against;
  • a scope binding — which provider serves which estate subtree: the declared default dispatch resolves when nothing names a provider explicitly.

Custody is reference-first throughout: the provider record stores credential_ref, a secret-vault:// address — a pointer, never a value — and no read, audit row or error ever returns credential material. An inline credential supplied on create or update is vault-write sugar: the server stores it in the platform vault under transport-providers/<code>/credential and pins that reference. A provider-qualified reference (secret-vault://<provider>@<path>/<key>) keeps the key in a bound customer vault, so it never rests in OpsChain at all.

The provider record​

AttributeMeaning
codeThe estate code bindings and node properties reference. Immutable after create.
display_nameHuman-readable name.
kindssh-static or ssh-ca. Immutable after create.
config_jsonKind-specific connection facts as a JSON object — never credential material; unknown keys refuse.
credential_refThe canonical secret-vault:// reference the credential lives at.
enabledWhether dispatch resolves sessions through the provider.
last_test_at / last_test_success / last_test_errorThe most recent test verb's recorded health (never credential material).

The two kinds:

  • ssh-static — the referenced credential is an SSH private key (auth_method: key, the default) or a password (auth_method: password); config carries username, auth_method, port and connect_timeout_seconds.
  • ssh-ca — the referenced credential is the CA signing key, and it never leaves the platform. Each admitted session generates an ephemeral ed25519 keypair and signs it into a short-lived SSH user certificate: principals from the config's principals (or username as the single principal — one of the two is required), validity_seconds between 1 and 86400 (default 300 — five minutes), the validity backdated sixty seconds to absorb clock skew, and a key id of opschain-transport:<code>:<username> so host-side logs name the provider and the requesting principal. No standing key ever rests on a host path.

Provider lifecycle​

  • Create registers the record; the code and kind are immutable afterwards. Exactly one credential channel is supplied: a credential_ref to pin, or an inline credential to vault. The reference is resolved as it is set — the write-time pin — so a record can never point at a dangling or foreign address. A duplicate code refuses: a transport provider with this code already exists.
  • Update changes any mutable field; a supplied reference re-pins, an inline credential rotates the stored vault value and re-pins. Any provider write (rotation included) bumps the session-pooling generation, so the next session dials fresh instead of reusing a pooled connection.
  • Enable / disable ride the update verb. Enabling a disabled provider re-checks the packs:rpc entitlement; disabling never gates, so a lapsed estate can always stand a provider down.
  • Test validates the record without dialling any host: the kind config parses against its schema and the referenced credential resolves and parses — for ssh-static as an SSH private key or password per the config's auth_method, for ssh-ca by signing a probe certificate with the CA key. The typed outcome lands on the row as last_test_*, and the failure reason never carries credential material.
  • Delete removes the record. It refuses while scope bindings still reference the provider — the transport provider is referenced by scope bindings - delete its bindings first — so a delete never silently re-routes bound scopes. The vault value a reference points at is never deleted: custody stays with the secrets plane.

Transport grants — the authority a session needs​

A grant answers who may use which provider, where, for what:

FieldMeaning
subject_kind / subject_idWho the grant admits: user (id or username), group, service_account or agent_group — humans and agents gate identically.
providerThe granted provider's code, or a code glob (nonprod-*).
scopeThe node-path glob sessions may target — it must start with /; ** matches any number of segments, and a fully-matched scope covers everything at and below it (e.g. /projects/*/environments/nonprod/**).
actionsrpc action patterns (exec, put, get, sync, facts, ensure, globs over them); empty grants every action.
require_step_upSession minting demands a stepped-up session from the requesting principal.
rate_per_minute / sessions_per_daySession caps in fixed windows (UTC day); 0 is capless.
expires_atThe validity window's end; unset grants until revoked.
approvers / note / lifecycle columnsThe funnel checklist's approvers, why the grant exists, and who granted/approved/revoked it when.

Lifecycle is pending → active → revoked, in the same mould as every other held authority:

  • Create proposes the grant. On a composition carrying the approval fabric it lands pending and opens a strict checklist in the one approvals inbox every held subject enters (subject kind transport_grant — see change governance); --approvers is then required, so a governed grant is never approver-less. Quorum completion activates the grant, rejection and quorum-TTL expiry revoke it without it ever activating, and the checklist's fingerprint binds every approval to the grant content the approver saw. Without the fabric the grant activates directly — the base posture.
  • Approve / reject are the direct base-posture verbs. While a funnel ceremony is open they refuse: transport grant <id> is held by the approval fabric: approve or reject it through the approvals inbox (opschain approvals / ChangeGovernanceService), never a direct grant edit.
  • Revoke terminally revokes a pending or active grant, and is never entitlement-gated.

Every grant verb sits behind the /admin updatable determination, and grant writes additionally carry the step-up session floor.

Scope bindings — which provider serves a subtree​

A binding is one declared scope path → provider mapping: / binds the estate default, /projects/<code> and /projects/<code>/environments/<code> override it down the node tree, and the deepest covering scope wins. Bindings are declared and auditable, never inferred.

  • Set declares (or re-points) the binding for a scope path — the named provider must be enabled — and is audited as a create or an update depending on whether the scope already had one.
  • Delete removes the binding; the scope falls back to the default.
  • Resolve answers selection provenance for a node path: the governing provider and the scope that decided it — the deepest covering binding, or the single-enabled default (reported with an empty scope) when nothing binds. Several enabled providers and no covering binding refuse rather than guess: several transport providers are enabled and no root binding names the default - bind scope path / to one of them.

Each listed binding carries its provider's enabled state and recorded test health, so the posture view answers which parts of the estate would dispatch against an unhealthy provider.

How a step's session is established at dispatch​

A dispatched command step that references the OPSCHAIN_TRANSPORT_SESSION environment variable asks for a brokered session; a step without the marker costs nothing. At spec build the server:

  1. Resolves the governing provider, in strict precedence: the explicit rpc.provider_ref node property (leaf-most node wins), else the deepest-wins scope binding, else the single enabled provider. Every miss refuses by name — a property naming an unregistered or disabled provider, a binding whose provider is disabled (transport provider "<code>" bound at <scope> is disabled, so transport sessions under <path> are refused - enable the provider or re-point the binding), no enabled provider at all, or the several-enabled ambiguity above.
  2. Re-points at a referenced host when host_ref is bound. A step carrying the infrastructure pack's host_ref input resolves the whole seam at the referenced infrastructure host node instead: the host's own rpc.provider_ref, else the binding at the host's path, and the grant is enforced with the host node's path as the target. An unknown, non-host, deregistered or destroyed reference each fail the step by name.
  3. Enforces the run principal's transport grant. The request carries the creating user's identity and effective groups, its service-account facet, the calling agent's fleet groups, and whether any of the principal's live sessions holds a step-up elevation. No covering active grant refuses: this run's principal holds no active transport grant on provider "<code>" covering <path> for action "<action>" - request one through the approvals funnel (see: opschain approvals). A grant that covers but demands step-up the principal lacks refuses naming the grant; an over-cap request refuses naming the cap, its limit and when the window resets. Cap counters are per server process — a cap bounds a loop, it is not a ledger.
  4. Resolves the credential with platform authority — an unqualified reference through the platform's governing secret backend, a provider-qualified one through its named secrets provider — and mints the session bundle: the rpcu credentials document (--credentials-stdin schema version 1), carrying the resolved key or password for ssh-static, or a freshly-signed short-lived certificate plus ephemeral key for ssh-ca. The bundle is pooled by provider reference and generation, never raw material, so a credential rotation forces a fresh dial.

The bundle lands in OPSCHAIN_TRANSPORT_SESSION as a masked secret env value: the credential and the bundle join the run mask set, never land in the step context, logs or events, and the step's setup preamble carries identifiers only — the provider code, its selection provenance, the admitting grant and (for ssh-ca) the certificate fingerprint and TTL.

A local (--local) run makes the same hand-off from transport-providers.yaml in $OPSCHAIN_HOME: each definition names a code, a kind and a credential as a secret-vault:// reference into the local secret store — a literal value refuses by name. Selection mirrors the server precedence without a binding tree (deepest rpc.provider_ref property, else the single defined provider, else a named ambiguity), and the local lane is single-user, so there is no grant enforcement.

Settings and configuration​

No config.yaml key tunes this component. The levers are the records themselves — providers, grants and bindings — plus two per-node properties: rpc.provider_ref (an explicit provider selection that overrides any binding) and the rpc.* connection profile the pack's actions read (opschain docs packs-rpc). The local lane reads $OPSCHAIN_HOME/transport-providers.yaml instead of the registry.

Governance and audit​

Every provider, grant and binding verb sits behind the /admin updatable determination (permission_denied on deny), and every write is audited as an ordinary event:

  • api:transport_providers:create / update / destroy — record lifecycle (connection facts and the credential reference, never material); api:transport_providers:test — a record validation ran.
  • api:transport_provider_bindings:create / update / destroy — a binding declared, re-pointed or removed.
  • api:transport_grants:create / approve / reject / revoke — grant lifecycle, the funnel's approve/reject included.

Dispatch enforcement is audited per attempt: every refused session lands warn:transport:refused, every over-cap request warn:transport:cap_refused, and every minted session info:transport:session (with the certificate fingerprint and TTL for ssh-ca) — payloads carry the provider, principal, target path, action and admitting grant, identifiers only. The full catalog is on opschain docs events.

The lane is pack-gated behind packs:rpc — a packs:infrastructure grant satisfies the same gate, never the reverse — with the polite-lapse posture: provider create and enable, grant create and direct approve, and binding set refuse on lapse with the typed ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED refusal naming the pack, while stored records keep serving dispatch, reads and the test verb stay up, and disable, delete, reject, revoke and binding deletes stay open so a lapsed estate can always unwind authority. See entitlements and packs-rpc.

  • opschain docs cli/admin-transport-providers — the catalog family (create, list, show, update, delete, test), and opschain docs cli/admin-transport-providers-bindings — the binding verbs (set, list, resolve, delete).
  • opschain docs cli/admin-transport-grants — the grant family (create, approve, reject, revoke, list, show).
  • opschain docs cli/approvals — the inbox a held grant is approved through.
  • The v2 services are TransportProviderService (providers and bindings) and TransportGrantService — procedure-by-procedure detail on opschain docs api-v2.
  • opschain docs runner-executors — the step environment contract the minted session rides.