Private or self-signed gateway certificate
Your applications reach a self-managed region through its gateway. The gateway serves TLS for every project endpoint, http-<project-id>.<region-domain> and grpc-<project-id>.<region-domain>, with the certificate you give it in gateway.tls.
When a public CA signed that certificate, nothing more is needed. When your corporate CA, an internal PKI, or a self-signed CA signed it, three things have to know the CA:
- The gateway secret carries the CA next to the certificate and key.
- The Catalyst agent reads the CA from that secret and passes it to every sidecar in the region.
- Your applications trust the CA, because they call your project endpoints through the gateway.
If an application doesn't trust the CA, every call fails certificate verification with an error such as CERTIFICATE_VERIFY_FAILED: unable to get local issuer certificate or self signed certificate in certificate chain.
Put the CA in the gateway secret
The gateway secret holds the CA under a third key, ca.crt, next to tls.crt and tls.key. Only a secret you create can do that: the secret the chart creates from inline gateway.tls.cert and gateway.tls.key has no CA key.
For a certificate from your corporate CA, create the secret as a generic secret of type kubernetes.io/tls, so it can carry the third key:
kubectl -n cra-agent create secret generic my-gateway-tls \
--type=kubernetes.io/tls \
--from-file=tls.crt=path/to/server.crt \
--from-file=tls.key=path/to/server.key \
--from-file=ca.crt=path/to/ca.crt
server.crt is the wildcard certificate for *.<region-domain>, followed by any intermediate certificates. ca.crt is the root.
If cert-manager issues the certificate from a CA issuer, it writes ca.crt into the secret for you.
Point the chart at the CA
Reference the secret and name the key that holds the CA:
gateway:
tls:
enabled: true
existingSecret: my-gateway-tls
certificates:
caFile: ca.crt
The chart copies caFile into the agent setting agent.config.host.ingress.tls.secret.ca_file. If you set both, the agent setting wins. The agent reads the CA only when it starts, so restart it after you set caFile or change the CA:
kubectl -n cra-agent rollout restart deploy/agent
kubectl -n cra-agent logs deploy/agent | grep 'ingress TLS'
You should see loaded ingress TLS CA certificate from secret cra-agent/<secret> (key: ca.crt). A failed to read ingress TLS secret line means the agent couldn't read the secret. No line at all means caFile isn't set, or the secret has no key with that name.
See the Helm reference for every gateway.tls value. For the sidecars' own outbound calls to services signed by a private CA, such as an MCP server, see Sidecar outbound TLS trust.
Make your applications trust the CA
Export the CA from the secret and mount it into each application pod, for example from a ConfigMap:
kubectl -n cra-agent get secret my-gateway-tls -o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt
kubectl -n <app-namespace> create configmap catalyst-ca --from-file=ca.crt=ca.crt
Then point the application's runtime at it. Most runtimes replace their default trust with the file you give them, so if the application also calls public endpoints, such as an LLM provider, build a bundle that holds the system roots followed by your CA, and point at that bundle.
A Python application needs three variables, because its HTTP and gRPC clients read different settings. grpcio ignores the operating system's trust store and SSL_CERT_FILE, so without GRPC_DEFAULT_SSL_ROOTS_FILE_PATH, workflows and agents fail to connect even when HTTP calls succeed:
env:
- name: GRPC_DEFAULT_SSL_ROOTS_FILE_PATH # gRPC: workflows, agents, the Dapr gRPC client
value: /etc/catalyst-ca/ca-bundle.pem
- name: SSL_CERT_FILE # Python's ssl module and the clients built on its defaults
value: /etc/catalyst-ca/ca-bundle.pem
- name: REQUESTS_CA_BUNDLE # requests, which reads certifi instead of SSL_CERT_FILE
value: /etc/catalyst-ca/ca-bundle.pem
For other runtimes:
| Runtime | Setting |
|---|---|
| Go | SSL_CERT_FILE, on Linux. It replaces the system bundle. |
| Node.js | NODE_EXTRA_CA_CERTS, which adds to the built-in roots, so the CA file alone is enough. |
| Java | Import the CA into a truststore and pass it with -Djavax.net.ssl.trustStore. |
| .NET | Add the CA to the container image's system trust store with update-ca-certificates. |
Adding the CA to the image's system trust store doesn't cover a Python gRPC client: set GRPC_DEFAULT_SSL_ROOTS_FILE_PATH as well.
Check it
From a machine that resolves the region's domain, ask the gateway for a project endpoint and verify it against the CA:
openssl s_client -connect grpc-<project-id>.<region-domain>:443 \
-servername grpc-<project-id>.<region-domain> \
-CAfile ca.crt -showcerts </dev/null
A good answer ends with Verify return code: 0 (ok). If it fails with unable to get local issuer certificate, compare the chain the gateway sends with what's in ca.crt: the secret's tls.crt must include any intermediate certificates.
Next steps
- Architecture — the gateway, the region's other components, and its PKI.
- Helm reference — the
gateway.tlsvalues. - Troubleshooting — certificate errors when connecting an app.