Client certificates & proxies

Call an internal API from a flow: mutual TLS with a client certificate, a trusted private CA, and an HTTP or HTTPS proxy with its own credentials.

5 min read

Public APIs need none of this. Internal ones frequently need all of it: a client certificate to get through the gateway, a private CA that the machine does not trust yet, and a corporate proxy in front of both.

Two optional blocks cover it. Like auth, each sits either on one request node’s config or at the top of the file as a default, and a node’s own block replaces the flow default entirely rather than merging with it.

version: "1.0"
name: Internal API

tls: # the default for every request node in this flow
  ca: ./certs/internal-ca.pem

proxy:
  url: ${{ env.HTTP_PROXY }}

nodes:
  - id: call_internal_api
    type: request
    config:
      method: GET
      url: https://internal.example.com/status
      tls: # this node only, replacing the flow default
        cert: ./certs/client.pem
        key: ./certs/client.key
        ca: ./certs/internal-ca.pem
      proxy:
        url: http://127.0.0.1:8080
        auth: # the proxy's own credentials, unrelated to the request's auth
          username: ${{ env.PROXY_USER }}
          password: ${{ env.PROXY_PASSWORD }}
    next: null

The two blocks are independent, so a node can override tls while still inheriting the flow’s proxy.

tls

FieldMeaning
certClient certificate for mutual TLS. Requires key.
keyPrivate key paired with cert. Requires cert.
caCertificate authority to trust in addition to the system trust store.
rejectUnauthorizedWhether to verify the server’s certificate chain. true by default.

A path or the certificate itself

cert, key and ca each accept a file path or an inline PEM string. Which one you gave is detected by the -----BEGIN prefix, and it is checked after ${{ }} interpolation, so an environment variable can hold either shape:

tls:
  ca: ${{ env.CA_PEM }} # a path on your laptop, the certificate text in CI

That is the difference between committing a flow that works on a developer machine and one that also works on a runner where the certificate arrives as a secret.

Paths resolve relative to the flow file unless they are absolute. A path that does not resolve to a readable file fails the node with an error naming the field, the path it tried, and the directory it tried it from.

cert and key travel together

The validator rejects one without the other. A client certificate with no private key can never complete a handshake, so it is caught when the file is parsed rather than halfway through a run.

rejectUnauthorized is an escape hatch, not a shortcut

It defaults to true, matching Node’s own behaviour. Setting it to false switches certificate verification off entirely for that request.

That is genuinely useful against a self-signed local dev server. It is genuinely dangerous anywhere else, because it accepts any certificate at all. Where you have the option, prefer ca, which trusts one specific certificate rather than abandoning verification:

tls:
  ca: ./certs/dev-ca.pem # trusts this, still rejects everything else

bandura lint flags rejectUnauthorized: false for exactly this reason, so it shows up in CI rather than living quietly in a flow for a year. See the CLI.

A request node with no tls block at all, which is nearly all of them, uses the default connection pooling and allocates nothing extra.

proxy

FieldMeaning
urlProxy URL, for example http://127.0.0.1:8080. http and https only.
fromEnvironmenttrue reads the machine’s own HTTP_PROXY / HTTPS_PROXY instead of naming a URL
authOptional { username, password } for the proxy itself.

Set exactly one of url and fromEnvironment. Setting neither is a parse error, and so is setting both, because a block that names a proxy and also defers to the environment has no single answer to which one wins. fromEnvironment: true is the portable form: the same committed flow goes through whatever proxy each machine and each CI runner is already configured for, with nothing about your network written into the repository.

proxy:
  fromEnvironment: true

HTTP and HTTPS proxies only. Any other scheme, socks5: among them, fails the node with a message saying so. It is never silently sent direct, because a request that quietly bypasses the proxy you configured is worse than one that fails.

proxy.auth authenticates to the proxy, not to the API. It has nothing to do with the request’s own auth block, and those credentials are never sent to the destination. If you would rather put them in the URL, http://user:pass@host:port works as a fallback when auth is absent; auth wins when both are present.

The two compose

An HTTPS request through a proxy uses CONNECT tunnelling, and a node that sets both blocks gets its client certificate and custom CA applied to the tunnelled connection to the origin. There is no separate setting for “mTLS through a proxy”: configure both and it works.

Keep the secrets out of the file

Every string here goes through ${{ }}: cert, key, ca, proxy.url, and the proxy username and password. A proxy credential almost always belongs in .env rather than in the flow you commit.

Certificate files are a different question. A public CA certificate is fine to commit. A client key is not, so keep it out of the repository and point at it through an environment variable, or supply the PEM itself as a secret in CI.

What this does not touch

tls and proxy configure the connection the request rides on. They do not interact with hooks, the auth block, or the cookie jar, all of which operate on the request’s URL, headers and body instead.

A gRPC node has its own separate tls: true boolean, which switches the target from insecure to TLS. It is not this block; see the node reference.

Last updated

Looking for something else? All 37 articles are on one page in the Help Center.