Linkerd, a lightweight and security-focused service mesh, excels at providing transparent mutual TLS (mTLS) for traffic within your Kubernetes cluster. However, many applications also need to securely communicate with services running outside the cluster – external databases, partner APIs, or legacy systems. When these external services require mTLS and utilize certificates signed by a custom Certificate Authority (CA) not part of public trust stores, configuring Linkerd’s sidecar proxies to establish trust becomes a critical task.
This comprehensive guide walks experienced engineers through the process of configuring mTLS between Linkerd-meshed applications and external services secured by custom CAs. We’ll cover the core concepts, provide step-by-step instructions with practical code examples, and discuss common pitfalls and debugging techniques.
The Challenge: External mTLS with Custom CAs
By default, Linkerd’s sidecar proxy (linkerd-proxy
), when acting as a client to an external service, validates the server’s certificate against a standard set of well-known public CAs (similar to a web browser). If the external service presents a certificate signed by your organization’s internal CA or any other private CA, the Linkerd proxy will fail the TLS handshake, unable to verify the server’s identity.
To resolve this, we must explicitly instruct the Linkerd proxy to trust this specific custom CA. This involves making the custom CA certificate available to the proxy within the application’s pod and ensuring the proxy uses it when establishing an mTLS connection to the designated external service.
Core Concepts
- mTLS (Mutual TLS): Both client and server authenticate each other using X.509 certificates. The client (Linkerd proxy) verifies the external server’s certificate, and the external server verifies the client’s certificate (if required by the server; this guide focuses on the Linkerd proxy trusting the external server).
- Linkerd Proxy: The sidecar injected into your application pods. It intercepts outbound traffic and can originate TLS connections.
- Custom CA: A private Certificate Authority whose root certificate is not in standard public trust stores.
- System Trust Store: An operating system-level repository of trusted CA certificates. Many applications, including proxies, can be configured to use this store.
Solution Overview: Leveraging the System Trust Store
A robust and common pattern to enable the Linkerd proxy to trust a custom CA for egress connections is to add the custom CA certificate to the pod’s system trust store. The Linkerd proxy, like many network clients, often respects the system’s CA certificates. This involves:
- Storing the Custom CA Certificate: Securely store your custom CA certificate (PEM-encoded) in a Kubernetes Secret.
- Mounting the Secret: Mount this Secret as a volume into your application pod.
- Using an Init Container: An
initContainer
runs before your main application container. It copies the custom CA certificate from the mounted volume into the appropriate system directory for CA certificates and updates the system trust store. - Verification: The Linkerd proxy in the same pod will then (typically) recognize this newly trusted CA when initiating TLS connections to external services that present certificates signed by it.
Step-by-Step Configuration
Let’s walk through an example. Assume you have an external service my-external-service.example.com:443
that requires mTLS and uses a certificate signed by your organization’s custom CA.
1. Prepare Your Custom CA Certificate
Ensure you have your custom CA certificate in PEM format. For this guide, let’s assume its content is:
|
|
Save this content as custom-ca.pem
.
2. Create a Kubernetes Secret
Create a Kubernetes Secret to store this CA certificate. This makes it securely available to your pods.
|
|
Replace ‘your-namespace’ with the namespace of your application```
This command creates a secret named custom-ca-secret
with a key ca.crt
containing the PEM content.
3. Deploy Your Application with an Init Container
Now, define your application’s Deployment. We’ll include an initContainer
that uses an image containing the ca-certificates
package (like alpine
or ubuntu
) to update the pod’s trust store.
|
|
Key points in this deployment:
linkerd.io/inject: enabled
: Ensures the Linkerd proxy is injected.initContainers
:- The
update-ca-certificates
init container runs first. - It mounts the
custom-ca-secret
viacustom-ca-volume
. - It copies
ca.crt
to/usr/local/share/ca-certificates/custom-external-ca.crt
. - It runs
update-ca-certificates
, which rebuilds the system’s list of trusted CAs. securityContext.runAsUser: 0
is often needed forupdate-ca-certificates
and writing to system directories. Ensure your PodSecurityPolicies/Standards allow this if applicable.
- The
- Application Container (
my-app
):- In this example, it uses
curlimages/curl
to attempt connections tohttps://my-external-service.example.com
. - The
curl
command with--cacert /mnt/custom-ca/ca.crt
explicitly tellscurl
to use the custom CA. This is useful for direct testing from the application container. - For the Linkerd proxy to handle the mTLS using the updated system trust store, the
curl
command without--cacert
would rely on the transparent mTLS provided by Linkerd. If the Linkerd proxy successfully uses the updated system CA store, this “naked”curl
call should succeed.
- In this example, it uses
- Volumes: Defines
custom-ca-volume
to provide theca.crt
fromcustom-ca-secret
.
After applying this deployment (kubectl apply -f app-deployment.yaml -n your-namespace
), the Linkerd proxy in the my-app
pods should be able to establish mTLS connections to my-external-service.example.com
by trusting its custom CA.
Alternative: Using LINKERD2_PROXY_TLS_TRUST_ANCHORS_PEM
Linkerd documentation also specifies that the LINKERD2_PROXY_TLS_TRUST_ANCHORS_PEM
environment variable can be set on the proxy container to provide the PEM-encoded CA certificate(s) directly. This is an alternative to modifying the system trust store.
You can set this environment variable for the injected linkerd-proxy
using the config.linkerd.io/proxy-env
annotation on the pod spec:
|
|
Challenges with proxy-env
for PEMs:
- Multi-line Strings: Kubernetes annotations are flat strings. Injecting a multi-line PEM certificate directly into an annotation value in YAML can be cumbersome and error-prone. You might need to process the PEM into a single-line string with literal
\n
characters. - Size Limits: Annotations have size limits, which might be an issue for large CA bundles.
For these reasons, the init container method modifying the system trust store is often more robust for larger or multiple CA certificates. If your CA PEM is small and you can manage the string formatting, proxy-env
offers a more direct configuration approach without needing an init container with root privileges.
Verification and Debugging
Check Pod Logs:
- Inspect the init container logs:Ensure it completed successfully.
1
kubectl logs <your-pod-name> -n your-namespace -c update-ca-certificates
- Inspect your application container logs:Look for successful connection attempts from
1
kubectl logs <your-pod-name> -n your-namespace -c my-app
curl
. - Inspect the Linkerd proxy logs:Look for messages related to TLS handshakes or certificate validation errors. Increase verbosity if needed (e.g., by setting
1
kubectl logs <your-pod-name> -n your-namespace -c linkerd-proxy
config.linkerd.io/proxy-log-level: debug
annotation).
- Inspect the init container logs:
linkerd tap
: Uselinkerd tap
to observe traffic from your application pod:1 2
linkerd tap deploy/my-app-consuming-external-service -n your-namespace \ --to svc/my-external-service.example.com # Adjust target if needed
This can show you details about the TLS handshake, including the server identity used.
openssl s_client
from within the pod: If your application container (or a debug sidecar) hasopenssl
, you can test manually:1 2 3 4 5 6 7 8 9
kubectl exec -it <your-pod-name> -n your-namespace -c my-app -- /bin/sh # Inside the pod: # Test with the CA explicitly provided to openssl openssl s_client -connect my-external-service.example.com:443 \ -CAfile /mnt/custom-ca/ca.crt # Test relying on system trust store (if openssl uses it) openssl s_client -connect my-external-service.example.com:443
Common Pitfalls
- Incorrect CA Certificate: Ensure the exact root CA (or intermediate CAs if the server doesn’t send the full chain and the root is what you trust) that signed the external service’s certificate is used.
- Namespace Mismatch: The Secret and the Deployment using it must be in the same Kubernetes namespace.
- Permissions for Init Container: The init container might need root privileges (
runAsUser: 0
) to write to system CA directories and runupdate-ca-certificates
. - External Service Name Mismatch: The hostname used by your application to connect (e.g.,
my-external-service.example.com
) must match a Subject Alternative Name (SAN) or the Common Name (CN) in the external service’s certificate. - Linkerd Proxy Not Using System Store: While common, verify that your Linkerd version and configuration indeed lead the proxy to respect the updated system CA list. If not, the
LINKERD2_PROXY_TLS_TRUST_ANCHORS_PEM
environment variable method becomes more critical. - Firewall/NetworkPolicy Issues: Ensure network connectivity to the external service is allowed from your pod.
Conclusion
Securing egress traffic to external services with mTLS is a vital aspect of a comprehensive security posture. By configuring your Linkerd-meshed applications to trust custom Certificate Authorities, you can extend Linkerd’s powerful security benefits beyond your cluster boundaries. The init container pattern provides a robust way to manage custom CA trust for the Linkerd proxy, ensuring that your applications can reliably and securely communicate with external dependencies. Always refer to the latest Linkerd official documentation for features and configuration specifics related to your Linkerd version.