Credential injection
Let the tools on a workstation call an API that needs a key, while the key stays off the workstation.
You name a host and a header. When a request to that host leaves the workstation, Ringleader adds the key to it from a Secret. The tools on the workstation use the API as if they had signed in, and nothing running on the workstation can read the key, print it or send it anywhere else.
A key in an environment variable or a config file is different. Every process on the workstation can read it, including an AI coding agent told to print it, and it leaves with every copy of the home directory.
You declare injection as an Integration of
type: httpProxy. It works on cloud workstations, and on local workstations on a Mac or a
Linux machine.
Inject a key on a cloud workstation
This example gives the workstations labeled team: platform authenticated access to the
GitHub API.
Store the key in a Secret:
rl secret create github-pat -n acme --from-string token=<your-token>Make sure the namespace has an Edge for the workstation’s cloud and region. An Edge is a small VM in your cloud account, and it is where the key is added. A namespace administrator declares it once for every workstation in that region. On Google Cloud it takes two fields:
apiVersion: core.ringleader.dev/v1 kind: Edge metadata: name: gcp-us-east4 namespace: acme spec: provider: gcp region: us-east4On AWS and Azure the Edge also names a subnet, and the workstations go in a subnet of their own. The Edge page says which.
Apply an Integration that names the host, the header and the Secret:
apiVersion: workstations.ringleader.dev/v1 kind: Integration metadata: name: github-api namespace: acme spec: type: httpProxy selector: matchLabels: team: platform httpProxy: rules: - host: api.github.com inject: header: Authorization value: "Bearer ${secret:github-pat/token}"An Integration you apply as a member reaches only your own workstations. For it to reach everyone’s workstations with the label, a namespace administrator applies it.
Give the workstation the label, in its manifest:
metadata: labels: team: platformWait until
rl workstation describe <name>shows theEgressEnforcedcondition asTrue. For a workstation with no egress policy the reason isCredentialOnly. Then, from a shell on the workstation, call the API with no key:curl https://api.github.com/userThe response is your GitHub user, not a
401.
If that is all you need, you are done. The rest of this page covers local workstations, what changes for a workstation that gets injection, MCP servers, and how to check what is being injected.
Where it works
Where the key is added depends on the workstation’s provider:
| Provider | Where the key is added | What you declare |
|---|---|---|
| GCP, AWS, Azure | On an edge instance, the Edge’s VM in your cloud account. | An Edge for the workstation’s provider and region, in the workstation’s namespace. |
| vz, the default on a Mac | In the workstation’s own network process on your own machine, or on an edge VM when the namespace declares an Edge for vz. | No Edge. |
| qemu, on Linux | In the workstation’s own network process on your own machine, or on an edge VM when the namespace declares an Edge for qemu. | No Edge. |
| lima, the fallback on a Mac | In the Ringleader daemon on your own machine. | No Edge. |
| wsl2 | Nowhere. Use a cloud workstation instead. | |
| hcs, on Windows | In the workstation’s own network process. | No Edge. An Edge cannot be declared for hcs because its own network process is its Edge. |
On a local workstation the key is held on the machine that runs it, so whoever uses that
machine could read it. A rule’s allowDaemonPlacement is a gate that says which local
workstations may hold its key:
- Unset, the default, opens the gate only on a workstation owned by the Integration’s author, the person who first applied it. The author can already read the Secret.
trueopens the gate on every local workstation the Integration reaches, whoever owns it. Set it only when the people who own those workstations may be trusted with the key.falsecloses the gate on every local workstation, the author’s own included.
The field has no effect on a cloud workstation, and a wsl2 workstation holds no key whatever it says.
When a closed gate keeps a rule’s key off a local workstation, the Integration still reaches
the workstation, and requests to the rule’s host go out without the key. A lima workstation
with no egress policy reports this on its EgressCredential condition. An hcs, vz or qemu
workstation reports it on EgressEnforced. See
Check what is being injected.
On a Mac
A Mac runs local workstations on vz by default, and on lima when it cannot run vz. Both can inject, with different trade-offs:
| vz | lima | |
|---|---|---|
| Works on | Apple Silicon Macs with macOS 13 or later | Every Mac |
| Needs an Edge | No. An Edge for vz is optional | No |
| Reads the traffic in | The workstation’s own network process. With an Edge for vz, an edge VM with 2 vCPUs and 1 GiB of memory | The Ringleader daemon |
inspect: true | Supported | Not supported |
A workstation created on lima stays on lima. A new workstation on the same Mac runs on vz, and it gets its key with no Edge too.
On Linux, qemu is the default local provider, and With qemu has its steps.
With vz
On vz, only the Integration is needed. The workstation’s own network process adds the key, so you declare no Edge and no VM is built. Apply the Integration shown under With lima. On a Mac that can run vz, a workstation that names no provider needs nothing more.
Until its network process serves it, the workstation reports EgressEnforced False with
reason GatewayNotServing, and the message says whether it reaches nothing or is still on the
open network. It then reports True, with reason CredentialOnly when it has no egress
policy.
To add the key on an edge VM instead, declare an Edge for vz in the namespace. It takes no region and no size. In your own personal namespace you are its administrator, so you can declare it yourself:
apiVersion: core.ringleader.dev/v1
kind: Edge
metadata:
name: local-vz
namespace: acme
spec:
provider: vzThe Ringleader daemon then builds one edge VM per namespace on your Mac, for the vz
workstations that need one. rl status lists it. Moving a vz workstation behind its edge VM
does not restart it.
A vz workstation created before vz gave each workstation its own address gets no key. With no
Edge for vz, it reports EgressEnforced False with reason NoEdge. It cannot sit behind an
edge VM either, so delete it and create it again.
With lima
On lima, only the Integration is needed. This one reaches only the workstations you own,
and injects your own key into them. Its rule leaves allowDaemonPlacement unset, because you
own every workstation it reaches:
apiVersion: workstations.ringleader.dev/v1
kind: Integration
metadata:
name: my-github-api
namespace: acme
spec:
type: httpProxy
selector:
ownerScope: true
httpProxy:
rules:
- host: api.github.com
inject:
header: Authorization
value: "Bearer ${secret:my-github-pat/token}"The workstation’s connections on ports 80 and 443 then go through the Ringleader daemon on
your Mac. Only the hosts you name are decrypted. The daemon looks up each host name itself,
so the workstation’s own /etc/hosts does not change where a connection goes. A name that
resolves to a private address, such as a service on your company VPN, is refused.
A lima workstation with no egress policy reports the result on its EgressCredential
condition: True with reason CredentialHeld once the key is being added, and False with
reason CredentialPending while it is not. The message names the host and the cause.
With qemu
On qemu, only the Integration is needed. The workstation’s own network process adds the key, so you declare no Edge and no VM is built. Apply the same Integration as for lima.
Until its network process serves it, the workstation reports EgressEnforced False with
reason GatewayNotServing, and the message says whether it reaches nothing or is still on the
open network. It then reports True, with reason CredentialOnly when it has no egress
policy. A qemu workstation with no egress policy restarts once when an Integration first
injects a key into it, and once more when the last one stops, because its network cards change.
To add the key on an edge VM instead, declare an Edge for qemu in the namespace. It takes no region and no size. In your own personal namespace you are its administrator, so you can declare it yourself:
apiVersion: core.ringleader.dev/v1
kind: Edge
metadata:
name: local-qemu
namespace: acme
spec:
provider: qemuThe Ringleader daemon then builds one edge VM per namespace on each machine where a qemu
workstation of that namespace needs one, and rl status lists it. Moving a qemu workstation
behind its edge VM does not restart it, and until the edge VM serves it, the workstation
reaches nothing.
A qemu workstation created before qemu gave each workstation its own address, or before
Ringleader could set up the network cards its own Edge needs, gets no key. With no Edge for
qemu, it reports EgressEnforced False with reason NoEdge. It cannot sit behind an edge VM
either, so delete it and create it again.
What changes for a workstation
Injection changes how a workstation reaches the hosts you name, and it can change how it reaches everything else.
Requests to a named host
For each request to a host you name, Ringleader does four things:
- It sets the header on every request, including each request on a reused connection. A header of that name that the workstation sent is replaced. If a tool refuses to run without a key of its own, give it any placeholder value, because the placeholder never reaches the API.
- It removes the key from the response. Where the key appears in a response header or
body, it is replaced by its
${secret:…}reference, so an API that echoes the request back showsBearer ${secret:github-pat/token}. The key is matched exactly, so an API that echoes it encoded, for example in base64, returns it to the workstation. - It checks the request’s
Hostheader. A request naming a different host from the one the connection was opened to gets421 Misdirected Request, and the connection is closed. - It removes the
Accept-Encodingheader, so responses come back uncompressed. A response compressed with anything other than gzip closes the connection.
To read these requests, Ringleader decrypts the HTTPS traffic to the named hosts. Each workstation gets its own certificate authority, valid only for the hosts named for that workstation. On Debian, Ubuntu and Alpine images, Ringleader installs it in the system trust store and sets these variables:
SSL_CERT_FILE,CURL_CA_BUNDLE,REQUESTS_CA_BUNDLE,GIT_SSL_CAINFOandCARGO_HTTP_CAINFO, set to the combined bundle,/etc/ssl/certs/ca-certificates.crt.NODE_EXTRA_CA_CERTS, set to the Ringleader certificate alone,/usr/local/share/ca-certificates/ringleader-egress-gateway.crt.
So curl, git, Python’s requests, Node.js and Go programs accept the connection with no
setup of your own. These values replace the same variables set in a
WorkstationConfig. A value set in a shell
startup file wins over them.
A tool with its own trust store, or one that checks for one fixed certificate, rejects the
connection. So does a client that only speaks HTTP/2, because a named host is served over
HTTP/1.1. Leave such a host out of your rules, or list it under
exclude.
Everything else
Traffic to every other host is not decrypted, and its bytes are not changed. Ringleader looks up each host name itself and connects to the address it gets, and it refuses a name that resolves to a private address.
What else changes depends on where the workstation runs:
- A cloud workstation with no egress policy is moved behind its edge instance. From there it can reach any public address on any TCP port. It can no longer reach addresses in its own network: on GCP and Azure any private address, and on AWS any address in its VPC. UDP traffic other than DNS is dropped. Until an edge instance serves it, the workstation runs as before, without the key.
- A cloud workstation behind an edge instance also stops answering at its own public
address.
rl shellreaches it through a port on the edge instance instead, as Reaching a workstation behind an Edge explains. - A workstation with an egress policy does not need to list the named hosts in it. Each rule’s host is added to what the workstation may reach, on ports 80 and 443.
- An hcs workstation reaches the network only through its own network process. A vz or qemu workstation reaches it through its own network process, or through its edge VM when the namespace declares an Edge for its provider, as described in With vz and With qemu.
- A lima workstation sends its connections on ports 80 and 443 through the daemon, as described in With lima.
Examples
An internal API that takes an API key
Any header name works except Host. Text around the Secret reference is kept, so a scheme
prefix such as Bearer goes in the value:
apiVersion: workstations.ringleader.dev/v1
kind: Integration
metadata:
name: internal-api
namespace: acme
spec:
type: httpProxy
selector:
matchLabels:
team: platform
httpProxy:
rules:
- host: api.internal.example.com
inject:
header: X-API-Key
value: "${secret:internal-api/key}"An MCP server that needs a key
An Integration can also register an MCP server with Claude Code and Codex on the
workstations it reaches. In injected mode, the default, the server’s key is added on the
way out like any other rule’s, so the workstation holds none:
apiVersion: workstations.ringleader.dev/v1
kind: Integration
metadata:
name: linear-mcp
namespace: acme
spec:
type: httpProxy
selector:
matchLabels:
team: platform
mcp:
name: linear
url: https://mcp.linear.app/mcp
inject:
header: Authorization
value: "Bearer ${secret:linear/token}"Do not also list the server’s host under httpProxy.rules. Two injections into one host
cancel each other out, and neither is used.
An injected server gets its key on cloud workstations, and on a local workstation owned by
the Integration’s author if its provider adds a key. A wsl2 workstation
gets no key, whoever owns it. An mcp block has no allowDaemonPlacement, so on any other
local workstation its requests go out without the key. For a workstation that gets no key,
use local mode, described with the other mcp fields on the
Integration page.
Check a host’s requests without injecting
inspect: true decrypts a host’s traffic and adds nothing. Each request is checked for a
Host header that matches the host the connection was opened to. That stops a request from
being sent to one service under another service’s name:
httpProxy:
rules:
- host: registry.npmjs.org
inspect: trueCloud, hcs, vz and qemu workstations check an inspected host. A lima workstation does not, and reports the host as pending.
Inspection does not make a host reachable. When a workstation has an egress policy, list the
host in that policy before an inspection rule can check it. Inspection adds no destination or
Integration source to the compiled allowlist. A host the policy already names remains allowed and
visible in rl workstation describe; a rule cannot widen a colleague’s policy. A workstation with
no egress policy remains unrestricted, so an inspection rule can still check its named host.
Leave a host alone
exclude lists hosts that are never decrypted, even when a rule names them. Use it for a
client that fails with Ringleader’s certificate. An entry is a hostname, or *. and a
domain for every name under it. *.example.com does not cover example.com itself.
httpProxy:
rules:
- host: api.github.com
inject:
header: Authorization
value: "Bearer ${secret:github-pat/token}"
exclude:
- uploads.github.com
- "*.pinned.example.com"An exclusion applies to every injection into the workstations the Integration reaches, including other Integrations’ rules.
Fields
spec.type is httpProxy, and spec.selector works as on every
Integration. The httpProxy block
takes at least one rule or one exclusion.
| Field | Description |
|---|---|
httpProxy.rules[] | Up to 32 rules. Each rule takes a host and either inject or inspect: true. |
rules[].host | One exact hostname: letters, digits, - and .. No wildcard, IP address, scheme, port or path. A host may appear in only one rule of an Integration. Only HTTPS on port 443 is decrypted. |
rules[].inject.header | The header to set: a letter, then letters, digits and -, at most 64 characters. Host is refused. |
rules[].inject.value | The value to set, at most 1024 characters, with exactly one ${secret:name/key} reference (or ${secret:name}) and any text around it. A value with no reference is refused, so the key never appears in the YAML. |
rules[].inspect | true to check the host’s requests without injecting. Refused beside inject. |
rules[].allowDaemonPlacement | Which local workstations may hold this rule’s key. Unset, the default, is only those the Integration’s author owns. true is every local workstation the Integration reaches, and false is none. It has no effect on a cloud workstation. Refused beside inspect. |
httpProxy.exclude[] | Up to 64 hosts, or *. domains, that are never decrypted. |
A rule can set headers only. It cannot change a request body or a query string, and plain HTTP on port 80 is never decrypted.
Who needs access to the Secret
The person who applies the Integration must be able to read the Secret, and so must anyone who applies a change to it later. The key is always read with the access of the Integration’s author, the person who first applied it, and Ringleader checks that access again whenever it reads the key. The owners of the workstations it reaches need nothing, because the key never reaches their workstations.
When the author loses access, or the Secret is deleted, the key stops being added.
If your organization has an OrgPolicy or a Policy
with egressDestinations, it must permit every host a rule names, and an MCP server’s host.
An Integration naming a host the policy does not permit is refused when you apply it.
Excluded hosts are not checked.
Rotating a key
Update the Secret. A rotated key is used for new connections after the serving Edge receives it. A connection already open keeps the old key until it closes.
On an hcs, vz or qemu workstation served by its own network process, an Integration edit or delete, a Secret change, or an egress policy change reaches that process within one five-second box-loop pass. The two-minute drift re-list is the backstop. When the new credential set removes or withholds a host’s credential, its open intercepted connections close. This includes deleting the key or leaving its referenced Secret missing or empty. Rotating a key does not close them.
A cloud Edge and a local Edge VM resolve changes on their own schedules, so they carry no five-second promise. On lima, the daemon picks up the change immediately and closes the workstation’s open connections, so a request made at that moment can fail. Removing someone’s access through a role reaches a local workstation within a few minutes.
Store the key with --from-string, or make sure a file you load has no trailing newline.
Ringleader never sends a value that contains a line break, a tab or another control
character, or one longer than 1024 characters once the key is filled in. Connections to the
host are then closed before any request is sent, until you fix the Secret.
Check what is being injected
The Integration reports what each edge instance and edge VM is doing with its rules. Run
rl integration describe <name> -n <namespace> and read the Credential injection
section. A host being injected reads:
Credential injection:
Injecting: api.github.com (Integration github-api)A host that is not being injected reads, for example:
Credential injection:
Not injecting: api.internal.example.com (Integration internal-api): the Secret internal-api does not exist, or has no such keyThe same list is in status.injection.outcomes, one entry per host. An entry for a host that
is not being injected has a reason:
| Reason | What to do |
|---|---|
SecretNotFound | Create the Secret, or fix the name or key in value. |
AccessDenied | Give the Integration’s author access to the Secret. |
EmptyValue | Put the key in the Secret. |
DaemonPlacementNotAllowed | The host is on a local workstation, and the rule’s allowDaemonPlacement gate is closed there: the rule sets false, or leaves it unset and the workstation’s owner is not the Integration’s author. To inject there, set allowDaemonPlacement: true, which opens the gate on every local workstation the Integration reaches. On the author’s own workstation, removing a false is enough. |
HostContested | Two injections name the same host for the same workstation, so neither is used: two Integrations, or one Integration’s rule and its mcp server. Remove one. |
HostNotIntercepted | The edge instance or edge VM is not decrypting this host. It has not started to yet, or an exclude in some Integration names the host. |
CheckFailed | The edge instance or edge VM could not check the Secret, and is not injecting. It tries again. |
NoAuthor | The Integration has no recorded author yet. |
A host with an inspect: true rule is not listed. When a key is not being added, the
request still goes to the API without it, so the API’s own answer is usually a 401.
On the workstation, rl workstation describe <name> shows the condition for its provider:
| Provider | Condition |
|---|---|
| GCP, AWS, Azure | EgressEnforced. Until an edge instance serves the workstation it reads False, with reason NoEdge when the namespace has no Edge for its provider and region. |
| hcs, vz, qemu | EgressEnforced. It reads True with reason InterceptionPending when an edge VM or the workstation’s own network process serves the workstation and a named host is not being injected. The message names the host and the cause. An hcs, vz or qemu workstation with no Edge for its provider reports only here, not on the Integration. |
| lima | EgressCredential, as described in With lima. A lima workstation reports only here, not on the Integration. |
After changing an Integration, Secret or egress policy on an hcs, vz or qemu workstation served by
its own network process, run rl workstation wait <name> --for=EgressApplied. Applied means the
workstation’s own Edge holds the resulting rule set and credentials. InputsOutstanding means it
does not yet. The command checks the current fingerprinted Integration, Secret and policy inputs,
so it does not return from an earlier Applied result after one of those changes. It does not
track a Grant, RBAC or Integration-author ownership change.
The Workstation page lists every reason.
See also
- Integration: the kind injection is declared as, who it reaches, and MCP servers.
- Edge: the edge instance that adds the key on a cloud workstation, and the edge VM that adds it on vz or qemu when a namespace declares an Edge for that provider. On hcs, the workstation’s own network process adds the key and no Edge can be declared.
- WorkstationConfig: egress policies, which injection works with or without.
rl secret share: giving another person access to a Secret.