TLS signer plugins support mTLS setups where Restish must present a client certificate but cannot hold the private key material directly. Instead, Restish delegates signing operations to a long-lived external process.
Some mTLS environments use:
- hardware tokens
- PKCS#11-backed keys
- externally managed signing services
In those cases, the normal client_cert and client_key file model is not
enough. Restish still needs a tls.Certificate for the Go TLS stack, but the
private key operations have to stay outside the process boundary.
TLS signer plugins are discovered through the normal plugin manifest path, but
they are treated as a distinct plugin type via the tls-signer hook.
The profile-level config model is:
tls_signer: plugin name or executable nametls_signer_params: plugin-specific key/value parameters
When a request needs mTLS and a TLS signer is configured, Restish resolves the plugin path and starts a persistent signer process. The startup handshake is:
- Restish starts the plugin executable.
- Restish sends an
initmessage containing plugin parameters. - The plugin replies with a
readymessage containing the leaf certificate. - Restish parses the certificate and constructs a
tls.CertificatewhosePrivateKeyis a proxy object that calls back into the plugin.
During the TLS handshake, Go eventually calls Sign(...) on that proxy. The
proxy then:
- sends a
signmessage with the digest and hash identifier - waits for a reply containing either
signatureorerror - returns the signature bytes back to the TLS stack
This gives Restish the certificate material needed to authenticate the client without ever requiring access to the underlying private key.
The signer process lifetime must be bounded by host-owned resources.
The design intent is:
- one signer session per transport or other clearly owned lifetime
- explicit close on request/transport teardown
- context-aware cancellation
- bounded wait before force kill if the signer will not exit
Starting a fresh signer for every transport build without later cleanup is not an acceptable steady-state design.
TLS signer plugins could have been folded into the general command-plugin protocol, but they have a very different operational profile:
- they exist to satisfy the Go TLS stack, not to add user-facing commands
- they need a stable request/reply signer object, not workflow orchestration
- they operate on key material boundaries, which deserves a smaller contract
Documenting them separately makes the security and lifecycle assumptions much clearer.
If the plugin cannot start, does not return ready, returns malformed
certificate data, or dies before a later signing operation, Restish surfaces a
clear error and lets the TLS handshake fail cleanly.
That failure path is important: external signers are optional infrastructure, not something Restish should silently fall back from.
Plugin stderr should be surfaced when it materially helps explain signer failures such as wrong PIN, unavailable slot, or token-session errors.
That keeps the implementation smaller, but it excludes hardware-backed and externally managed mTLS environments that are common in enterprise use.
That would simplify state management, but it would make the protocol slower and more brittle. A persistent signer fits repeated signing during a TLS session better.
Possible, but unnecessarily broad. The TLS signer contract is much smaller and easier to reason about when it stands on its own.
The current implementation lives in
internal/plugin/tls_signer.go,
with request integration in
internal/cli/hooks.go
and profile-level coverage in
internal/cli/tls_signer_test.go.
One detail worth preserving is that TLS signer selection happens through the same profile-driven request options model as the rest of Restish's transport configuration. It is an extension of the mTLS story, not a separate config system.