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

Start from the credential you want the agent to use, then choose the storage backend that matches the host.

Agent credentialRecommended storageTypical host
Bearer tokenmacOS KeychainmacOS laptops, build agents, operator workstations
Bearer tokenPlatform secret injectionLinux services, containers, CI runners
mTLS private keymacOS KeychainSigned macOS agents where Keychain ACLs are part of the trust model
mTLS private keyPKCS#11Linux services, embedded devices, YubiKeys, smart cards, HSMs
mTLS private keySoftHSM through PKCS#11Local 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 yaml

The 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-keychain

For 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_TOKEN

The 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.io

For 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_TOKEN

Check that the configuration references Keychain:

grep -nA8 'prod-keychain-token' "$HOME/.rstream/config.yaml"
grep -n 'value:' "$HOME/.rstream/config.yaml" || true

The 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.crt

Register 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_TOKEN

Import 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}"
EOF

The 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-manager

The provisioning steps use ykman, the YubiKey Manager CLI. On macOS, install YubiKey Manager from Yubico if ykman is not already present:

command -v ykman

Find the PKCS#11 module path. Common values are:

PlatformTypical 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.crt

If 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.crt

Inspect 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-objects

YubiKey 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
EOF

For 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-provider

Create 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"
    ;;
esac

Initialize 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-sign

Create 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.crt

Register 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
EOF

The 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.1

In 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-check

Use the context name that matches the path you configured:

PathContext name in this guide
macOS token Keychainprod-keychain-token
macOS mTLS Keychainprod-mtls-keychain
YubiKey or HSM PKCS#11prod-mtls-pkcs11
SoftHSM PKCS#11prod-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-required

Restore the PIN after the check:

read -r -s -p "PKCS#11 PIN: " RSTREAM_PKCS11_PIN
echo
export RSTREAM_PKCS11_PIN

For 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 list

If 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 256

If 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-objects

If 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.