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.

  1. Store the key in a Secret:

    rl secret create github-pat -n acme --from-string token=<your-token>
  2. 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-east4

    On AWS and Azure the Edge also names a subnet, and the workstations go in a subnet of their own. The Edge page says which.

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

  4. Give the workstation the label, in its manifest:

    metadata:
      labels:
        team: platform
  5. Wait until rl workstation describe <name> shows the EgressEnforced condition as True. For a workstation with no egress policy the reason is CredentialOnly. Then, from a shell on the workstation, call the API with no key:

    curl https://api.github.com/user

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

ProviderWhere the key is addedWhat you declare
GCP, AWS, AzureOn 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 MacIn 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 LinuxIn 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 MacIn the Ringleader daemon on your own machine.No Edge.
wsl2Nowhere. Use a cloud workstation instead.
hcs, on WindowsIn 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.
  • true opens 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.
  • false closes 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:

vzlima
Works onApple Silicon Macs with macOS 13 or laterEvery Mac
Needs an EdgeNo. An Edge for vz is optionalNo
Reads the traffic inThe workstation’s own network process. With an Edge for vz, an edge VM with 2 vCPUs and 1 GiB of memoryThe Ringleader daemon
inspect: trueSupportedNot 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: vz

The 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: qemu

The 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 shows Bearer ${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 Host header. A request naming a different host from the one the connection was opened to gets 421 Misdirected Request, and the connection is closed.
  • It removes the Accept-Encoding header, 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_CAINFO and CARGO_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 shell reaches 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: true

Cloud, 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.

FieldDescription
httpProxy.rules[]Up to 32 rules. Each rule takes a host and either inject or inspect: true.
rules[].hostOne 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.headerThe header to set: a letter, then letters, digits and -, at most 64 characters. Host is refused.
rules[].inject.valueThe 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[].inspecttrue to check the host’s requests without injecting. Refused beside inject.
rules[].allowDaemonPlacementWhich 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 key

The same list is in status.injection.outcomes, one entry per host. An entry for a host that is not being injected has a reason:

ReasonWhat to do
SecretNotFoundCreate the Secret, or fix the name or key in value.
AccessDeniedGive the Integration’s author access to the Secret.
EmptyValuePut the key in the Secret.
DaemonPlacementNotAllowedThe 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.
HostContestedTwo 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.
HostNotInterceptedThe 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.
CheckFailedThe edge instance or edge VM could not check the Secret, and is not injecting. It tries again.
NoAuthorThe 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:

ProviderCondition
GCP, AWS, AzureEgressEnforced. 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, qemuEgressEnforced. 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.
limaEgressCredential, 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.