Jetty web server customisation: common use cases

Care should be taken when applying customisation to the Jetty web server. A malformed script may prevent the Jetty web server from starting which will cause Funnelback’s administration and search interfaces to stop functioning.

Prerequisites

The following article assumes that a working Funnelback 15 server is available. The term $SEARCH_HOME is used throughout the document and this usually refers to either /opt/funnelback (Linux) or <DRIVE>:\funnelback (Windows).

Before starting check your Funnelback version and the corresponding documentation to ensure that the CustomiseJettyServers.groovy script is available.

Background

To provide a worked example of how to set up customJettyServers.groovy to deal with some common embedded Jetty customisations, i.e. host custom keystore/truststore, excluding protocols and including Cipher Suites.

Steps

  • Login into the server that Funnelback is installed.

  • Create $SEARCH_HOME/web/conf/customiseJettyServers.groovy in a text editor.

  • Enter the following into the file:

    import org.eclipse.jetty.server.Server;
    import org.eclipse.jetty.server.ServerConnector;
    import org.eclipse.jetty.util.ssl.SslContextFactory;
    import org.eclipse.jetty.server.HttpConfiguration;
    import org.eclipse.jetty.server.HttpConnectionFactory;
    import org.eclipse.jetty.server.Connector;
    import org.eclipse.jetty.util.log.Log;
    
    def removeConnectors(Map<String, Server> servers, String serverName) {
    
        //Finding curently set connectors for the  server context
        Connector[] publicConnectors = servers.get(serverName).getConnectors();
    
    
        //Removing ALL set connectors - to be replaced by the ones we create later!
        publicConnectors.each {
            servers.get("public").removeConnector(it);
        }
    }
    
    def HttpConnectionFactory createHttpConnectionFactory() {
        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setOutputBufferSize(32768);
        httpConfig.setRequestHeaderSize(8192);
        httpConfig.setResponseHeaderSize(8192);
        HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
        return httpConnectionFactory;
    }
    
    
    def SslContextFactory createSslContextFactory(String keyStorePath, String keyStorePassword, String trustStorePath,String trustStorePassword,String[] excludeProtocols,String[] includeCipherSuites) {
    
        SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setKeyStorePath(keyStorePath);
        sslContextFactory.setKeyStorePassword(keyStorePassword);
        sslContextFactory.setTrustStorePath(trustStorePath);
        sslContextFactory.setTrustStorePassword(trustStorePassword);
    
        sslContextFactory.addExcludeProtocols(excludeProtocols);
        sslContextFactory.setIncludeCipherSuites(includeCipherSuites);
    
        return sslContextFactory;
    }
    
    
    def Map<String, Server> customise(Map<String, Server> servers) {
        //Removing existing connectors for the public server context
        removeConnectors(servers, "public");
    
        // Add a HTTP connector to the public server
        ServerConnector httpConnector = new ServerConnector(servers.get("public"), createHttpConnectionFactory());
        httpConnector.setPort(9080);
    
        String[] excludeProtocols = ["SSL","SSLv2","SSLv2Hello","SSLv3"];
        String[] includeCipherSuites = ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA"];
    
        //SslContextFactory contstruction - to be shared by both admin and public; create a separate one if the properties used are different
        SslContextFactory sslContextFactory = createSslContextFactory(
                  <$SEARCH_HOME>/web/conf/keystore","OBF:<keystore password>",
                  <$SEARCH_HOME>/web/conf/keystore (** truststore **)","OBF:<trustore password>",
                  excludeProtocols,includeCipherSuites);
    
        //An an SSL Connector to the public server context
        ServerConnector publicSslConnector = new ServerConnector(servers.get("public"), sslContextFactory);
        publicSslConnector.setPort(9843);
    
        //Adding http and https connectors to the public context
        servers.get("public").addConnector(httpConnector);
        servers.get("public").addConnector(publicSslConnector);
    
        Log.getRootLogger().info("CustomiseJettyServers - public server info:");
        Log.getRootLogger().info("Public SSL Exclude Cipher Suites:"+sslContextFactory.getExcludeCipherSuites());
        Log.getRootLogger().info("Public SSL Include Cipher Suites:"+sslContextFactory.getIncludeCipherSuites());
        Log.getRootLogger().info("Public Port:"+publicSslConnector.getPort());
    
        //Finding currently set connectors for the admin server context
        removeConnectors(servers,"admin");
    
        //Add an SSL Connector to the admin server context
        ServerConnector adminSslConnector = new ServerConnector(servers.get("admin"), sslContextFactory);
        adminSslConnector.setPort(8443);
        servers.get("admin").addConnector(adminSslConnector);
    
        Log.getRootLogger().info("CustomiseJettyServers - admin server info:");
        Log.getRootLogger().info("Admin SSL Exclude Cipher Suites:"+sslContextFactory.getExcludeCipherSuites());
        Log.getRootLogger().info("Admin SSL Include Cipher Suites:"+sslContextFactory.getIncludeCipherSuites());
        Log.getRootLogger().info("Admin Port:"+adminSslConnector.getPort());
    
        return servers;
    }

The code above will allow you to set the following up:

Custom keystore and truststore:

def SslContextFactory createSslContextFactory(String keyStorePath, String keyStorePassword, String trustStorePath,String trustStorePassword,String[] excludeProtocols,String[] includeCipherSuites) {

    SslContextFactory sslContextFactory = new SslContextFactory();
    sslContextFactory.setKeyStorePath(keyStorePath);
    sslContextFactory.setKeyStorePassword(keyStorePassword);
    sslContextFactory.setTrustStorePath(trustStorePath);
    sslContextFactory.setTrustStorePassword(trustStorePassword);

    sslContextFactory.addExcludeProtocols(excludeProtocols);
    sslContextFactory.setIncludeCipherSuites(includeCipherSuites);

    return sslContextFactory;
}

...

SslContextFactory sslContextFactory = createSslContextFactory(
              <$SEARCH_HOME>/web/conf/keystore","OBF:<keystore password>",
              <$SEARCH_HOME>/web/conf/keystore (** truststore **)","OBF:<trustore password>",
              excludeProtocols,includeCipherSuites);

Cipher suite inclusion and protocol exclusion

...
    String[] excludeProtocols = ["SSL","SSLv2","SSLv2Hello","SSLv3"];
    String[] includeCipherSuites = ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA"];
...

To ensure that ports don’t conflict on Jetty startup, edit $SEARCH_HOME/conf/global.cfg and remove values for jetty.search_port, jetty.search_port_https and jetty.admin_port:

jetty.search_port=
jetty.search_port_https=
jetty.admin_port=

Listed below is an example global.cfg with notes on what to enter:

mail.smtp.host=localhost
mail.smtp.port=25
mail.smtp.auth=false
platform.bitness=64bit
mail.smtp.user=
mail.smtp.password=
jetty.search_port=
jetty.search_port_https=
jetty.admin_port=
...
urls.search_port=<set to the same port as jetty.search_port >
urls.admin_port=<set to the CUSTOM admin port number>
...

Once $SEARCH_HOME/web/conf/customiseJettyServers.groovy and global.cfg have been edited, restart the jetty service.

Check the $SEARCH_HOME/log/jetty.log.* files for progress and for any errors.

Troubleshooting

jetty-logging.properties

Sometimes more detail can be gleaned by increasing the logging level found in Jetty.

Logging levels for $SEARCH_HOME/log/jetty.log* are defined in $SEARCH_HOME/web/conf/jetty-logging.properties.

The default logging level is set to INFO - this can be amended to the following levels:

  • ALL (log everything)

  • SEVERE (highest value)

  • WARNING

  • INFO

  • CONFIG

  • FINE

  • FINER

  • FINEST (lowest value)

  • OFF (log nothing)

General tips

Look for errors from customiseJettyServers.groovy in the jetty logs. If a syntax error is detected in the customiseJettyServers.groovy script it will not execute.

If customiseJettyServers.groovy contains code the following will be seen in the $SEARCH_HOME/log/jetty.log*:

SEVERE: *** Server config has been altered with /opt/funnelback/web/conf/customiseJettyServers.groovy ***

Syntax errors in customiseJettyServers.groovy (if they exist) will appear below the above message in quite close proximity.

Custom log messages from within the customiseJettyServers.groovy are generated using the Log.getRootLogger().LEVEL() call. e.g.

Log.getRootLogger().info("CustomiseJettyServers - public server info:");
Log.getRootLogger().info("Public SSL Exclude Cipher Suites:"+sslContextFactory.getExcludeCipherSuites());
Log.getRootLogger().info("Public SSL Include Cipher Suites:"+sslContextFactory.getIncludeCipherSuites());
Log.getRootLogger().info("Public Port:"+publicSslConnector.getPort());