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 held encrypted at rest
on the record itself (never written into a vault): credential_ref stays
empty, credential_set reports it, and dispatch dials with it without a
secrets provider registered. A reference write clears the at-rest value
and an inline write clears the 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
| Attribute | Meaning |
|---|---|
code | The estate code bindings and node properties reference. Immutable after create. |
display_name | Human-readable name. |
kind | ssh-static or ssh-ca. Immutable after create. |
config_json | Kind-specific connection facts as a JSON object — never credential material; unknown keys refuse. |
credential_ref | The canonical secret-vault:// reference the credential lives at; empty when the credential is held encrypted at rest on the record. |
credential_set | Whether a credential is held — a pinned reference or an inline value encrypted at rest. |
enabled | Whether dispatch resolves sessions through the provider. |
last_test_at / last_test_success / last_test_error | The 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 carriesusername,auth_method,portandconnect_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'sprincipals(orusernameas the single principal — one of the two is required),validity_secondsbetween 1 and 86400 (default 300 — five minutes), the validity backdated sixty seconds to absorb clock skew, and a key id ofopschain-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_refto 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:rpcentitlement; 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-staticas an SSH private key or password per the config'sauth_method, forssh-caby signing a probe certificate with the CA key. The typed outcome lands on the row aslast_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:
| Field | Meaning |
|---|---|
subject_kind / subject_id | Who the grant admits: user, group, service_account or agent_group — humans and agents gate identically. A user subject is stored and matched as the principal id, never the username: the write accepts a principal id, a username exactly one account holds, or <provider>:<username> (the account's auth_provider value — local:sam, ldap:corp:sam) and refuses a username several accounts hold until it is qualified, so a federated sam never inherits local sam's grant. |
provider | The granted provider's code, or a code glob (nonprod-*). |
scope | The 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/**). |
actions | rpc action patterns (exec, put, get, sync, facts, ensure, globs over them); empty grants every action. |
require_step_up | Session minting demands a stepped-up session from the requesting principal. |
rate_per_minute / sessions_per_day | Session caps in fixed windows (UTC day); 0 is capless. |
expires_at | The validity window's end; unset grants until revoked. |
approvers / note / lifecycle columns | The funnel checklist's approvers (principal ids, resolved from the same spellings as the subject), why the grant exists, and who granted/approved/revoked it when. |
Grants written before user subjects moved to principal ids keep
matching by username through a dual-read window; every such admission
records warn:identity:username_match naming the grant, and
opschain admin identity convert-username-subjects rewrites the stored
usernames to ids — refusing a username several accounts hold until it is
qualified — after which the window is inert.
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
pendingand opens a strict checklist in the one approvals inbox every held subject enters (subject kindtransport_grant— see change governance);--approversis 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:
- Resolves the governing provider, in strict precedence: the
explicit
rpc.provider_refnode 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. - Re-points at a referenced host when
host_refis bound. A step carrying the infrastructure pack'shost_refinput resolves the whole seam at the referenced infrastructure host node instead: the host's ownrpc.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. - 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. - 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
rpcucredentials document (--credentials-stdinschema version 1), carrying the resolved key or password forssh-static, or a freshly-signed short-lived certificate plus ephemeral key forssh-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.
Usage telemetry
Every session a transport provider hands a step is counted on the
provider usage substrate (opschain docs provider-usage): checkouts
(a step handed its session bundle), sessions_minted (an ssh-ca
certificate signed for it) and session_failures (the credential did
not resolve or the bundle did not mint), on the project of the run.
opschain admin providers usage --kind transport --code <code> reads
them; the provider's record opens its operational dashboard.
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.
Related CLI and API
opschain docs cli/admin-transport-providers— the catalog family (create,list,show,update,delete,test), andopschain 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) andTransportGrantService— procedure-by-procedure detail onopschain docs api-v2. opschain docs runner-executors— the step environment contract the minted session rides.