Tomcat application server

Security

Using LDAP server for BASIC authentication

In this setup we will use the office-wide LDAP authentication server to authenticate the client using HTTP basic authentication (digest will not work).

First of all discover the structure of LDAP directory. You need to know the LDAP server URL, DN prefix for user and group search:

$ ldapsearch -H ldap://ldap.company.org:389/ -D uid=myuid,ou=people,dc=company,dc=org -w <mypassword> -b 'uid=myuid,ou=people,dc=company,dc=org'

dn: uid=myuid, ou=people, dc=company, dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
uid: myuid
cn: Dmitry Katsubo
sn: Katsubo
givenname: Dmitry
displayname: Dmitry Katsubo
...

For group (role) search we will use the following filter:

$ ldapsearch -H ldap://ldap.company.org:389/ -D uid=myuid,ou=people,dc=company,dc=org -w <mypassword> -b 'ou=groups,dc=company,dc=org' -s sub '(member=uid=myuid,ou=people,dc=company,dc=org)'

dn: cn=subdiv5587, ou=groups, dc=company, dc=org
objectclass: top
objectclass: groupOfNames
cn: subdiv5587
description: Subdevision 55.87
member: uid=myuid, ou=people, dc=company, dc=org
... other members ...

dn: cn=div55, ou=groups, dc=company, dc=org
objectclass: top
objectclass: groupOfNames
cn: div55
description: Devision 55
member: uid=myuid, ou=people, dc=company, dc=org
... other members ...

Based on the discovered information we define in Tomcat's server.xml the following realm1):

server.xml

<Server ...>
    ...
    <Realm className="org.apache.catalina.realm.CombinedRealm">
        <Realm  className="org.apache.catalina.realm.UserDatabaseRealm"
                resourceName="UserDatabase"/>
 
        <Realm  className="org.apache.catalina.realm.JNDIRealm"
                connectionURL="ldap://ldap.company.org:389/"
                alternateURL="ldap://ldap-replica.company.org:389/"
                userPattern="uid={0},ou=people,dc=company,dc=org"
                roleBase="ou=groups,dc=company,dc=org"
                roleName="cn"
                roleSearch="(member={0})"
                roleSubtree="false" />
    </Realm>
</Server>

This combined realm uses the local user database (e.g. you want to define local Tomcat admins or application deployers) as well as the LDAP directory to authenticate. The last one applies authentication-by-binding mechanism. If want client to check the password, use userSearch + userSubtree JNDIRealm properties.

Now you can say in application's WEB-INF/web.xml:

web.xml

<web-app version="2.4" ...>
    ...
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>All Pages</web-resource-name>
            <url-pattern>/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>dir2711</role-name>
        </auth-constraint>
    </security-constraint>
 
    <security-role id="SecurityRole_0">
        <description>The role that is assigned to all members of Subdevision 55.87.</description>
        <role-name>subdiv5587</role-name>
    </security-role>
 
    <login-config>
        <auth-method>BASIC</auth-method>
        <realm-name>MyApplication</realm-name>
    </login-config>
</web-app>

You can define several <url-pattern>s and several <http-method>s in <web-resource-collection>. The <role-name> from <auth-constraint> should be also described by <security-role>.

<auth-method> represents basic HTTP/1.1 authentication methods:

  • BASIC, DIGEST – described in 2617
  • NEGOTIATE – described in 4559
  • FORM – means the credentials should be entered via HTML form defined by <form-login-config> element2).

In the code one can use HttpServletRequest#getRemoteUser() and HttpServletRequest#isUserInRole() methods:

package org.company.application.web;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
 
@Controller
public class MyWebController {
 
    private static final Log logger = LogFactory.getLog(MyWebController.class);
 
    @RequestMapping(value = "/processFile", method = RequestMethod.POST)
    public void processFile(HttpServletRequest request, HttpServletResponse response)
    {
        logger.debug("User:" + request.getRemoteUser() + ", is in directorate:" + request.isUserInRole("subdiv5587"));
        ...
    }
}

Let's test the above declared POST request handler:

$ wget -S -nv -t 1 --post-file myfile.txt -O - http://myuid:mypasword@service-id.company.org:8080/application/processFile

  HTTP/1.1 401 Unauthorized
  Server: Apache-Coyote/1.1
  WWW-Authenticate: Basic realm="MyApplication"
  Content-Type: text/html;charset=utf-8
  Content-Length: 954
  Date: Thu, 05 Aug 2010 16:00:40 GMT
  Connection: keep-alive

  HTTP/1.1 200 OK
  Server: Apache-Coyote/1.1
  Content-Type: text/plain;charset=US-ASCII
  Content-Length: 3999
  Date: Thu, 05 Aug 2010 16:01:09 GMT
  Connection: keep-alive

In a log file:

/var/log/tomcat6/tomcat.log

DEBUG [org.apache.catalina.authenticator.AuthenticatorBase] Security checking request POST /application/processFile
DEBUG [org.apache.catalina.realm.RealmBase]   Checking constraint 'SecurityConstraint[All Pages]' against POST /processFile --> true
DEBUG [org.apache.catalina.authenticator.AuthenticatorBase]  Calling hasUserDataPermission()
DEBUG [org.apache.catalina.realm.RealmBase]   User data constraint has no restrictions
DEBUG [org.apache.catalina.authenticator.AuthenticatorBase]  Calling authenticate()
DEBUG [org.apache.catalina.realm.CombinedRealm] Attempting to authenticate user "myuid" with realm "org.apache.catalina.realm.UserDatabaseRealm/1.0"
DEBUG [org.apache.catalina.realm.CombinedRealm] Cannot find message associated with key combinedRealm.authFail
DEBUG [org.apache.catalina.realm.CombinedRealm] Attempting to authenticate user "myuid" with realm "org.apache.catalina.realm.JNDIRealm/1.0"
DEBUG [org.apache.catalina.core.ContainerBase.[Catalina]] Found role: div55
DEBUG [org.apache.catalina.core.ContainerBase.[Catalina]] Found role: subdiv5587
DEBUG [org.apache.catalina.realm.CombinedRealm] Authenticated user "myuid" with realm "org.apache.catalina.realm.JNDIRealm/1.0"
DEBUG [org.apache.catalina.authenticator.AuthenticatorBase] Authenticated 'myuid' with type 'BASIC'
DEBUG [org.apache.catalina.authenticator.AuthenticatorBase]  Calling accessControl()
DEBUG [org.apache.catalina.realm.RealmBase]   Checking roles GenericPrincipal[myuid(div55,subdiv5587)]
DEBUG [org.apache.catalina.realm.RealmBase] Different realm org.apache.catalina.realm.CombinedRealm@1d32e45 org.apache.catalina.realm.JNDIRealm@12f1bf0
DEBUG [org.apache.catalina.realm.RealmBase] Username myuid has role subdiv5587
DEBUG [org.apache.catalina.realm.RealmBase] Role found:  subdiv5587
DEBUG [org.apache.catalina.authenticator.AuthenticatorBase]  Successfully passed all security constraints
DEBUG [org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping] Mapping [/processFile] to handler 'org.company.application.web.MyWebController@24f4eb'
DEBUG [org.company.application.web.MyWebController] User:myuid, is in directorate:true

For more information see Realm Configuration HOWTO.

Using NEGOTIATE authentication

General installation steps:

  • Create /etc/krb5.conf (or use the system-wide one, defined by Linux administrator):

    krb5.conf

    [libdefaults]
    default_realm = REALM.COMPANY.ORG
    default_tkt_enctypes = des-cbc-md5 des-cbc-crc
    default_tgs_enctypes = des-cbc-md5 des-cbc-crc
    
    kdc_timesync = 0
    kdc_default_options = 0x40000010
    clockskew = 300
    check_delegate = 0
    ccache_type = 3
    kdc_timeout = 60
    
    [domain_realm]
    company.org = REALM.COMPANY.ORG
    .company.org = REALM.COMPANY.ORG
    
    [realms]
    REALM.COMPANY.ORG = {
        admin_server = server1.company.org
        kdc = server1.company.org
        kdc = server2.company.org
    }
  • Additionally to the following installation instructions, use this:

    Installation of SF SPNEGO module

    For Jetty see How to configure Jetty to use SPNEGO/NEGOTIATE authentication.
    For local services development, follow SPNEGO Tomcat instructions:

    Installation of in-house SPNEGO

Questions answered

Tomcat cannot start due to java.net.SocketException

The following error is in server log file:
SEVERE: StandardServer.await: create[8005]:
java.net.SocketException: Invalid argument
        at java.net.PlainSocketImpl.socketBind(Native Method)
        at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:365)
        at java.net.ServerSocket.bind(ServerSocket.java:319)
        at java.net.ServerSocket.<init>(ServerSocket.java:185)
        at org.apache.catalina.core.StandardServer.await(StandardServer.java:373)
        at org.apache.catalina.startup.Catalina.await(Catalina.java:662)
        at org.apache.catalina.startup.Catalina.start(Catalina.java:614)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:289)
        at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:414)

Start your JVM with -Djava.net.preferIPv4Stack=true (see Java Networking Properties) or set net.ipv6.bindv6only kernel system variable to zero (see this conversion):

/etc/sysctl.d/bindv6only.conf

# This sysctl sets the default value of the IPV6_V6ONLY socket option.
#
# When disabled, IPv6 sockets will also be able to send and receive IPv4
# traffic with addresses in the form ::ffff:192.0.2.1 and daemons listening
# on IPv6 sockets will also accept IPv4 connections.
#
# When IPV6_V6ONLY is enabled, daemons interested in both IPv4 and IPv6
# connections must open two listening sockets.
# This is the default behaviour of all modern operating systems.

net.ipv6.bindv6only = 0

The form fields values submitted by a browser are not correctly UTF-8 encoded

From Character Encoding Issues:

For correct UTF-8 URL handling set <Connector ... URIEncoding="UTF-8" /> option in server.xml or use <Connector ... useBodyEncodingForURI="true" />.

The contents (request body) is not correctly represented because by default the character encoding of HttpServletRequest is set to ISO-8859-1 (unless is provided in browser request). Spring has a special CharacterEncodingFilter filter that fixes this (see sample here).

Unable to add the resource at [/…] to the cache for web application [/app] because there was insufficient free space available after evicting expired cache entries - consider increasing the maximum size of the cache

To fix this add:

/etc/tomcat8/context.xml

<Context>
  ...
  <Resources cachingAllowed="true" cacheMaxSize="100000" />

How to specify the context path for application deployment in Tomcat?

Before Tomcat 6 the context path could have been specified in META-INF/context.xml

META-INF/context.xml

<Context path="/myapp">
</Context>

however this behaviour has changed in Tomcat 6 (see Context Container, bug#46713 and Tomcat's context path for a webapp not working):

path attribute must only be used when statically defining a Context in server.xml. In all other circumstances, the path will be inferred from the filenames used for either the .xml context file or the docBase.

For deployment to Tomcat 6 you should use tomcat-maven-plugin (that deploys via Tomcat Manager):

pom.xml

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>tomcat-maven-plugin</artifactId>
    <configuration>
        <!-- Server credentials are stored in settings.xml (should match with ones in /etc/tomcat6/tomcat-users.xml): -->
        <server>tomcat-server-id</server>
        <url>${tomcat.server.url}/manager</url>
        <!-- Deployment context: -->
        <path>${context.path}</path>
    </configuration>
</plugin>

Why ImageIO library does not work when deployed to Tomcat AS?

The problem is correctly described in comment #13 for bug 788160 (Error reading in TIFF images):

Tomcat has build-in memory leak protections, one of which does not allow to automatically image providers from web application in JVM-wide ImageIO registry. This is done by JreMemoryLeakPreventionListener which initializes ImageIO (call to any static method forces static registry to be initialized).

As a workaround appContextProtection has to be disabled for JreMemoryLeakPreventionListener:

server.xml

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" appContextProtection="false"/>

however this is not a complete solution as if application is redeployed again and again, the references to image providers will still be kept together with classloader and all classes it has loaded. This will cause PermGen to run out of memory and under certain conditions may cause ClassCastException when using ImageIO.

One solution is provided in IIOProviderContextListener.java (see How to resolve OutOfMemoryError with ImageIO plugins as the cause), and basically consists of the following steps:

  • Web application needs to call ImageIO.scanForPlugins() before using any ImageIO API (servlet initialization).
  • On destruction, web application needs to deregister providers loaded from web application classpath.

Another solution is to put jai_imageio.jar + jai_codec.jar into server-wide lib/ folder.

References:

How to enable log4j logging for Tomcat?

See steps here. Recommended Log4j configuration:

/usr/share/tomcat6/lib/log4j.properties

log4j.rootLogger=INFO, R
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=${catalina.base}/logs/tomcat.log
log4j.appender.R.MaxFileSize=50MB
log4j.appender.R.MaxBackupIndex=5
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%d{dd.MM HH:mm:ss} %-5p [%c] %m%n
 
# For a more detailed Catalina localhost log, uncomment below:
#log4j.logger.org.apache.catalina.core.ContainerBase.[Catalina].[localhost]=DEBUG
#log4j.logger.org.apache.catalina.core=DEBUG
#log4j.logger.org.apache.catalina.session=DEBUG

If you remove /etc/tomcat6/logging.properties file you will not be able to see the debug output done via java.util.logging. So either keep it, or remove/rename and continue with steps below.

Also you might be interested in redirecting the java.util.logging to Log4j completely so you don't need to dig two log files for errors as everything will be in one file. For that you need to do further steps to one mentioned above:

  • Download jul-log4j-bridge from here and put it to /usr/share/tomcat6/lib.
  • Make sure you have Log4j implementation (e.g. log4j-1.2.15.jar) in /usr/share/tomcat6/lib.
  • Include apache-jul-log4j-bridge.jar, log4j-1.2.15.jar and log4j.properties directory location into Tomcat boot classpath:

    /usr/share/tomcat6/bin/catalina.sh

    --- catalina.sh.orig    2010-06-14 21:21:36.000000000 +0200
    +++ catalina.sh 2010-08-09 13:06:38.000000000 +0200
    @@ -164,12 +164,17 @@
     # Add tomcat-juli.jar and bootstrap.jar to classpath
     # tomcat-juli.jar can be over-ridden per instance
     if [ ! -z "$CLASSPATH" ] ; then
    -  CLASSPATH="$CLASSPATH":
    +  CLASSPATH="$CLASSPATH:"
     fi
    -if [ "$CATALINA_BASE" != "$CATALINA_HOME" ] && [ -r "$CATALINA_BASE/bin/tomcat-juli.jar" ] ; then
    -  CLASSPATH="$CLASSPATH""$CATALINA_BASE"/bin/tomcat-juli.jar:"$CATALINA_HOME"/bin/bootstrap.jar
    -else
    -  CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar
    +
    +CLASSPATH="$CLASSPATH""$CATALINA_HOME"/bin/bootstrap.jar
    +
    +if [ "$CATALINA_BASE" != "$CATALINA_HOME" ] && [ -r "$CATALINA_HOME/bin/tomcat-juli.jar" ] ; then
    +  CLASSPATH="$CLASSPATH:$CATALINA_HOME/bin/tomcat-juli.jar"
    +fi
    +
    +if [ -r "$CATALINA_HOME/lib/apache-jul-log4j-bridge.jar" -a -r "$CATALINA_HOME/lib/log4j-1.2.15.jar" ] ; then
    +  CLASSPATH="$CLASSPATH:$CATALINA_HOME/lib/apache-jul-log4j-bridge.jar:$CATALINA_HOME/lib/log4j-1.2.15.jar:$CATALINA_HOME/lib"
     fi
    
     if [ -z "$CATALINA_OUT" ] ; then
  • Override java.util.logging.manager property either via LOGGING_MANAGER or JAVA_OPTS (check your startup scripts and catalina.sh about how to do it correctly):

    /etc/default/tomcat6

    LOGGING_MANAGER="-Djava.util.logging.manager=org.apache.logging.julbridge.JULBridgeLogManager"
  • If you install Tomcat on SuSE from rpm, instead of fixing catalina.sh you need to fix /etc/init.d/tomcat6 (or /usr/sbin/dtomcat6):

    /usr/bin/dtomcat6

    --- /usr/bin/dtomcat6.orig     2011-02-22 23:20:16.000000000 +0100
    +++ /usr/bin/dtomcat6  2011-12-12 10:29:12.000000000 +0100
    @@ -24,6 +25,9 @@
     fi
     CLASSPATH="${CLASSPATH}:${CATALINA_HOME}/bin/bootstrap.jar"
     CLASSPATH="${CLASSPATH}:${CATALINA_HOME}/bin/tomcat-juli.jar"
    +CLASSPATH="${CLASSPATH}:${CATALINA_HOME}/lib/apache-jul-log4j-bridge.jar"
    +CLASSPATH="${CLASSPATH}:${CATALINA_HOME}/lib/log4j-1.2.15.jar"
    +CLASSPATH="${CLASSPATH}:${CATALINA_HOME}/lib"
     CLASSPATH="${CLASSPATH}:$(build-classpath commons-daemon 2>/dev/null)"
    
     if [ "$1" = "start" ]; then
    @@ -34,7 +38,7 @@
         -Djava.endorsed.dirs="$JAVA_ENDORSED_DIRS" \
         -Djava.io.tmpdir="$CATALINA_TMPDIR" \
         -Djava.util.logging.config.file="${CATALINA_BASE}/conf/logging.properties" \
    -    -Djava.util.logging.manager="org.apache.juli.ClassLoaderLogManager" \
    +    -Djava.util.logging.manager="org.apache.logging.julbridge.JULBridgeLogManager" \
         org.apache.catalina.startup.Bootstrap start \
         >> ${CATALINA_BASE}/logs/catalina.out 2>&1 &
         if [ ! -z "$CATALINA_PID" ]; then

Finally, checklist:

  • /usr/share/tomcat6/bin contains:
    • catalina.sh (patched for classpath)
    • tomcat-juli.jar (from extras)
  • /usr/share/tomcat6/lib contains:
    • tomcat-juli-adapters.jar (from extras)
    • log4j.properties (newly created)
    • log4j-1.2.15.jar
    • apache-jul-log4j-bridge.jar
  • /etc/default/tomcat6 is fixed for LOGGING_MANAGER.

and you are ready to restart Tomcat and enjoy the result.

For slf4j logging, check here.

Quick deployment of Web application

Tomcat/Jetty remote JMX monitoring of JVM state

Enabling HTTP/2

Several resposes from mod_jk could not be multiplexed into one HTTP/2 stream, see this post. To enable multiplexing JBoss 5.x / Apache Tomcat 9 is required. There could be different options where to offload TLS:
  • Decrypt traffic and re-encrypt it for backend (ProxyPass "/path" "h2://jboss.server" + <UpgradeProtocol className=“org.apache.coyote.http2.Http2Protocol” /> inside TLS enabled <Connector …> element).
  • Decrypt traffic and send it plaintext to backend (ProxyPass "/path" "h2c://jboss.server" + <UpgradeProtocol className=“org.apache.coyote.http2.Http2Protocol” /> inside <Connector …> element).

See also:

1) Check another Tomcat configuration example for LDAP authentication here
2) See Java Servlet 2.4 Specification, §12.5 "Authentication" for more details about authentication methods and roles configuration
3) Provides XmiRoleMappingLoginModule module, that is responsible for mapping users to security roles using the mapping file application-bnd.xmi
4) Provides the support of Microsoft Kerberos Extension called PAC (Privilege Attribute Certificate)
5) Bouncy Castle Crypto package – a Java implementation of cryptographic symmetric block algorithms (AES, Blowfish, CASTx, DES, IDEA, RCx, Rijndael, …), symmetric stream algorithms (RC4, HCx, Grain, …), asymmetric block algorithms (OAEP, PCKS1, …) and digest algorithms(MDx, SHAx, RipeMD, Tiger, …)
software/tomcat.txt · Last modified: 2012/10/10 12:18 by dmitry
 
 
Recent changes RSS feed Driven by DokuWiki