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
| Field | Meaning |
|---|---|
cert | Client certificate for mutual TLS. Requires key. |
key | Private key paired with cert. Requires cert. |
ca | Certificate authority to trust in addition to the system trust store. |
rejectUnauthorized | Whether 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
| Field | Meaning |
|---|---|
url | Proxy URL, for example http://127.0.0.1:8080. http and https only. |
fromEnvironment | true reads the machine’s own HTTP_PROXY / HTTPS_PROXY instead of naming a URL |
auth | Optional { 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.
What to read next
- Authentication & cookies for the credential the request itself carries.
- Variables and environments for where to keep a secret.
- Troubleshooting if a connection fails and you are not sure which layer rejected it.