Secure rstream Agent Authentication with Keychain and PKCS#11
Store rstream agent tokens in macOS Keychain, use macOS Keychain for mTLS agent identities, and keep mTLS private keys inside a YubiKey, HSM, or SoftHSM through PKCS#11.
This guide shows how to harden the credentials used by an rstream agent, CLI, or SDK runtime when it connects to the rstream engine control channel. That connection is what lets the agent create tunnels, attach streams, dial private tunnels, and publish local services.
The guide covers three hardened storage models. On macOS, bearer tokens can be stored in Keychain instead of the YAML configuration file. On macOS, mTLS agent identities can also live in Keychain so the private key is used through the platform security framework. On Linux, macOS, and embedded systems, mTLS private keys can stay inside a YubiKey, HSM, smart card, TPM-backed module, or SoftHSM test token through PKCS#11.
This is agent authentication. It is separate from mTLS on a published tunnel endpoint. When a published tunnel enables mtls_auth, the client that connects to that public endpoint, such as curl, a browser, a mobile app, or another device, must be provisioned with its own client certificate. That downstream client certificate is not the same credential as the one used by the rstream agent to authenticate to the engine.
For the field-by-field reference, see Credential Storage. For the broader mTLS model, including published tunnel access, see mTLS.
Contents
- Choose a storage model
- Prepare a project and engine context
- Store an agent token in macOS Keychain
- Use a macOS Keychain mTLS identity
- Use a YubiKey or HSM with PKCS#11
- Test PKCS#11 locally with SoftHSM
- Run and verify the agent
- Operate the credential over time
- Troubleshooting
Choose a storage model
Start from the credential you want the agent to use, then choose the storage backend that matches the host.
| Agent credential | Recommended storage | Typical host |
|---|---|---|
| Bearer token | macOS Keychain | macOS laptops, build agents, operator workstations |
| Bearer token | Platform secret injection | Linux services, containers, CI runners |
| mTLS private key | macOS Keychain | Signed macOS agents where Keychain ACLs are part of the trust model |
| mTLS private key | PKCS#11 | Linux services, embedded devices, YubiKeys, smart cards, HSMs |
| mTLS private key | SoftHSM through PKCS#11 | Local validation and CI coverage before using hardware |
PKCS#11 is for private-key operations. A bearer token is a string secret, so the useful protection is a platform secret store or a short-lived injected environment value. Use PKCS#11 when the agent authenticates with mTLS and the private key should stay inside the token or module.
macOS Keychain support depends on a stable binary identity. Use the signed rstream release binary, or sign your internal build with the certificate identity that your organization trusts. The security value comes from Keychain access control: the item is tied to an allowed application identity, and unrelated binaries cannot use it as the same trusted application.
Prepare a project and engine context
The hardened storage options protect the local secret. The credential still needs to be authorized by rstream. For token-based agent authentication, use a token that can create the tunnels the agent needs. For mTLS agent authentication, register the public certificate as an mTLS Client Certificate (MTLS) credential and grant Engine API permissions to that credential.
The examples in this guide use the hosted project model, or a private deployment with project-backed authentication, when they reference RSTREAM_PROJECT_ENDPOINT or the hosted Control plane API. Self-hosted CE is different: it has no projects, does not use project endpoints, and currently supports token-based agent authentication rather than agent mTLS. For CE, use the base engine host directly and store the CE JWT in macOS Keychain on macOS or inject it from your Linux service manager or secret manager.
For a first validation, keep three values in the shell:
export RSTREAM_ENGINE="<engine-host:port>"
export RSTREAM_PROJECT_ENDPOINT="<project-endpoint>"If you already have a normal rstream context, inspect it before creating the hardened one:
rstream context list
rstream context get <context-name> -o yamlThe hosted Control plane can resolve the engine for a project when you have token access. Long-lived mTLS-only agents normally store the resolved engine address in their context, because the Control plane API itself is token-authenticated. In self-hosted CE, there is no resolution step: the engine is the base host you operate.
The mTLS examples below use a dedicated config file so the guide does not overwrite an existing default CLI configuration:
mkdir -p "$HOME/.rstream"
export RSTREAM_CONFIG="$HOME/.rstream/hardened-agent.yaml"Store an agent token in macOS Keychain
This path keeps token-based agent authentication and moves the token value out of ~/.rstream/config.yaml.
First verify that the rstream binary has a stable macOS signature:
RSTREAM_BIN="$(command -v rstream)"
codesign --verify --strict "$RSTREAM_BIN"
codesign -dv "$RSTREAM_BIN" 2>&1 | sed -n '/Identifier=/p;/TeamIdentifier=/p;/Authority=/p'Use the official signed release or an internally signed build. If you replace the binary with another build signed by a different identity, re-check Keychain access before treating the host as production-ready.
Log in and store the token in Keychain:
rstream login --token-storage macos-keychainFor non-browser setup, read the token from stdin so it stays out of shell history:
read -r -s -p "rstream token: " RSTREAM_TOKEN
echo
printf "%s" "$RSTREAM_TOKEN" \
| rstream login --token-stdin --token-storage macos-keychain
unset RSTREAM_TOKENThe environment-level configuration now contains a Keychain reference instead of the token value:
environments:
- auth:
token:
storage:
kind: keychain
provider: macos
service: io.rstream.auth
account: api:https://rstream.ioFor a hosted project context, create the context with token storage enabled:
read -r -s -p "rstream token: " RSTREAM_TOKEN
echo
printf "%s" "$RSTREAM_TOKEN" \
| rstream context create prod-keychain-token \
--project-endpoint "$RSTREAM_PROJECT_ENDPOINT" \
--token-stdin \
--token-storage macos-keychain \
--default
unset RSTREAM_TOKENCheck that the configuration references Keychain:
grep -nA8 'prod-keychain-token' "$HOME/.rstream/config.yaml"
grep -n 'value:' "$HOME/.rstream/config.yaml" || trueThe second command should not print the token. If value: contains a bearer token, recreate the context with --token-storage macos-keychain.
Use a macOS Keychain mTLS identity
This path uses a certificate-backed agent identity. rstream authorizes the certificate fingerprint, while macOS Keychain holds the private key and performs the signing operation.
Generate a client certificate if you do not already have one:
openssl genpkey \
-algorithm EC \
-pkeyopt ec_paramgen_curve:P-384 \
-out rstream-agent.key
chmod 600 rstream-agent.key
openssl req \
-new \
-x509 \
-key rstream-agent.key \
-sha384 \
-days 397 \
-subj "/CN=rstream-agent-macos-01" \
-addext "keyUsage=digitalSignature" \
-addext "extendedKeyUsage=clientAuth" \
-out rstream-agent.crtRegister rstream-agent.crt as an mTLS Client Certificate (MTLS) credential in the Dashboard. Give it the Engine API permissions required by this agent, such as tunnel creation and resource listing for a publishing agent.
For automation, the same registration can be done with the Control plane API. The token used here must have credential-management permission:
command -v jq
read -r -s -p "control token: " RSTREAM_CONTROL_TOKEN
echo
jq -n --rawfile cert rstream-agent.crt '{
type: "mtls",
name: "rstream-agent-macos-01",
certificatePem: $cert,
permissionPolicy: {
control: { mode: "none", permissions: [] },
engine: {
mode: "select",
permissions: [
"tunnels.resources.read-only",
"tunnels.tunnels.create-delete"
]
}
}
}' | curl -fsS \
-X POST "https://rstream.io/api/credentials" \
-H "Authorization: Bearer $RSTREAM_CONTROL_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @-
unset RSTREAM_CONTROL_TOKENImport the private key and certificate into the login keychain. The -T argument restricts the imported identity to the rstream binary path.
RSTREAM_BIN="$(command -v rstream)"
openssl pkcs12 \
-export \
-inkey rstream-agent.key \
-in rstream-agent.crt \
-out rstream-agent.p12 \
-name rstream-agent-macos-01
security import rstream-agent.p12 \
-k "$HOME/Library/Keychains/login.keychain-db" \
-T "$RSTREAM_BIN"Avoid importing with an allow-all access policy. The point of this setup is that the signed rstream binary is the application allowed to use the identity.
Compute the certificate fingerprint:
CERT_SHA256="$(
openssl x509 \
-in rstream-agent.crt \
-outform DER \
| openssl dgst -sha256 -binary \
| xxd -p -c 256
)"
printf '%s\n' "$CERT_SHA256"Create or update a context that uses the Keychain identity:
: "${RSTREAM_CONFIG:=$HOME/.rstream/hardened-agent.yaml}"
mkdir -p "$(dirname "$RSTREAM_CONFIG")"
cat > "$RSTREAM_CONFIG" <<EOF
version: 1
defaults:
context:
name: prod-mtls-keychain
contexts:
- name: prod-mtls-keychain
engine: ${RSTREAM_ENGINE}
auth:
mtls:
storage:
kind: keychain
provider: macos
certificateSHA256: "${CERT_SHA256}"
EOFThe YAML stores only the certificate fingerprint. The private key remains in Keychain.
Use a YubiKey or HSM with PKCS#11
This path is the portable hardware-backed model for mTLS agent authentication. The rstream agent loads the vendor PKCS#11 module, selects a token and a private key, and asks the module to sign the TLS handshake. The private key is not exported into the rstream process.
The example below uses a YubiKey PIV slot. The same rstream configuration shape is used for other PKCS#11 modules, including smart cards, network HSMs, USB HSMs, and TPM-backed modules.
Install the vendor tooling and the generic PKCS#11 inspection tools:
# macOS
brew install opensc yubico-piv-tool
# Ubuntu / Debian
sudo apt-get update
sudo apt-get install -y opensc yubico-piv-tool yubikey-managerThe provisioning steps use ykman, the YubiKey Manager CLI. On macOS, install YubiKey Manager from Yubico if ykman is not already present:
command -v ykmanFind the PKCS#11 module path. Common values are:
| Platform | Typical module |
|---|---|
| macOS with Homebrew | /opt/homebrew/lib/libykcs11.dylib |
| Intel macOS with Homebrew | /usr/local/lib/libykcs11.dylib |
| Linux with Yubico PIV tool | /usr/lib/x86_64-linux-gnu/libykcs11.so |
| Linux with OpenSC | /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so |
Set the value for the rest of the commands:
export PKCS11_MODULE="/opt/homebrew/lib/libykcs11.dylib"If the YubiKey is not already provisioned, generate a key in PIV slot 9a and create a self-signed certificate:
ykman piv keys generate \
--algorithm ECCP256 \
--pin-policy ONCE \
--touch-policy ALWAYS \
9a rstream-yubikey.pub
ykman piv certificates generate \
--subject "CN=rstream-agent-yubikey-01" \
--valid-days 397 \
9a rstream-yubikey.pub
ykman piv certificates export \
--format PEM \
9a rstream-yubikey.crtIf your organization issues the certificate from its own CA, generate a CSR instead, have it signed, then import the signed certificate into the same PIV slot:
ykman piv certificates request \
--subject "CN=rstream-agent-yubikey-01" \
9a rstream-yubikey.pub rstream-yubikey.csr
# Submit rstream-yubikey.csr to your CA, then import the returned certificate.
ykman piv certificates import \
--verify \
9a rstream-yubikey.crtInspect the token and object labels through PKCS#11:
pkcs11-tool --module "$PKCS11_MODULE" --show-info
pkcs11-tool --module "$PKCS11_MODULE" --list-slots
pkcs11-tool --module "$PKCS11_MODULE" --login --list-objectsYubiKey PIV object labels are fixed by the PIV slot. For slot 9a, the private key label exposed by YKCS11 is usually Private key for PIV Authentication. Use the label shown by pkcs11-tool on your host.
Register rstream-yubikey.crt as an mTLS Client Certificate (MTLS) credential in the Dashboard, or through the API command from the macOS mTLS section with --rawfile cert rstream-yubikey.crt.
Create the rstream context:
: "${RSTREAM_CONFIG:=$HOME/.rstream/hardened-agent.yaml}"
mkdir -p "$(dirname "$RSTREAM_CONFIG")"
read -r -s -p "PKCS#11 PIN: " RSTREAM_PKCS11_PIN
echo
export RSTREAM_PKCS11_PIN
cat > "$RSTREAM_CONFIG" <<EOF
version: 1
defaults:
context:
name: prod-mtls-pkcs11
contexts:
- name: prod-mtls-pkcs11
engine: ${RSTREAM_ENGINE}
auth:
mtls:
storage:
kind: pkcs11
module: ${PKCS11_MODULE}
# C++ only, when the OpenSSL provider is not named pkcs11prov:
# opensslProvider: pkcs11
tokenLabel: "YubiKey PIV"
keyLabel: "Private key for PIV Authentication"
certificateFile: $(pwd)/rstream-yubikey.crt
pinEnv: RSTREAM_PKCS11_PIN
EOFFor portable Go/C++ configs, prefer tokenLabel or tokenSerial, keyLabel or keyIdHex, and certificateFile. Go also accepts slot; C++ rejects slot so that unsupported selectors do not look portable by accident.
The C++ SDK uses OpenSSL for PKCS#11. On OpenSSL 3, the provider name depends on the package. Homebrew libp11 exposes pkcs11prov; Ubuntu/Debian pkcs11-provider exposes pkcs11.
# macOS Homebrew with libp11.
export OPENSSL_MODULES=/opt/homebrew/lib/ossl-modules
export OPENSSL_PKCS11_PROVIDER=pkcs11prov
openssl list -providers -provider default -provider "$OPENSSL_PKCS11_PROVIDER"
# Ubuntu / Debian with pkcs11-provider.
export OPENSSL_PKCS11_PROVIDER=pkcs11
openssl list -providers -provider default -provider "$OPENSSL_PKCS11_PROVIDER"If the provider name is not pkcs11prov, set opensslProvider in the PKCS#11 storage block. The module field still points to the HSM vendor module; opensslProvider names the OpenSSL provider that loads it.
OpenSSL 3.5 and later let rstream pass provider parameters at runtime. Older OpenSSL 3 deployments, including Ubuntu 24.04, require provider-specific settings through openssl.cnf or OPENSSL_CONF; the SoftHSM section below shows that setup.
Test PKCS#11 locally with SoftHSM
SoftHSM is useful before touching production hardware. It exposes a PKCS#11 module and can create non-exportable keys inside an isolated token directory. The private key is software-backed, but the runtime path is the same class of integration as a hardware module.
Install the tools:
# macOS
brew install softhsm opensc libp11
# Ubuntu / Debian
sudo apt-get update
sudo apt-get install -y softhsm2 opensc pkcs11-providerCreate an isolated token store:
WORKDIR="$(mktemp -d)"
cd "$WORKDIR"
cat > softhsm2.conf <<EOF
directories.tokendir = ${WORKDIR}/tokens
objectstore.backend = file
log.level = ERROR
slots.removable = false
EOF
mkdir -p tokens
export SOFTHSM2_CONF="${WORKDIR}/softhsm2.conf"
export RSTREAM_PKCS11_PIN="123456"Set the SoftHSM module path and, when OpenSSL is used, the provider name matching your host:
case "$(uname -s):$(uname -m)" in
Darwin:arm64)
export PKCS11_MODULE="/opt/homebrew/lib/softhsm/libsofthsm2.so"
export OPENSSL_MODULES="/opt/homebrew/lib/ossl-modules"
export OPENSSL_PKCS11_PROVIDER="pkcs11prov"
;;
Darwin:*)
export PKCS11_MODULE="/usr/local/lib/softhsm/libsofthsm2.so"
export OPENSSL_MODULES="/usr/local/lib/ossl-modules"
export OPENSSL_PKCS11_PROVIDER="pkcs11prov"
;;
Linux:*)
export PKCS11_MODULE="/usr/lib/softhsm/libsofthsm2.so"
export OPENSSL_MODULES="$(openssl version -m | sed -n 's/^MODULESDIR: "\(.*\)"$/\1/p')"
export OPENSSL_PKCS11_PROVIDER="pkcs11"
;;
esacInitialize a token and generate an EC signing key inside it:
softhsm2-util \
--init-token \
--free \
--label RSTREAM \
--so-pin 12345678 \
--pin "$RSTREAM_PKCS11_PIN"
pkcs11-tool \
--module "$PKCS11_MODULE" \
--login \
--pin "$RSTREAM_PKCS11_PIN" \
--keypairgen \
--key-type EC:prime256v1 \
--label rstream-client \
--id 01 \
--usage-signCreate a self-signed certificate using the token key. Homebrew libp11 reads PKCS11_MODULE_PATH; Ubuntu/Debian pkcs11-provider is configured through an OpenSSL config file:
export PKCS11_MODULE_PATH="$PKCS11_MODULE"
export PKCS11_PIN="$RSTREAM_PKCS11_PIN"
OPENSSL_PKCS11_ARGS=()
if [ "$OPENSSL_PKCS11_PROVIDER" = "pkcs11" ]; then
cat > "$WORKDIR/openssl-pkcs11.cnf" <<EOF
openssl_conf = openssl_init
[openssl_init]
providers = provider_sect
[provider_sect]
default = default_sect
pkcs11 = pkcs11_sect
[default_sect]
activate = 1
[pkcs11_sect]
module = ${OPENSSL_MODULES:-/usr/lib/x86_64-linux-gnu/ossl-modules}/pkcs11.so
pkcs11-module-path = ${PKCS11_MODULE}
pkcs11-module-token-pin = ${RSTREAM_PKCS11_PIN}
activate = 1
EOF
export OPENSSL_CONF="$WORKDIR/openssl-pkcs11.cnf"
openssl list -providers
else
OPENSSL_PKCS11_ARGS=(-provider "$OPENSSL_PKCS11_PROVIDER" -provider default)
openssl list -providers -provider default -provider "$OPENSSL_PKCS11_PROVIDER"
fi
openssl req \
-new \
-x509 \
"${OPENSSL_PKCS11_ARGS[@]}" \
-key "pkcs11:token=RSTREAM;object=rstream-client;type=private" \
-sha256 \
-days 397 \
-subj "/CN=rstream-agent-softhsm-01" \
-out rstream-softhsm.crtRegister rstream-softhsm.crt as an mTLS Client Certificate (MTLS) credential, then create a context:
: "${RSTREAM_CONFIG:=$HOME/.rstream/hardened-agent.yaml}"
mkdir -p "$(dirname "$RSTREAM_CONFIG")"
cat > "$RSTREAM_CONFIG" <<EOF
version: 1
defaults:
context:
name: prod-mtls-softhsm
contexts:
- name: prod-mtls-softhsm
engine: ${RSTREAM_ENGINE}
auth:
mtls:
storage:
kind: pkcs11
module: ${PKCS11_MODULE}
opensslProvider: ${OPENSSL_PKCS11_PROVIDER}
tokenLabel: RSTREAM
keyLabel: rstream-client
certificateFile: ${WORKDIR}/rstream-softhsm.crt
pinEnv: RSTREAM_PKCS11_PIN
EOFThe key remains in the SoftHSM token directory. Removing $WORKDIR deletes the test token.
Run and verify the agent
Use a small local HTTP server so the tunnel has a real upstream:
python3 -m http.server 8080 --bind 127.0.0.1In another terminal, select the hardened context and start a tunnel:
export RSTREAM_CONFIG="$HOME/.rstream/hardened-agent.yaml"
export RSTREAM_CONTEXT="prod-mtls-pkcs11"
rstream forward 8080 --http --publish --name hardened-agent-checkUse the context name that matches the path you configured:
| Path | Context name in this guide |
|---|---|
| macOS token Keychain | prod-keychain-token |
| macOS mTLS Keychain | prod-mtls-keychain |
| YubiKey or HSM PKCS#11 | prod-mtls-pkcs11 |
| SoftHSM PKCS#11 | prod-mtls-softhsm |
Successful tunnel creation proves that the agent authenticated to the engine with the selected credential. For mTLS storage, the rstream engine checked the registered certificate fingerprint, and the local runtime used the private key through Keychain or PKCS#11.
Run one negative check before considering the rollout complete. For PKCS#11, unset the PIN and confirm that the agent fails before it can create a tunnel:
unset RSTREAM_PKCS11_PIN
RSTREAM_CONTEXT="prod-mtls-pkcs11" \
rstream forward 8080 --http --publish --name pkcs11-pin-requiredRestore the PIN after the check:
read -r -s -p "PKCS#11 PIN: " RSTREAM_PKCS11_PIN
echo
export RSTREAM_PKCS11_PINFor macOS Keychain mTLS, test with a wrong fingerprint in a temporary copy of the config. The agent should fail because the referenced identity cannot be found or because the registered rstream credential does not match the certificate presented by the agent.
Operate the credential over time
Treat the rstream agent identity as a machine identity. One agent, device, service, or workload should have its own credential so revocation can be targeted.
For tokens, prefer the smallest permissions that let the agent perform its job. Rotate the token from the Dashboard or API, then rerun the Keychain login or context creation command that stores the replacement token.
For mTLS credentials, rotation means creating a new key pair, registering the replacement certificate as a new mTLS Client Certificate (MTLS) credential, updating the local Keychain or PKCS#11 object, and then switching the rstream config to the replacement fingerprint or certificate file. After the new agent connection succeeds, revoke the old mTLS credential.
For PKCS#11 agents, keep the PIN in the service manager or secret manager that starts the process. In a systemd deployment, use an environment file with restrictive permissions or a native secret integration. In CI, use the CI secret store. In Kubernetes, use a Secret mounted or injected into the agent environment, and avoid writing the PIN into the rstream YAML file.
For macOS agents, import Keychain identities after the final signed binary is installed. If you rotate from a development build to a release build, validate the binary signature again and update Keychain access for the production binary.
Troubleshooting
If the agent reports that authentication is missing, confirm that the selected context is the hardened context:
printf 'RSTREAM_CONTEXT=%s\n' "${RSTREAM_CONTEXT:-}"
rstream context listIf token Keychain lookup fails on macOS, inspect the config reference and repeat the login with --token-storage macos-keychain. The account should match the API URL or context name shown in ~/.rstream/config.yaml.
If macOS mTLS Keychain lookup fails, verify that the certificate fingerprint in the config is the SHA-256 digest of the certificate stored with the private key:
openssl x509 \
-in rstream-agent.crt \
-outform DER \
| openssl dgst -sha256 -binary \
| xxd -p -c 256If a macOS Keychain prompt appears for a different binary, stop and check the binary path and signature. The trusted application should be the signed rstream binary that will run in production.
If PKCS#11 cannot find the token, list slots through the same module path used in the rstream config:
pkcs11-tool --module "$PKCS11_MODULE" --list-slots
pkcs11-tool --module "$PKCS11_MODULE" --login --list-objectsIf C++ PKCS#11 fails before connecting, verify the OpenSSL provider:
openssl list -providers -provider default -provider "$OPENSSL_PKCS11_PROVIDER"If OpenSSL cannot load the provider, install the provider package, set OPENSSL_MODULES when the module is outside OpenSSL's default directory, and set opensslProvider in the rstream config when the provider name is not pkcs11prov.
If rstream rejects the mTLS connection, confirm that the exact certificate presented by the agent is registered as an mTLS Client Certificate (MTLS) credential and that the credential has Engine API permissions for the attempted operation.
If you are configuring mTLS for clients that connect to a published tunnel, use mTLS instead. That flow provisions certificates for downstream clients such as browsers, curl, mobile apps, or devices. The storage backends in this guide protect the rstream agent's own connection to the engine.