This article provides guidance on ensuring that the embedded Tomcat server provided with the Spring Framework is configured in accordance with the Tomcat Secure Technical Implementation Guide STIG from DISA.

The following sections have been designed in an implementation-friendly, as opposed to an assessment, or testing-friendly, fashion. STIG identifiers are included where practical.

Long blocks of code have been collapsed, so please look out for sections like the following:

Important stuff here

Implementation Reference!


Assumptions


  • A fairly stock gradle setup with a project generated by the Spring Initializr is in use.
  • Where version numbers are specified, it is assumed that the user will use the latest available at the time of implementation
  • Your application is using Spring Security.

Disclaimer


I am not a Spring expert!

The material provided in this document should be considered reference material.

No warranties for fitness to any particular purpose are expressed or implied.

Sources were referenced when possible. If you feel that your material was used without reference, please contact the author for corrections to the oversight.

Corrections, updates, or better ways to do things are welcome as a PR to the source repository


FIPS 140-2/3 - TCAT-AS-000750


Starting with FIPS-enablement removes what may be the largest hurdle first. If you find that this becomes excessively problematic due to other parts of your infrastructure not being FIPS compatible, you can skip this and come back to it later.

IMPORTANT: Using a FIPS-validated cryptographic library does not make your application FIPS-validated.

Obtain BouncyCastle FIPS

NOTE: Some platforms, such as Red Hat Enterprise Linux and derivatives, have FIPS support in the underlying system.

If you are using one of these systems, you simply need to ensure that it is configured for your system java installation.

These instructions have been written assuming that you wish to use BouncyCastle for portability across systems.

Add the following to your build.gradle in the dependencies section:

implementation 'org.bouncycastle:bc-fips:1.0.2.3'
implementation 'org.bouncycastle:bctls-fips:1.0.13'
implementation 'org.bouncycastle:bcpkix-fips:1.0.6'

Relevant Documentation

Set up Custom Security Providers

Create a $projectDir/src/main/resources/java.security file with the following content:

security.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
security.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS
security.provider.3=com.sun.net.ssl.internal.ssl.Provider BCFIPS
security.provider.4=SUN
security.provider.5=

fips.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
fips.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS
fips.provider.3=com.sun.net.ssl.internal.ssl.Provider BCFIPS
fips.provider.4=SUN
fips.provider.5=

You may see a warning similar to invalid entry for security.provider.5 which can be ignored.

The empty line is required to keep the remaining internal security providers from loading.

Add the Necessary Options to build.gradle

plugins {
  <your other plugins>
  id 'application'
  id 'distribution'
}

apply plugin: 'application'
apply plugin: 'distribution'

application {
  applicationDefaultJvmArgs = [
    // STIG:> TCAT-AS-000750
    // STIG:> APSC-DV-001860
    // STIG:> APSC-DV-002010
    // STIG:> APSC-DV-002020
    // STIG:> APSC-DV-002030
    // STIG:> APSC-DV-002040
    // STIG:> APSC-DV-002050
    // STIG:> APSC-DV-003100
    "-Djava.security.properties=src/main/resources/java.security",
    "-Dorg.bouncycastle.fips.approved_only=true"
  ]
}

startScripts {
  // Needed to fix the java.security path
  doLast {
    unixScript.text = unixScript.text.replace('src/main/dist', '\$APP_HOME')
    windowsScript.text = windowsScript.text.replace('src/main/dist', '%~dp0..')
  }
}

jar {
  // This is the default
  enabled = true
}

Building

To build your application, run gradle assembleDist. The output will be located in build/distributions/<AppName>.zip.

Executing your application can be done by unzipping the artifact and executing the appropriate script in the bin directory.

How to verify your configuration

The following code can be used to verify the configuration:

import javax.crypto.Cipher;
import java.security.Provider;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import javax.crypto.Mac;

System.out.println("BC Approved Only Mode: " + CryptoServicesRegistrar.isInApprovedOnlyMode());

try {
  Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

  String algorithm = cipher.getAlgorithm();
  Provider provider = cipher.getProvider();
  int blockSize = cipher.getBlockSize();

  System.out.println("Output using default Provider:");
  System.out.println("Algorithm :"+algorithm);
  System.out.println("Provider Name:"+provider.getName());
  System.out.println("Block Size :"+blockSize);
}
catch(Exception e) {
  System.out.println(e);
}

try {
  Mac mac = Mac.getInstance("hmacMD5");
}
catch(Exception e) {
  System.out.println(e);
}

The expected output is as follows:

BC Approved Only Mode: true
Output using default Provider:
Algorithm :AES/CBC/PKCS5Padding
Provider Name:BCFIPS
Block Size :16
java.security.NoSuchAlgorithmException: Algorithm hmacMD5 not available

Necessary Code

To prevent the accidental use of our application without being FIPS enabled, we need to add code to ensure that the application fails to start if the configuration is incorrect.

To accomplish this, we can add a configuration parameter stig.fips-mode to allow FIPS to be toggled for debugging.

Create a file in your applications config directory called FipsCheck.java with the following content:

FipsCheck.java
package my.app.starter.config;

import javax.crypto.Mac;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;

import lombok.extern.slf4j.Slf4j;
import my.app.starter.config.exceptions.FipsCheckException;

@Slf4j
@Configuration
@ConditionalOnWebApplication(
  type = Type.SERVLET
)
public class FipsCheck implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>{
  @Value("${stig.fips-mode:true}")
  private boolean fipsMode;

  @Autowired
  ApplicationContext context;

  public void customize(TomcatServletWebServerFactory factory) {
    // Ideally, we will find a method for doing this without binding explicitly to Tomcat
    factory.addContextCustomizers((context) -> {
      Boolean fipsOK = false;

      try {
        Mac mac = Mac.getInstance("hmacMD5");
      }
      catch(Exception e) {
        fipsOK = true;
      }

      if ( fipsMode && fipsOK ) {
        log.info("FIPS Mode Correctly Enabled for Core Java");
      }
      else if ( !fipsMode ) {
        log.warn("FIPS Mode Disabled Via Configuration for Core Java");
      }
      else {
        throw new FipsCheckException("Core Java");
      }
    });
  }
}
``` </details>

Next, create a directory called `config/exceptions` and a file in that directory named `FipsCheckException.java` with the following content:

```java
package my.app.starter.config.exceptions;

public class FipsCheckException extends RuntimeException {

    public FipsCheckException(String target) {
        super("FIPS Mode Not Enabled in " + target);
    }
}

The Rest of the STIGs


Only the Tomcat Server May Be Used

Spring has the ability to use web servers other than Tomcat.

Users must ensure that no other webserver is selected for use since there is a STIG for Tomcat.

build.gradle Updates

You will need to add the following to your build.gradle.

Where items overlap with configuring the system for FIPS mode, simply append to the existing set.

implementation 'org.springframework.boot:spring-boot-starter-validation:2.6.7'

apply plugin: 'application'

application {
  applicationDefaultJvmArgs = [
    // STIG:> TCAT-AS-001660
    // STRICT_SERVLET_COMPLIANCE must be set to true
    "-Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=true",
    // STIG:> TCAT-AS-001680
    // ALLOW_BACKSLASH must be set to false.
    "-Dorg.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH=false",
    // STIG:> TCAT-AS-001690
    // ENFORCE_ENCODING_IN_GET_WRITER must be set to true
    "-Dorg.apache.catalina.connector.Response.ENFORCE_ENCODING_IN_GET_WRITER=true",
    // STIG:> TCAT-AS-001670
    // RECYCLE_FACADES must be set to true
    "-Dorg.apache.catalina.connector.RECYCLE_FACADES=true"
  ]
}

appplication.yaml Settings

Ensure that the following settings appear in the application.yaml file and not in a profile.

application.yaml
# NOTE: Just because a STIG item is listed here does not mean that it is
# fully met by setting the value. This only focuses on what Spring and
# Tomcat can do natively.

## TOMCAT STIG ##

# STIG:> TCAT-AS-000020
#   Secured connectors must be configured to use strong encryption ciphers.
# STIG:> TCAT-AS-000040
#   TLS 1.2 must be used on secured HTTP connectors.
server.ssl.enabled-protocols=TLSv1.2

# STIG:> TCAT-AS-000070
#   Cookies must have secure flag set.
server.servlet.session.cookie.secure: true

# STIG:> TCAT-AS-000080
#   Cookies must have http-only flag set.
server.servlet.session.cookie.http-only: true

# STIG:> TCAT-AS-000100
#   Connectors must be secured.
server.ssl.enabled: true

# STIG:> TCAT-AS-000170
# Tomcat servers behind a proxy or load balancer must log client IP.
server.tomcat.accesslog.enabled: true
server.tomcat.accesslog.request-attributes-enabled: true
server.tomcat.accesslog.directory: /dev
server.tomcat.accesslog.prefix: stdout
server.tomcat.accesslog.buffered: false
# The following two items must be empty!
server.tomcat.accesslog.suffix:
server.tomcat.accesslog.file-date-format:

# STIG:> TCAT-AS-000240
#   Date and time of events must be logged.
# STIG:> TCAT-AS-000250
#   Remote hostname must be logged.
# STIG:> TCAT-AS-000260
#   HTTP status code must be logged.
# STIG:> TCAT-AS-000270
#   The first line of request must be logged.
# STIG:> TCAT-AS-001080
#   Application user name must be logged.
server.tomcat.accesslog.pattern: "%h %l %t %u \"%r\" %s %b"

# STIG:> TCAT-AS-000470
#   Stack tracing must be disabled.
trace: false
server.error.include-stacktrace: never
server.error.whitelabel.enabled: false

# STIG:> TCAT-AS-000510
#   DefaultServlet debug parameter must be disabled.
server.servlet.register-default-servlet: false

# STIG:> TCAT-AS-000550
#   xpoweredBy attribute must be disabled.
#   This should be empty!
server.server-header:

# STIG:> TCAT-AS-000610
#   JMX authentication must be secured.
# STIG:> TCAT-AS-000630
#   TLS must be enabled on JMX.
spring.jmx.enabled: false

# STIG:> TCAT-AS-000750
#   Tomcat must use FIPS-validated ciphers on secured connectors
#
#   You will need to change this to "off" if your underlying platform is not
#   FIPS-enabled with the appropriate tomcat-native libraries installed
stig.spring.embedded-tomcat.fips-mode: "on"

## APPLICATION STIG ##

# APSC-DV-000447
#   The application must not be subject to input handling vulnerabilities.
server.tomcat.reject-illegal-header: true

# APSC-DV-001480
#   The application must prevent program execution in accordance with
#   organization-defined policies regarding software program usage and
#   restrictions, and/or rules authorizing the terms and conditions of
#   software program usage. (Least Privilege)
management.server.port: -1
management.endpoints.enabled-by-default: false
management.endpoints.web.exposure.include: ""
management.endpoints.jmx.exposure.include: ""

# APSC-DV-001660
#   Service-Oriented Applications handling non-releasable data must
#   authenticate endpoint devices via mutual SSL/TLS.
server.ssl.client-auth: true

# APSC-DV-002270
#   Applications must not use URL embedded session IDs.
server.servlet.session.tracking-modes: COOKIE

# APSC-DV-002310
#   The application must fail to a secure state if system initialization
#   fails, shutdown fails, or aborts fail.
server.shutdown: graceful

# APSC-DV-002500
#   The application must protect from Cross-Site Request Forgery (CSRF)
#   vulnerabilities.
server.servlet.session.cookie.same-site: Strict

Additional Application Code

The following items are necessary to meet STIG requirements that would usually be met via XML configuration files. Unfortunately, the embedded Tomcat does not allow for XML configuration so we need to do it under the hood.

Enable HSTS

TCAT-AS-000030 - HTTP Strict Transport Security (HSTS) must be enabled

Ensure that your application has HSTS enabled by adding the following to the class that you have created that hooks @EnableWebSecurity. This only shows the HSTS portion of the configuration, access controls and other items outside of the Tomcat STIG are left to the reader.

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.
      .headers()
        .httpStrictTransportSecurity()
        .maxAgeInSeconds(31536000)
        .includeSubDomains(true)
        .preload(false)
  }
}

Tomcat STIG Error Valve

To ensure that no information is inadvertently leaked, a custom Error Valve should be created in your config directory as TomcatStigErrorValve.java with the following content:

package my.app.starter.config;

import java.io.IOException;
import java.io.Writer;

import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ErrorReportValve;
import org.springframework.context.annotation.Configuration;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class TomcatStigErrorValve extends ErrorReportValve{
  // Based on work in https://github.com/spring-projects/spring-boot/issues/21257#issuecomment-745565376
  protected void report(Request request, Response response, Throwable throwable) {
    if  (!response.setErrorReported()) {
      return;
    }

    log.error("{} Fatal error before getting to Spring.", response.getStatus());
    try {
        Writer writer = response.getReporter();
        writer.write(Integer.toString(response.getStatus()));
        writer.write(" Fatal error. Could not process request.");
        response.finishResponse();
    } catch (IOException e) {}
  }
}

Tomcat STIG Class

Create TomcatStig.java in your config directory with the following content to apply the remainder of the STIG items.

This could be done in many ways but has been shown here in the most compact form.

Ideally, the Spring community would pick this up and turn it into a plugin (hint, hint)!

TomcatStig.java

package my.app.starter.config;

import javax.validation.constraints.Pattern;

import org.apache.catalina.Container;
import org.apache.catalina.core.AprLifecycleListener;
import org.apache.catalina.core.StandardHost;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;

import my.app.starter.config.TomcatStigErrorValve;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@ConditionalOnWebApplication(
  type = Type.SERVLET
)
public class TomcatStig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

  @Value("${server.ssl.enabled:false}")
  private boolean sslEnabled;

  @Value("${stig.fips-mode:true}")
  private boolean globalFipsMode;

  @Pattern(regexp = "^(on|off)$")
  @Value("${stig.spring.embedded-tomcat.fips-mode:on}")
  private String fipsMode;

  // Based on work from https://www.behindjava.com/run-a-fully-fips-compliant/
  @Autowired
  ApplicationContext context;

  public void customize(TomcatServletWebServerFactory factory) {
    // Set fipsMode to match the global if FIPS is disabled everywhere
    if ( !globalFipsMode ) {
      fipsMode = "off";
    }

    String pkg_name = this.getClass().getPackageName();

    // STIG:> TCAT-AS-000750
    //   Tomcat must use FIPS-validated ciphers on secured connectors
    AprLifecycleListener aprLifecycleListener = new AprLifecycleListener();
    aprLifecycleListener.setFIPSMode(fipsMode);

    if ( ! aprLifecycleListener.getFIPSMode().equals("on") ) {
      log.warn("FIPS Mode Disabled in Tomcat");
    }

    factory.addContextLifecycleListeners(aprLifecycleListener);
    // End TCAT-AS-000750

    factory.addConnectorCustomizers((connector) -> {
      // STIG:> TCAT-AS-000100
      //   Connectors must be secured
      connector.setSecure(sslEnabled);
      // End TCAT-AS-000100

      // STIG:> TCAT-AS-000550
      //   xpoweredBy attribute must be disabled.
      connector.setXpoweredBy(false);

      // STIG:> TCAT-AS-000470
      //   Stack tracking must be disabled
      connector.setAllowTrace(false);
      // End TCAT-AS-000470

      // STIG:> TCAT-AS-001670
      //   RECYCLE_FACADES must be set to true
      connector.setDiscardFacades(true);
      // End TCAT-AS-001670

      // Start APSC-DV-002520
      //   The application must protect from canonical representation vulnerabilities
      connector.setUseBodyEncodingForURI(true);
      // End APSC-DV-002520
    });

    factory.addContextCustomizers((context) -> {
      SecurityConstraint constraint = new SecurityConstraint();
      SecurityCollection collection = new SecurityCollection();

      // Apply to everything
      collection.setName("restricted_methods");
      collection.addPattern("/*");

      // STIG:> TCAT-AS-000090
      //   DefaultServlet must be set to readonly for PUT and DELETE
      collection.addMethod("PUT");
      collection.addMethod("DELETE");
      // End TCAT-AS-000090 - Readonly Defaults

      // STIG:> TCAT-AS-000040
      //   TLS 1.2 must be used on secured HTTP connectors
      if ( sslEnabled ) {
        collection.setName("https_only");
        constraint.setUserConstraint("CONFIDENTIAL");
      }
      // STIG:> TCAT-AS-000040

      constraint.addCollection(collection);

      // Start APSC-DV-000460
      //   The application must enforce approved authorizations for logical access
      //   to information and system resources in accordance with applicable
      //   access control policies.
      constraint.setAuthConstraint(true);
      // End APSC-DV-000460

      context.addConstraint(constraint);
    });

    // STIG:> TCAT-AS-000470
    //   Stack tracing must be disabled
    //
    // STIG:> TCAT-AS-000920
    //   ErrorReportValve showServerInfo must be set to false
    //
    // STIG:> TCAT-AS-000940
    //   ErrorReportValve showReport must be set to false
    //
    // This fully disables the embedded Tomcat stack trace and only presents the contents of the TomcatStigErrorValve.
    // See: https://github.com/spring-projects/spring-boot/issues/21257#issuecomment-745565376
    factory.addContextCustomizers((context) -> {
        Container parent = context.getParent();
        if ( parent instanceof StandardHost) {
          ((StandardHost) parent).setErrorReportValveClass(pkg_name + ".TomcatStigErrorValve");
        }
    });
    // End TCAT-AS-000940
    // End TCAT-AS-000920
    // End TCAT-AS-000470

    // STIG:> TCAT-AS-000590
    //   Applications in privileged mode must be approved by the ISSO
    factory.addContextCustomizers((context) -> {
      context.setPrivileged(false);
    });
    // End TCAT-AS-000590
  }
}

References


Troubleshooting


Once FIPS mode is enabled, you may run into various issues that have to be remedied.

Something is randomly breaking

If you find that your application is randomly crashing, the first thing to try is to disble FIPS mode to see if it has to do with your cryptography setup.

For testing, you can set stig.fips-mode: false in your application.yml but DO NOT MAKE THIS THE DEFAULT.

The underlying system isn’t FIPS compliant

You can individually disable FIPS for Tomcat (but leave it on for BouncyCastle) by setting the following in application.yaml:

  • stig.spring.embedded-tomcat.fips-mode: "off"
    • Note: The quotes are important around off due to YAML switching it to a Boolean natively
    • Different operating systems have different methods for enabling FIPS in tomcat so you will need to consult your systems documentation for details. That said, it is HIGHLY recommended that you work inside a FIPS-enabled virtual machine for initial testing.
      • Ubuntu requires the libtcnative-1 package
      • RHEL requires the tomcat-native package from EPEL
      • macOS needs to re-roll libtcnative using a FIPS-compliant OpenSSL
        • This is not the default
Your passwords are not long enough

If you see an error like the following, then you need to make sure that your passwords meet the required length.

org.bouncycastle.crypto.fips.FipsUnapprovedOperationError:
password must be at least 112 bits

In this case, the password must be at least 14 characters (112 bits).

The application hangs

You may discover that your application hangs during startup. This is because the cryptography library has been configured to use SecureRandom instead of the regular random pool.

Modern systems should have a Trusted Platform Module (or CPU-bound equivalent) so you may neve rencounter this issue. However, if you do, you will need to install a Pseudo-Random Number Generator (PRNG) on your system to prevent hangs.

java.io.IOException: DER length more than 4 bytes: 109

If you see this error, you are trying to use a jks file that has not been created in a FIPS-compatible format.

The best solution is to use a p12 file if possible.

Converting jks to p12 with keytool

Use the following command to convert your Java Keystore to PKCS12 format.

Since we are using the Bouncy Castle FIPS libraries, we want to ensure that they used for the conversion so that we prevent any possible issues.

If you run into issues with the conversion, you may need to use a non-FIPS keytool for the initial conversion since not all jks files are FIPS-compatible.

keytool \
  -importkeystore \
  -srckeystore <keystore.jks> \
  -srcstoretype JKS \
  -srcstorepass <src_password> \
  -destkeystore <keystore.p12> \
  -deststoretype PKCS12 \
  -deststorepass <dest_password>

Verifying your p12 file

To verify that your keystore works with the FIPS-enabled system, you need to use keytool with the BCFIPS provider enabled as follows.

keytool \
  -list \
  -keystore <keystore.p12> \
  -storetype PKCS12 \
  -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvder \
  -providerpath </path/to>/bc-fips-<version>.jar \
  -storepass <password>

This work is derived from work in the performance of Federal Government Contract Number W52P1J-21-F-0245.
Any copyright in this work is subject to the Government's Unlimited Rights license as defined in DFARS 252.227-7013 and/or DFARS 252.227-7014.
The reproduction of this work for commercial purposes is strictly prohibited.
Nongovernmental users may copy and distribute this document in any medium, either commercially or noncommercially, provided that this copyright notice is reproduced in all copies.
Nongovernmental users may not use technical measures to obstruct or control the reading or further copying of the copies they make or distribute.
Nongovernmental users may not accept compensation of any manner in exchange for copies.
All other rights reserved.