In a Specter integration, a Link backend is the external risk engine Specter calls. Specter reaches it over
the
specter-v1 protocol — see Specter backends.Anatomy
id— a short identifier you choose, used to address the backend (for example when disambiguating an invocation).protocol— the protocol’s$idURL; must match an imported protocol.host— the provider’s hostname (no scheme, no path).credentials— consumed by the authentication pipeline; encrypted at rest.auth_pipeline— how outbound requests are authenticated (see below).connections— a map from action (oraction.variant) to its mapping configuration.tls— optional: the client certificate the backend presents (see Client certificates).
Authentication strategies
Theauth_pipeline determines how Link authenticates outbound calls to the provider. A non-empty pipeline
requires a source_type (inline or vault — where the credentials come from) and a credential_type:
Digest, etc.) take precedence over mapped headers on conflict, so signing
guarantees are never overwritten.
Credentials and secrets
Credentials are held either inline (encrypted at rest in the backend record) or pulled from a secrets store through a pointer, so the secret value itself never lives in the backend document — Link fetches it at request time. Inline credentials can be rotated in place with the rotate-credentials endpoint, without recreating the backend.Provisioning: self vs. managed
A backend declares a provisioning mode:self(default) — you own and manage the backend and its credentials.managed— the backend is provisioned and operated by Hellgate on your behalf (an optional service). Its create, update, and delete operations require the additionaladmin:managed-backends:writescope on top ofadmin:backends:write.
Mapping
Mapping is declarative: adapter logic lives in versioned templates, not in code. Each connection has arequest_mapping and a response_mapping.
Template grammar
Mapping leaves are plain JSON values. Strings carry a mustache-style grammar — zero or more{{ expr }}
holes inside a literal string — where expr is a scoped path followed by an optional chain of filters:
Filters chain with
|. Common filters include required (abort the call if the value is missing),
default(<path or literal>) (fall back to another value), omit_if_null (drop the key when null),
map({ FROM: TO }) (translate enum values), to_int, and first / last / prefix for string slicing.
A map value can also be a path, which Link resolves only when that entry matches. An optional default:
argument, a literal or a path, applies when the input is not a key of the dict or is null. Without default:,
an input that matches no key passes through unchanged. Chained, two map filters combine two provider fields
into one protocol value:
actionCode of 00 passes the provider’s nameMatch on to the second map, and any other code, a
missing value, or an unknown match result ends in BLOCK.
Every path in a template is checked against its scope, including paths inside filter arguments: a request
mapping that uses $.response.* anywhere, for example in default(...), is rejected when you save the backend.
Request mapping
request_mapping mirrors the provider’s expected request. It has an optional body (a nested object matching
the provider’s shape) and headers (a flat map of header name to template):
Response mapping
response_mapping is keyed by the provider’s HTTP status (200, 4xx, default, …). Each entry has a
return (the HTTP status Link returns to the caller) and a body template that produces the protocol-shaped
result:
body templates are validated at backend-write time against the protocol’s response schema for the
given return code — missing required keys, unknown keys, and unmatched union branches are rejected before the
backend goes live.
Connection keys
A connection key is either the bare action name (assess) or action.variant (assess.pan) when the action
has a discriminator. Link validates connections against the imported protocol and rejects a backend that misses a
required action or variant.
Mocks
Instead of live mapping, a connection can declare mock responses — match conditions plus canned responses. When present, invocations are matched against the mocks and served without any external call. Mocks are ideal for local development, tests, and protocol fixtures.Message-level encryption
Some providers require every request and response body to be encrypted on top of TLS (JWE). A backend can carry anencryption block that makes Link encrypt outbound bodies and decrypt inbound ones transparently, so callers
keep sending and receiving plain JSON. Encryption keys are managed separately and support rotation.
Link decrypts every response that carries the encryption envelope, whatever its status. Some providers answer
errors unencrypted: a non-2xx response without the envelope is mapped as received, through the connection’s
response_mapping, as for a backend without encryption. A 2xx response without the envelope fails with
502 and the code DECRYPT_FAILED, because a successful answer must be encrypted.
Client certificates (mutual TLS)
Some providers accept a call only when the client presents a certificate they issued. Link stores that certificate and its private key as a key with purposeclient_tls, and a backend names the key in its tls
field. Every call to that backend then presents the certificate in the TLS handshake. Backends without tls
present no client certificate.
Upload the certificate
Managing keys requires theadmin:keys:write scope. The key_material of a client_tls key is one PEM
bundle, in this order:
- The certificate issued to you (the leaf), first.
- Its intermediate certificates, in chain order, if the provider issues any.
- Exactly one unencrypted RSA or EC private key that belongs to the leaf. PKCS#1
(
RSA PRIVATE KEY), SEC 1 (EC PRIVATE KEY), and PKCS#8 (PRIVATE KEY) forms are accepted. AnEC PARAMETERSblock is allowed and ignored.
client_tls key has no algorithm and no encryption_method: leave both out, because setting either one
is rejected. Leave out not_before and not_after too: Link takes them from the leaf certificate, and
setting either one is rejected (null is accepted).
201 with the key’s metadata. algorithm and encryption_method are null, not_before and
not_after are the leaf certificate’s validity, and the key material is never returned:
422 VALIDATION_ERROR on key_material when it is not PEM,
holds no certificate, holds no private key or more than one, holds an encrypted key or a key that is neither
RSA nor EC (for example Ed25519), holds any other kind of entry, or when the private key does not match the
first certificate. A bundle that lists an intermediate before the leaf fails for that reason. It also fails
when any certificate in the bundle, the leaf, an intermediate or a root, has expired or is not yet valid. The
messages never repeat the material or a date from it. Like every key, the material is encrypted at rest and
never appears in responses, logs, or error results.
Reference the key from a backend
Settls.client_key_kid when you create the backend, or when you update a disabled backend:
- The key must exist and have purpose
client_tls. Otherwise the save fails with422VALIDATION_ERROR. tlsis a closed object:client_key_kidis required and must be a non-empty string, and any other field is rejected."tls": nullremoves the reference.- A
client_tlskey cannot be used in a backend’sencryptionblock.
tls, with null for a backend without one.
A key that any backend’s tls.client_key_kid names, enabled or not, cannot be deleted:
DELETE /api/admin/keys/{id} answers 409 CONFLICT and the key stays. Point the disabled backend’s tls at
another key, or set it to null, first.
How calls change
- Every call presents the leaf certificate and its chain, and proves possession of the private key in the handshake.
- Calls with a client certificate use HTTP/1.1. Their idle connections close after 5 minutes without a request, and the next call opens a new connection.
- If the key cannot be used when a call is made, Link sends no request. The result is an error with
"source": "transport", status502, and the code:client_tls_key_not_found(the key no longer exists or no longer has purposeclient_tls) or:client_tls_key_invalid(the stored material holds no usable certificate and key). - If the provider rejects the certificate, for example because it does not trust its issuer, the call also
fails as a transport error with status
502.
tls can add a client certificate.
A custom server CA per backend is not supported.
Rotate the certificate
An enabled backend accepts no change other than being disabled, so a rotation takes four steps, and the backend takes no traffic between steps 2 and 4:1
Rotate the key
POST /api/admin/keys/{id}/rotate with a new kid and the new bundle. The new key is validated like a new
upload, including the validity of every certificate, and takes the new leaf’s not_before and not_after.
The old key stays.2
Disable the backend
PUT /api/admin/backends/{id} with {"enabled": false}.3
Point the backend at the new key
PUT /api/admin/backends/{id} with {"tls": {"client_key_kid": "<new kid>"}}.4
Enable the backend
PUT /api/admin/backends/{id} with {"enabled": true}. You can then delete the old key.Selecting a backend
When several enabled backends implement the same action and variant, the caller disambiguates with abackend
query parameter on invocation. See Invocation.
Next steps
Invocation
Call an action, match the method, select a backend, and handle errors.
Protocols
The contract a backend implements.