I am using the WSO2 API Manager 4.2 platform, which is fronted by an AWS Application Load Balancer (ALB). I need to implement Mutual TLS (mTLS) authentication for a specific API deployed on WSO2 gateway. To achieve this, I configured mTLS on the AWS ALB using passthrough mode.
On the WSO2 side, I have configured the API to require mTLS and updated the deployment.toml file to handle the certificate header sent by ALB.
[apimgt.mutual_ssl]
certificate_header = "X-Amzn-Mtls-Clientcert"
enable_client_validation = false
client_certificate_encode = true
Despite these configurations, I am encountering issues where WSO2 responds with an error.
curl -v https://WSO2-GW-ALB.env.dev/my-api/1.0.0/test --key client-v3.key --cert client-v3.crt
< content-type: application/json; charset=UTF-8
< set-cookie: AWSALBTG=xxxxxxxxxx...; Expires=Tue, 06 Aug GMT; Path=/
< set-cookie: AWSALBTGCORS=xxxxxxxxxx...; Expires=Tue, 06 Aug 2024 14:03:35 GMT; Path=/; SameSite=None; Secure
< activityid: e18b453b-f9ff-4dec-8aaa-0e9ea316312e
< access-control-expose-headers:
< access-control-allow-origin: *
< access-control-allow-methods: GET
< x-forwarded-proto: https
< x-forwarded-for: 10.XX.XX.XX
< x-forwarded-port: 443
< access-control-allow-headers: authorization,Access-Control-Allow-Origin,Content-Type,SOAPAction,apikey,Internal-Key,Authorization
< x-amzn-trace-id: Root=1-6ccccf2b7-4c1026ccccc83630554457e0
< x-amzn-mtls-clientcert: -----BEGIN%20CERTIFICATE-----%0AMIIFJDCCAwxxxxxxxxxxxxxxxxxxxxxxxxygAwxAgI0Aw6<reduced>%0A-----END%20CERTIFICATE-----%0A
<
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection #0 to host WSO2-GW-ALB.env.dev left intact
{"code":"900900","message":"Unclassified Authentication Failure","description":"Error while validating into Certificate Existence"}
It appears that the ALB is sending the client certificate in the X-Amzn-Mtls-Clientcert header, but WSO2 cannot parse it (see the logs below).
WSO2 LOGS :
TID: [] [] [2024-07-30 13:32:12,953] ERROR {org.wso2.carbon.apimgt.gateway.handlers.Utils} -
Error while validating into Certificate Existence org.wso2.carbon.apimgt.api.APIManagementException:
Error while converting into X509Certificate
at org.wso2.carbon.apimgt.gateway.handlers.Utils.getClientCertificateFromHeader_aroundBody34(Utils.java:491)
at org.wso2.carbon.apimgt.gateway.handlers.Utils.getClientCertificateFromHeader(Utils.java:1)
at org.wso2.carbon.apimgt.gateway.handlers.Utils.getClientCertificate_aroundBody32(Utils.java:448)
at org.wso2.carbon.apimgt.gateway.handlers.Utils.getClientCertificate(Utils.java:1)
at org.wso2.carbon.apimgt.gateway.handlers.security.authenticator.MutualSSLAuthenticator.authenticate_aroundBody4(MutualSSLAuthenticator.java:105)
at org.wso2.carbon.apimgt.gateway.handlers.security.authenticator.MutualSSLAuthenticator.authenticate(MutualSSLAuthenticator.java:1)
at org.wso2.carbon.apimgt.gateway.handlers.security.APIAuthenticationHandler.isAuthenticate_aroundBody56(APIAuthenticationHandler.java:546)
...
...
...
Caused by: java.security.cert.CertificateException: Could not parse certificate: java.io.IOException: Incomplete BER/DER data
at java.base/sun.security.provider.X509Factory.engineGenerateCertificate(X509Factory.java:115)
at java.base/java.security.cert.CertificateFactory.generateCertificate(CertificateFactory.java:355)
at org.wso2.carbon.apimgt.gateway.handlers.Utils.getClientCertificateFromHeader_aroundBody34(Utils.java:488)
On the other hand, When I bypass the ALB and call the API directly through the gateway node, the mTLS authentication works as expected. Additionally, when I use NGINX as the load balancer and configure it with the ssl-client-cert header for mTLS, the authentication also functions correctly using the same certificate I used in ALB.
Any ideas on what the problem could be ?
EDIT
After further investigation, it appears that the ALB sends the certificate header in URL-encoded PEM format, but the encoding does not include characters such as +, /, and =, which confuses WSO2, as it expects a fully URL-encoded header.
Is there a way to ensure that these characters (+, /, =) are properly encoded before sending the header to WSO2 ?
I had to patch wso2 source code to make it work .
The certificate header is currently decoded within the getClientCertificateFromHeader method of the Utils.java class using a simple URL decode operation.
The patch adds support for AWS ALB mTLS, by incorporated the following logic :
[apimgt.mutual_ssl]
certificate_header = "X-Amzn-Mtls-Clientcert"
client_certificate_encode = true
### NEW Property ###
aws_alb_certificate_encode = true
Code implementation :
private static boolean isAwsAlbEncodedCertificate() {
APIManagerConfiguration apiManagerConfiguration =
ServiceReferenceHolder.getInstance().getAPIManagerConfiguration();
if (apiManagerConfiguration != null) {
String firstProperty = apiManagerConfiguration
.getFirstProperty(APIConstants.MutualSSL.AWS_ALB_CERTIFICATE_ENCODE);
return Boolean.parseBoolean(firstProperty);
}
return false;
}
private static Certificate getClientCertificateFromHeader(org.apache.axis2.context.MessageContext axis2MessageContext)
throws APIManagementException {
Map headers =
(Map) axis2MessageContext.getProperty(org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);
String certificate = (String) headers.get(Utils.getClientCertificateHeader());
byte[] bytes;
if (certificate != null) {
if (!isClientCertificateEncoded()) {
certificate = APIUtil.getX509certificateContent(certificate);
bytes = certificate.getBytes();
} else {
try {
// If ALB AWS header is received , Encode certificate characters that are considered safe by AWS ALB : + = /
if (isAwsAlbEncodedCertificate()) {
certificate = certificate
.replace("+", "%2B") // Encode '+' to '%2B'
.replace("/", "%2F") // Encode '/' to '%2F'
.replace("=", "%3D"); // Encode '=' to '%3D
// then Url Decode the certificate
certificate = URLDecoder.decode(processedAwsCeritifcate, "UTF-8");
} else {
certificate = URLDecoder.decode(certificate, "UTF-8");
}
} catch (UnsupportedEncodingException e) {
String msg = "Error while URL decoding certificate";
throw new APIManagementException(msg, e);
}
certificate = APIUtil.getX509certificateContent(certificate);
bytes = Base64.decodeBase64(certificate);
}
try (InputStream inputStream = new ByteArrayInputStream(bytes)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return cf.generateCertificate(inputStream);
} catch (IOException | CertificateException e) {
String msg = "Error while converting into X509Certificate";
throw new APIManagementException(msg, e);
}
}
return null;
}
public static class MutualSSL {
public static final String MUTUAL_SSL_CONFIG_ROOT = "MutualSSL";
public static final String CLIENT_CERTIFICATE_HEADER = MUTUAL_SSL_CONFIG_ROOT + ".ClientCertificateHeader";
public static final String CLIENT_CERTIFICATE_ENCODE = MUTUAL_SSL_CONFIG_ROOT + ".ClientCertificateEncode";
// ########### ALB mTLS Constant ##########
public static final String AWS_ALB_CERTIFICATE_ENCODE = MUTUAL_SSL_CONFIG_ROOT + ".AwsAlbCertificateEncode";
public static final String ENABLE_CLIENT_CERTIFICATE_VALIDATION = MUTUAL_SSL_CONFIG_ROOT +
".EnableClientCertificateValidation";
}
<MutualSSL>
<ClientCertificateHeader>{{apimgt.mutual_ssl.certificate_header}}</ClientCertificateHeader>
<EnableClientCertificateValidation>{{apimgt.mutual_ssl.enable_client_validation}}</EnableClientCertificateValidation>
<!-- AWS ALB Encoding -->
<AwsAlbCertificateEncode>{{apimgt.mutual_ssl.aws_alb_certificate_encode}}</AwsAlbCertificateEncode>
{% if apimgt.mutual_ssl.client_certificate_encode is defined %}
<ClientCertificateEncode>{{apimgt.mutual_ssl.client_certificate_encode}}</ClientCertificateEncode>
{% endif %}
</MutualSSL>
[apimgt.mutual_ssl]
certificate_header = "x-amzn-mtls-clientcert"
enable_client_validation = false
client_certificate_encode = true
aws_alb_certificate_encode = true
I have submitted an enhancement request for this use case . This issue addresses the need for better support for AWS ALB's mTLS passthrough mode in WSO2 Gateway : https://github.com/wso2/api-manager/issues/3058