How to bypass certificate validation when creating a local HTTPS request from app to Jira instance

Hi,
During app initialization, we need to create a local HTTP request to the plugin URL.
It’s implemented using Atlassian provided com.atlassian.sal.api.net.RequestFactory and com.atlassian.sal.api.net.Request.
Recently we faced a problem when only SSL enabled local connection can be made. In case of self-signed certificate or name mismatch, we needed to bypass SSL certificate validation.
We implemented it using hints from this blog post - Svetlin Nakov - Svetlin Nakov – Official Web Site and Blog » Disable Certificate Validation in Java SSL Connections.
The downside for this solution is that modifying default SSL validations will affect all Jira instance.
Is there a better way how to create an SSL request without certificate validation?

Best regards,
Janis Baiza
eazyBI

1 Like

Can you elaborate a little more on the problems your facing?

I will try to provide more details about the question that my colleague @janis.baiza asked.

This is needed for our eazyBI app when used in Jira Data Center. In our case, all our application is implemented as a servlet and we need need to initialize it when the app (plugin) is enabled. Currently, the only way how we can achieve it (that we have figured out) is to make a local HTTP request to our servlet URL. Our plugin lifecycle onStart event handler schedules a job which will make the request to our servlet URL.

In a Jira Data Center environment, we need to make the request to the servlet URL on each individual node. If we will use a Jira Base URL then the front-end load balancer will route a request to just one node. Therefore we construct a URL http://127.0.0.1:{port}/{context} where port is a local port on which Jira Tomcat is listening. During the Data Center AppWeek with a help from Atlassians we got a solution how to find a local port on which Tomcat is listening. Here is a shortened code without error handling:

MBeanServer beanServer = ManagementFactory.getPlatformMBeanServer();
Set<ObjectName> objectNames = beanServer.queryNames(new ObjectName("Catalina:type=Manager,*"), null);
ObjectName objectName = objectNames.iterator().next();
String context = objectName.getKeyProperty("context");
objectNames = beanServer.queryNames(
  new ObjectName("*:type=Connector,*"),
  Query.match(Query.attr("protocol"), Query.value("HTTP/1.1"))
);
objectName = objectNames.iterator().next();
String port = objectName.getKeyProperty("port");
String baseUrl = "http://127.0.0.1:" + port + context;
if (baseUrl.charAt(baseUrl.length() - 1) == '/') {
  baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
}

We identified a customer who has set up HTTPS also for the Tomcat server (so it means that the front-end web server proxies the incoming connections using HTTPS to the back-end Jira Tomcat server). Therefore now we have added that if the servlet URL request fails with NoHttpResponseException then we retry the request with HTTPS using the https://127.0.0.1:{port}/{context} prefix. As we are making the request to 127.0.0.1, but the SSL certificate uses a different hostname, then our HTTPS request was failing with a certificate validation error.

So the solution that we have found as referred in the original question is to disable SSL certificate validation for a short time when we make this request. Here is a shortened code without error handling:

SSLSocketFactory currentSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
HostnameVerifier currentHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() {
  public X509Certificate[] getAcceptedIssuers() {
    return null;
  }
  public void checkClientTrusted(X509Certificate[] certs, String authType) {}
  public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}
// Create all-trusting trust manager
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
// Create all-trusting host name verifier
HostnameVerifier allHostsValid = new HostnameVerifier() {
  public boolean verify(String hostname, SSLSession session) {
    return true;
  }
};
// Install the all-trusting trust manager
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
// Install the all-trusting host verifier
HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);

URL url = new URL(pluginUrl);
URLConnection con = url.openConnection();

// We can restore default values as they used only for creating a connection
HttpsURLConnection.setDefaultSSLSocketFactory(currentSSLSocketFactory);
HttpsURLConnection.setDefaultHostnameVerifier(currentHostnameVerifier);

con.getContent();

This solution disables SSL certificate validation for a short time (should be less than a second) during the plugin initialization for all JVM instance while the connection to the localhost is established.

Therefore we are interested if there is another way how to make an HTTPS request to localhost without SSL certificate validation during the plugin initialization?

P.S. In general, it would be good if there would be a Jira Server API for making local REST API requests to localhost and avoiding network layer overhead. For example, sometimes we have identified problems that when we make a request from Jira Server to the canonical Jira Base URL (which is going to the front-end web server) then either DNS is providing wrong IP address (when servers have different public or private IP addresses) or a firewall is blocking requests from Jira Server to the fron-end web server / load balancer.

We need to use local REST API requests also for accessing either some Jira data (for which there is no Java public API and only a REST API) or for accessing REST APIs provided by other Jira apps.

Kind regards,
Raimonds

@rwhitbeck Do you have any comments or suggestions to the detailed description that I provided?

Kind regards,
Raimonds

I’m not @rwhitbeck but - you could make the initializing call through the plugin system. It’s a bit of a hack (and I’m sure there are memory concerns etc involved - but depending on how your servlet functions…).

public class StartLifeCycleAware implements LifecycleAware
{

    private final Logger logger = LoggerFactory.getLogger(StartLifeCycleAware.class);
    @ComponentImport
    private final PluginAccessor pluginAccessor;

    @Inject
    public StartLifeCycleAware( final PluginAccessor pluginAccessor)
    {
        this.pluginAccessor = pluginAccessor;
    }

    public void onStart()
    {

        logger.error("Starting the app");

        Plugin thePlugin = this.pluginAccessor.getEnabledPlugin("demo.sample-http-plugin");


        ServletModuleDescriptor theServlet =  (ServletModuleDescriptor) thePlugin.getModuleDescriptor("my-servlet");

        HttpServletRequest httpServletRequest = new FakeHttpServletRequest();
        HttpServletResponse httpServletResponse = new FakeHttpServletResponse();

        try {
            theServlet.getModule().service(httpServletRequest, httpServletResponse);
        }catch(ServletException servletExpection)
        {
            servletExpection.printStackTrace();
        }catch(IOException ioException)
        {
            ioException.printStackTrace();
        }


    }

    public void onStop() {

    }
}

Replace the my-servlet with the key of the servlet you’re calling. The only gotcha is that you have to create the HttpServletRequest and HttpServletResponse to be passed in.

Seems to work in the refapp when I just tried it.

1 Like

@daniel Thanks for looking into this and for a suggestion. In our case we actually have a servlet-filter and servlet-context-listener modules and not just a simple servlet module. And for the ServletContextListener initialization, we need to pass a servlet context (and we use the servlet context during our plugin initialization to get the reference to the OSGi bundle and the context path). I think that was the problem that we discussed with Atlassian Jira developers during the Appweek, how to pass the correct servlet context for the servlet context listener initialization, and they could not provide a solution for that.

Any suggestions regarding this?

Kind regards,
Raimonds