Sunday, December 3. 2017
Certificate setup in wildfly
Long time ago I posted some entries about how to integrate client certificate authentication using glassfish. I am going to start another series about doing the same but in wildfly. In the new entries the protected resource is going to be a web service (jax-ws/SOAP is used), so it is very similar but a bit different (if everything goes as expected the last entry is going to be completely new). As in the previous time the first post is not very interesting because it is just the setup of the server and the presentation of the little WS application.
Again some certificates are needed in order to test the application. The same steps were performed (I am not going to repeat them, you can see the exact openssl commands in the previous entry) and at the end the following files are obtained:
The custom CA certificate that is imported in a custom cacerts file that will be used to validate the server and clients certificates.
The server certificate that is generated in a server.jks to be used by the wildfly server.
Two client1.p12 and client2.p12 files that contain a client certificate each. The latter is revoked.
A my.crl file which is the revocation list (it should contain the client2 certificate in it).
The just released wildfly 11 is going to be used in the entry. I need to learn more about the elytron subsystem so this is a good opportunity to start with it. Once the container is unzipped and started (using standalone mode and default standalone.xml configuration file), the following CLI commands were executed to create all the needed configuration.
Using the cacerts and server.jks files two stores are added to elytron:
/subsystem=elytron/key-store=localhost:add(type=jks, relative-to=jboss.server.config.dir, path=server.jks, credential-reference={clear-text=Kiosko_00}) /subsystem=elytron/key-store=ca:add(type=jks, relative-to=jboss.server.config.dir, path=cacerts, credential-reference={clear-text=changeit})
With the previous stores a key manager and a trust manager are created. In the trust manager the CRL is also added to take the revoked certificates into account. The wildfly server does not support OCSP checking for the moment, for that reason OCSP is not mentioned in the entry.
/subsystem=elytron/key-manager=localhost-manager:add(key-store=localhost, alias-filter=server, credential-reference={clear-text=Kiosko_00}) /subsystem=elytron/trust-manager=ca-manager:add(key-store=ca, certificate-revocation-list={relative-to=jboss.server.config.dir, path=my.crl})
Finally the SSL context is created for the https port (only TLSv1.2 is configured and client certificate is needed).
/subsystem=elytron/server-ssl-context=localhost-context:add(key-manager=localhost-manager, protocols=["TLSv1.2"], trust-manager=ca-manager, need-client-auth=true)
Finally the previous context is assigned to the https port (the old security realm should be removed first):
batch /subsystem=undertow/server=default-server/https-listener=https:undefine-attribute(name=security-realm) /subsystem=undertow/server=default-server/https-listener=https:write-attribute(name=ssl-context, value=localhost-context) run-batch
Just reload and the new configuration for the secure port will available.
reload
At this point the server is listening in the default wildfly port 8443 and client authentication is compulsory.
Moving to the application side, the WS endpoint is extremely easy, it's just a echo service that return the logged user and the same text that is passed to it.
@Stateless
@WebService(name = "echo",
targetNamespace = "http://es.rickyepoderi.sample/ws",
serviceName = "echo-service")
@SOAPBinding(style = SOAPBinding.Style.RPC)
public class Echo {
@Resource SessionContext ctx;
@WebMethod
public String echo(String input) {
return ctx.getCallerPrincipal() + " -> " + input;
}
}
To make compulsory the https access at application level two more things are needed. In the web.xml a security constraint is added to restrict the access to confidential data (this makes wildfly redirect all the http traffic to the https port).
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<security-constraint>
<web-resource-collection>
<web-resource-name>Protect all application</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<web-app>
And a jboss-webservices.xml is also added to force the scheme to be https (the generated WSDL will use https for generating the port location).
<?xml version="1.1" encoding="UTF-8"?>
<webservices version="1.2"
xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee">
<property>
<name>wsdl.soapAddress.rewrite.wsdl-uri-scheme</name>
<value>https</value>
</property>
</webservices>
Finally the client is developed and this step also has some tricks. Doing the previous steps the WSDL itself is protected behind the https (mutual auth) restriction. It means that any connection that reaches the server must present a valid certificate in order to get through. And using jax-ws there are two problems: configuration for SSL is not standardized by the API and the initial connection (the one that is done to download the WSDL) cannot be customized. Therefore we have two options, using global SSL configuration for the client (for example passing the following options to the java client -Djavax.net.ssl.trustStore=cacerts -Djavax.net.ssl.keyStore=client1.p12 -Djavax.net.ssl.keyStorePassword=Kiosko_00 -Djavax.net.ssl.keyStoreType=PKCS12), which is not perfect because your client can be another application container with several apps with different needs, or developing some specific code for the implementation you are using.
As the global properties are easy to understand my little client is going to perform a specific CXF implementation (wildfly uses apache CXF implementation for jax-ws). My final client assigns a custom SSLSocketFactory this way:
URL url = new URL("https://localhost:8443/wildfly-x509/echo-service/echo?wsdl");
EchoService service = new EchoService(url);
Echo echo = service.getEchoPort();
// get the client and assign a specific SSLContext configuration
Client client = ClientProxy.getClient(echo);
HTTPConduit http = (HTTPConduit) client.getConduit();
TLSClientParameters parameters = new TLSClientParameters();
parameters.setSSLSocketFactory(createSSLContext(args[0]).getSocketFactory());
http.setTlsClientParameters(parameters);
System.err.println(echo.echo(args[1]));
If you see once the echo port is created, a specific CXF/Apache lines assign your specific SSLContext (with your key and trust stores). Those lines are different for the Sun/Oracle metro implementation (code is also very easy but different). Having a standard property in the BindingProvider to assign a custom SSLSocketFactory would be a very nice extension for the standard. But the second problem still stands, the SSLSocketFactory is assigned once the port is created but, to reach this point, a first connection has already been established to read the WSDL (this part is done in the new EchoService(url) line). There is no way to set a SSLSocketFactory specifically to this request (only global solutions work, but they affect another applications in the same JVM as it was previously commented). You can avoid this issue just specifying a local WSDL file instead the final URL, but I prefer to use a jax-ws catalog, it caches the WSDL and avoids the first contact to the endpoint. Besides this point is standard, you just need to create a jax-ws-catalog.xml in the META-INF folder of the WAR bundle. In my case:
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog" prefer="system">
<system systemId="https://localhost:8443/wildfly-x509/echo-service/echo?wsdl" uri="wsdl/echo.wsdl"/>
</catalog>
The catalog marks that the WSDL for that location can be locally read from the wsdl/echo.wsdl file. This way when the service is created no initial connection is made (the local file is read instead of the network URL). With both ideas (catalog and specific CXF SSL configuration) the WS client can now access the endpoint using TLS mutual authentication.
Here it is the final video. First firefox browser (both client certificates were imported previously) is managed to access the service's WSDL. If client2 is used an error is presented but using client1 the definition is shown. That means everything is working as expected. Then maven is used to execute the web service client. The first execution uses client2.p12 as the trust store which does not work (it is revoked). But then the argument is changed to read the client1.p12 and it works. Please note that the user that is returned by the server is anonymous.
Let's do a little summary of the idea presented in today's entry. The wildfly container was configured to require client certificate in the https port. Besides it was configured to read a CRL file at startup (the client2 certificate is revoked, present in the CRL and not admitted). The jax-ws application is configured as confidential so any access to the application is restricted to the https port (wildfly redirects any plain access to the encrypted port). This setup makes compulsory to configure the WS client to use a client certificate. This configuration is not standard and a catalog is used to avoid the initial download of the WSDL. I want to emphasize that there is no real authentication in the entry, it is just a TLS level mutual authentication, but no user is really passed to the echo WS. That is why the user is anonymous in the call. JavaEE authentication will be the topic of the next entry in the series. The sample WS maven project used in the entry can be download from here.
Best regards!
Comments