Implementing Mutual TLS for Node.js-Based AWS Lambda Functions

At SMG Real Estate, we’ve partnered with UBS to provide a specialized service that allows users to request personalized mortgage advice directly from UBS experts. This offering is tailored for individuals who have found their ideal property on homegate.ch or immoscout24.ch and are now looking for the right mortgage solution.


The system powering this feature is based on a distributed architecture with two key services as shown in Figure 1.1:
● SMG service: This service which runs on AWS lambda, collects user requests and forwards them to UBS service.
● UBS service: Processes the requests and connects users with mortgage experts.

Figure 1.1 Service architecture

While API keys were already securing the communication between both systems, UBS as a bank requested additional safeguards — specifically, the ability to authenticate SMG service as a trusted client before accepting any requests. To meet this requirement, we explored implementing mutual TLS (mTLS) to ensure both parties could securely identify each other.
This blog post outlines our approach to designing an mTLS-enabled integration — covering certificate authority (CA) setup, certificate distribution, AWS Lambda configuration, and lessons learned.

What Is Mutual TLS?

Mutual TLS is an extension of the standard TLS (Transport Layer Security) protocol. While TLS ensures that the client can verify the server’s identity (e.g., when you visit a website with HTTPS), mTLS goes a step further: it allows both parties — client and server — to authenticate each other.
In a typical TLS handshake, the server presents a certificate to prove its identity. The client verifies that certificate before proceeding. With mTLS, the server still presents its certificate, but the client also presents its own certificate, and the server verifies it. Figure 1.2 shows this process.

Figure 1.2 MTLS handshake

This mutual authentication is especially valuable for service-to-service communication, ensuring that both parties in a connection are verified and trusted — exactly the level of assurance required when SMG interacts with UBS.

Why Certificate Authorities Matter?

To implement mTLS, both sides of the connection must trust each other’s certificates. This trust is established via a Certificate Authority (CA) — a trusted entity that issues and signs certificates.
In TLS, external CAs (like Let’s Encrypt or DigiCert) validate domain ownership and provide certificates for public-facing services. However, in mutual TLS (mTLS), both the client and server must authenticate each other, which introduces the need for internal identity management.
For this reason, organizations implementing mTLS often operate their own internal CA. This enables them to maintain full control over the issuance, validation, and lifecycle of certificates used for internal services, users, and systems. External CAs are not designed to manage these types of internal identities.
Initially, we considered using AWS Private CA due to our cloud-native architecture. However, we realized that using Private CA for just one application would be overkill—especially given the cost of approximately $400/month and $0.75 per issued certificate.
As an interim measure, we adopted a local CA setup with OpenSSL, opting for this localized approach until further similar use-cases arise within SMG and standardized, ideally centralized, certificate management processes are established

Internal Certificate Authority

OpenSSL is an open-source library that provides several tools for handling digital certificates. Some of these tools can be used to act as a certificate authority. [1]
Acting as a Certificate Authority (CA) means managing cryptographic key pairs, specifically private keys and public certificates. According to OpenSSL docs, a certificate chain is required where each certificate plays a distinct role.

1. Root CA

The Root CA is the trust anchor of our certificate chain. In order to build our root CA, the very first step is to create the root pair. This consists of the root private key and the root certificate. Together, they establish the identity of our CA and form the foundation of trust for all certificates our CA will issue.
In most cases, the root Certificate Authority (CA) does not directly issue certificates for servers or clients. Instead, its primary role is to generate and authorize one or more intermediate CAs. These intermediates, trusted by the root, handle the task of signing certificates. This layered approach is considered a best practice because it allows the root key to remain offline and rarely used—minimizing the risk of compromise, which would have serious security consequences.

Ensuring the security of root and intermediate private keys is paramount in the lifecycle of a mutual TLS (mTLS) infrastructure, particularly when implementing an internal certificate authority (CA) with OpenSSL. These keys serve as the foundational trust anchors for all issued certificates and are critical to the integrity of the entire authentication system. A compromised root key can invalidate the security of every certificate in the ecosystem, necessitating a complete re-issuance and potentially disrupting service availability. Similarly, intermediate keys, while offering compartmentalization, must also be stringently protected with access restricted and auditable. Furthermore, key rotation policies, certificate revocation mechanisms, and secure archival practices are essential to mitigate risks and ensure the long-term resilience of the mTLS solution.

2. Intermediate CA

Next, we created an Intermediate CA — a bridge that signs the actual client/server certificates under the authority of the root CA. This consists of the intermediate private key and the intermediate certificate. It receives its certificate from the root, establishing a trusted certificate chain.
Using an intermediate CA enhances security by allowing the root CA’s private key to remain offline and rarely accessed. In the event the intermediate key is compromised, the root can invalidate it by revoking the certificate and issuing a replacement, without affecting the root’s integrity.

3. Client Certificate

Finally a certificate was created and signed by our intermediate CA to be used by SMG service when establishing a connection to UBS service. With the client certificate in place, the final step in our mTLS setup involves securely deploying this certificate within our service and configuring it to use the certificate when communicating with the UBS service in AWS lambda.

Figure 1.3 Directory Structure of Certificate Authority (CA) Hierarchy at SMG service

Certificate Distribution to UBS

Applications rely on a chain of trust to validate certificates. When a certificate is signed by an intermediate CA, the application must also trust the intermediate certificate, which in turn must be validated against the root certificate. To support this process, the entire CA certificate chain must be provided to the application. Therefore, to enable UBS service to authenticate our mTLS requests, we provided a single PEM (‘fullchain’) containing our client certificate followed by all issuing certificates, in the format as below.
UBS adds the issuing CA certificates to its trust store, enabling it to validate and accept mTLS connections from SMG service.

Using the client certificate in AWS Lambda

To ensure the security and manageability of client certificates, we stored the client certificate and client private key in AWS Secrets Manager. In the AWS Lambda function, we use the AWS SDK to securely fetch both the certificate and private key at runtime. Once the certificate and key are retrieved, we configure Axios to use them when making HTTPS requests. Axios allows us to pass a custom httpsAgent to handle mutual TLS:

JavaScript
import axios from 'axios';
import https from 'https';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from '@smg-real-estate/lambda-middleware'; // SMG's internal package
export const requestMortgageAdvice = async (
event: APIGatewayProxyEvent,
context: CustomContext
): Promise<APIGatewayProxyResult> => {
const cert = context.clientCertificate;
const key = context.clientPrivateKey;
const httpsAgent = new https.Agent({
cert,
key,
rejectUnauthorized: true,
});
try {
const response = await axios.post('https://ubs.service.endpoint/api/resource', {
httpsAgent,
});
return {
statusCode: 200,
body: JSON.stringify(response.data),
};
} catch (error: any) {
return {
statusCode: error?.response?.status || 500,
body: JSON.stringify({
message: 'Failed to forward mortgage request to UBS service',
error: error.message,
}),
};
}
};
// Wrapper will inject client certificate & private key into aws lambda function
export const handler = createApiGatewayHandler(requestMortgageAdvice, {
secretsManager: {
secrets: {
clientCertificate: 'ClientCertificate',
clientPrivateKey: 'ClientPrivateKey',
},
},
});

Conclusion

By establishing our own internal certificate authority using OpenSSL, we demonstrated a cost-effective and standards-compliant approach to identity verification between services. This exercise also helped us understand the practical complexities of certificate lifecycle management, secure distribution, and integration within cloud-native environments like AWS Lambda.
Looking forward, the groundwork we’ve established can serve as a blueprint for future mTLS deployments at SMG — whether with UBS or other partners — and will guide the development of centralized certificate management practices across our systems.

References

[1] https://jamielinux.com/docs/openssl-certificate-authority/introduction.html

Author

Ramin Ahadi

Senior Engineer

Real Estate

Scroll to Top