> ## Documentation Index
> Fetch the complete documentation index at: https://docs.minimus.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Java FIPS Tutorial

> How to build Java applications with Minimus FIPS 140-3 validated images

To run Java workloads in FIPS-compliant environments, you need to use FIPS-validated build and runtime images and make sure the code is correctly configured. This guide walks through the process of migrating a Java project and building it with Minimus FIPS 140-3 validated images. The guide explains the underlying concepts, image differences, and migration steps.

This guide is for:

* Application developers migrating Java services from standard OpenJDK images
* DevOps and platform teams owning Docker and Kubernetes rollout
* Compliance and security teams reviewing migration controls

## Overview

Use Minimus images to build a FIPS compliant Java app using FIPS 140-3 validated images. These images are configured with FIPS-validated cryptographic providers and enforce strict FIPS compliance at runtime to ensure cryptographic operations are compliant with Federal Information Processing Standards. To be FIPS-compliant, every cryptographic operation (encryption, hashing, key generation, TLS) must go through a CMVP-certified provider.

### Multi-stage build technique

Multi-stage builds are great for keeping the build and runtime environments separate and include only the compiled .class in the final image. The recommended process for all workloads is to build with Maven/Gradle/OpenJDK-FIPS and use OpenJRE-FIPS for the runtime stage.

Java development in Minimus typically employs multi-stage Dockerfiles that separate the build/compile and runtime stages by using complementary images:

* **OpenJDK (Open Java Development Kit)** is an open-source implementation of the Java Platform, Standard Edition (Java SE). Use it to compile, package, run unit tests, and any step that needs Java build tooling. It includes a Java compiler `javac` and the full JDK.
* **OpenJRE (open-source Java Runtime Environment)** is used to run Java applications built with OpenJDK. OpenJRE includes the JVM (Java Virtual Machine) and core libraries needed to run Java applications, without the compiler `javac`. OpenJRE has a smaller footprint than the OpenJDK image and only includes what is needed to run a compiled Java application.

<Info>
  See also the [**Minimus tutorial for Java**](https://docs.minimus.io/guides/java) and [**Minimus tips for multi-stage builds**](/guides/multi-stage-build).
</Info>

### Components

| **Component**                                   | **Purpose**                                                                     |
| ----------------------------------------------- | ------------------------------------------------------------------------------- |
| `reg.mini.dev/maven`<br />`reg.mini.dev/gradle` | Build stage images                                                              |
| `reg.mini.dev/openjdk-fips`                     | Minimal OpenJDK image, full JDK toolset, does not <br />include Maven or Gradle |
| `reg.mini.dev/openjre-fips`                     | Production runtime stage, leaner JRE-only image                                 |
| Application JAR                                 | Build artifact copied from the build stage into the runtime stage               |
| Optional BCFKS keystore                         | Required keystore format for private keys in FIPS deployments                   |

### Environment variables

The images are pre-set with the following environment variables:

| **Variable**          | **Value**                                                                                                                                                      |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CLASSPATH`           | Includes FIPS libraries from `/usr/share/fips-libs/*`                                                                                                          |
| `JAVA_FIPS_CLASSPATH` | Explicit FIPS classpath reference                                                                                                                              |
| `JAVA_HOME`           | Points to the default JVM installation                                                                                                                         |
| `JDK_JAVA_OPTIONS`    | `--add-exports=java.base/sun.security.internal.spec=ALL-UNNAMED --add-exports=java.base/sun.security.provider=ALL-UNNAMED -Djavax.net.ssl.trustStoreType=FIPS` |

<Warning>
  Never overwrite `JDK_JAVA_OPTIONS`, `CLASSPATH`, or `JAVA_FIPS_CLASSPATH` in your Dockerfile or at runtime. These variables carry the FIPS provider wiring pre-set by the image. Always extend them by referencing the existing value, `${JDK_JAVA_OPTIONS}`, rather than replacing it. Overwriting any of these will silently break FIPS compliance at runtime.
</Warning>

### The critical `java -jar` problem

`java -jar` ignores `CLASSPATH` (and any `-cp`/`--class-path` you might pass), so the FIPS provider jars do not get loaded unless they are on the bootstrap classpath. Regardless of whether `-Xbootclasspath/a` is supplied directly in your `ENTRYPOINT` or injected via `JDK_JAVA_OPTIONS`, the CCJ provider jars must be appended to the bootstrap classpath; otherwise the JVM will have no available FIPS ciphers. The symptoms are either an empty cipher list or a `NoSuchAlgorithmException` at startup.

The correct way to launch applications from a FIPS image is to use `-Xbootclasspath/a` to append the FIPS jars directly to the bootstrap classloader, which is not bypassed by `java -jar`:

```dockerfile theme={null}
ENTRYPOINT ["java", "-Xbootclasspath/a:/usr/share/fips-libs/*", "-jar", "/app/app.jar"]
```

The wildcard `/usr/share/fips-libs/*` loads all jars in that directory. Do not hardcode specific jar filenames, as the version numbers change as the image updates. Pinning the version numbers will cause silent classpath failures.

## FIPS approved algorithms

FIPS approved-only mode (`com.safelogic.cryptocomply.fips.approved_only=true`) enforces a hard algorithm blocklist. Your application will throw a `NoSuchAlgorithmException` or `GeneralSecurityException` at runtime - not at compile time - if it calls any of the blocked algorithms.

Below is a table showing the recommended migration paths:

| **Algorithms blocked by FIPS**  | **FIPS-approved replacement algorithm** |
| ------------------------------- | --------------------------------------- |
| MD5 (for any security purpose)  | SHA-256 or SHA-3                        |
| SHA-1 signatures                | SHA-256 or stronger                     |
| DES / 3DES                      | AES-128 or AES-256                      |
| RC4                             | AES-GCM                                 |
| TLS 1.0 / TLS 1.1               | TLS 1.2 or TLS 1.3                      |
| RSA or DH keys under 2048 bits  | RSA-2048 minimum, RSA-3072 preferred    |
| PKCS#12 for private key storage | BCFKS keystore format                   |

To migrate your project to FIPS mode, you will need to audit your codebase, and search for string literals like `"MD5"`, `"SHA1"`, `"DES"`, `"RC4"`, `"TLSv1"`, and `"PKCS12"` in any `getInstance()` or `KeyStore.getInstance()` calls.

## How to deploy Java with the Minimus OpenJRE-FIPS image

### Prerequisites

* Docker or Podman available locally
* Token to pull images from the Minimus image registry
* Existing Java project (Maven or Gradle)
* A working test environment for smoke tests and crypto-related checks
* Host with FIPS-enabled kernel as listed in the CMVP certificate

<Warning>
  Minimus Java FIPS images use a kernel-dependent FIPS module. Unlike Minimus OpenSSL-based FIPS images, Java FIPS images require a FIPS-enabled kernel and specialized hardware as listed in CMVP certificate [#4912](https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/4912). Verify your target environment meets these requirements before deploying. To check whether a specific Minimus image includes the Java FIPS module, look for the package `minimus-java-fips-libs` in the image SBOM.
</Warning>

### Step 1: Pre-flight modifications

<Steps>
  <Step title="Update your application to use only FIPS-approved algorithms">
    The first step is to audit your application for non-FIPS algorithm usage. Before changing your Dockerfile, scan your codebase for algorithm strings that FIPS will reject at runtime.

    Common offenders are `MD5`, `SHA1`, `SHA-1`, `DES`, `RC4`, `TLSv1`, `TLS1.0`, `TLS1.1`, and `PKCS12` used as a keystore type for private keys.

    Replace any incompatible algorithms found with FIPS-approved equivalents and ensure TLS configuration specifies a minimum of `TLSv1.2`. [See the list of approved algorithms](#fips-approved-algorithms)

    Run your existing test suite before proceeding.
  </Step>

  <Step title="Update your Dockerfile">
    Update your Dockerfile to use Minimus images:

    1. Use a build image that includes your build tooling ([Maven ](https://images.minimus.io/gallery/images/maven/risk-reduction)or [Gradle](https://images.minimus.io/gallery/images/gradle/risk-reduction)).  We need to use Maven or Gradle because the Minimus OpenJDK-FIPS image does not include Maven or Gradle by default. 

    <Tip>
      The Minimus [OpenJDK-FIPS image](https://images.minimus.io/gallery/images/openjdk-fips/risk-reduction) is a minimal JDK image that does not include Maven or Gradle by default. Therefore it cannot be used for the build stage.

      \
      You can use the OpenJDK-FIPS image for the build stage if you install Maven in a prior step or supply your own build tool, such as a Maven Wrapper.
    </Tip>

    2. Copy the produced JAR into the [OpenJRE-FIPS image](https://images.minimus.io/gallery/images/openjre-fips/risk-reduction) for the runtime stage.
    3. Make sure the ENTRYPOINT includes `-Xbootclasspath/a:/usr/share/fips-libs/*` to load the CCJ FIPS provider. Without this flag, `java -jar` bypasses the image's pre-set `CLASSPATH` (and any `-cp`/`--class-path` you might pass), so the FIPS provider jars are never loaded into the bootstrap classloader and the JVM will have no available FIPS ciphers.
    4. Use a wildcard such as `/usr/share/fips-libs/*` to load all CryptoComply jars in that directory. Avoid hardcoding specific jar filenames such as `ccj-4.0.0-fips.jar` since the version numbers change as the image updates. Pinning the version numbers will cause classpath failures silently and should be avoided.
    5. Set a Main-Class to avoid errors. If your build produces a non-executable JAR (no `Main-Class` manifest entry), `java -jar /app/app.jar` will fail with the error: `no main manifest attribute`. Ways to fix this issue:
       * Configure Maven or Gradle to produce an executable JAR (set `Main-Class`)
       * Run the app with an explicit main class, e.g. `java -cp /app/app.jar com.example.App`.
  </Step>
</Steps>

### Step 2: Deploy your Java project

<Steps>
  <Step title="Prepare your Dockerfile">
    You can use the below example Dockerfile with your Java project. 

    <CodeGroup>
      ```dockerfile Dockerfile example using Maven theme={null}
      # Use the Minimus Maven image for the build stage

      FROM reg.mini.dev/maven:latest AS builder
      WORKDIR /workspace

      COPY pom.xml ./
      COPY src ./src

      RUN mvn -DskipTests package

      FROM reg.mini.dev/openjre-fips:21
      WORKDIR /app

      COPY --from=builder /workspace/target/*.jar /app/app.jar

      # Extend existing options, do not replace them

      ENV JDK_JAVA_OPTIONS="${JDK_JAVA_OPTIONS} -XX:+ExitOnOutOfMemoryError"

      ENTRYPOINT ["java", "-Xbootclasspath/a:/usr/share/fips-libs/*", "-jar", "/app/app.jar"]
      ```
    </CodeGroup>
  </Step>

  <Step title="Validate that image roles are correct">
    Confirm the build image (`openjdk-fips`) has compiler access:

    ```bash Confirm theme={null}
    docker run --rm reg.mini.dev/openjdk-fips:21 javac -version
    ```

    Confirm the runtime image (`openjre-fips`) is JRE-only:

    ```bash Confirm theme={null}
    docker run --rm reg.mini.dev/openjre-fips:21 java -version
    ```

    If any step in your Dockerfile requires `javac` or other JDK tools, it belongs in the `openjdk-fips` build stage. The `openjre-fips` runtime stage should only execute the already-compiled artifact.
  </Step>

  <Step title="Build Java FIPS app">
    Build the image from your Dockerfile:

    ```bash Build theme={null}
    docker build -t myapp-fips:latest .
    ```
  </Step>

  <Step title="Run Java FIPS app">
    Run the application:

    ```bash Run theme={null}
    docker run --rm -p 8080:8080 myapp-fips:latest
    ```
  </Step>
</Steps>

### Step 3: Verify your app

<Steps>
  <Step title="Verify FIPS provider loading">
    Save the following code as `TestFIPS.java`. We will run it against the image to confirm the CCJ and BCJSSE providers are loaded at the correct positions and that non-FIPS algorithms are blocked:

    ```java TestFIPS.java expandable theme={null}
    import java.security.Provider;
    import java.security.Security;

    public class TestFIPS {
        public static void main(String[] args) {
            System.out.println("=== FIPS Compliance Test ===");

            String approvedOnly = Security.getProperty("com.safelogic.cryptocomply.fips.approved_only");
            boolean isApprovedOnly = approvedOnly != null && approvedOnly.equals("true");

            if (!isApprovedOnly) {
                System.err.println("[ERROR] SafeLogic CryptoComply FIPS Approved Only Mode is disabled!");
                System.exit(1);
            }

            Provider[] providers = Security.getProviders();
            boolean foundCryptoComply = false;
            boolean foundBCJSSE = false;
            int cryptoComplyPosition = -1;
            int bcjssePosition = -1;

            for (int i = 0; i < providers.length; i++) {
                String name = providers[i].getName();
                if (name.contains("CCJ") || name.contains("CryptoComply")) {
                    foundCryptoComply = true;
                    cryptoComplyPosition = i + 1;
                    System.out.println("[OK] SafeLogic CryptoComply provider found at position " + cryptoComplyPosition);
                }
                if (name.contains("BouncyCastleJsse") || name.contains("BCJSSE")) {
                    foundBCJSSE = true;
                    bcjssePosition = i + 1;
                    System.out.println("[OK] Bouncy Castle JSSE provider found at position " + bcjssePosition);
                }
            }

            if (!foundCryptoComply) { System.err.println("[ERROR] CCJ provider NOT found!"); System.exit(1); }
            if (!foundBCJSSE) { System.err.println("[ERROR] BCJSSE provider NOT found!"); System.exit(1); }
            if (cryptoComplyPosition != 1) System.err.println("[WARNING] CCJ should be at position 1");
            if (bcjssePosition != 2) System.err.println("[WARNING] BCJSSE should be at position 2");

            try {
                javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding", "CCJ");
                System.out.println("[OK] AES algorithm available");
                java.security.MessageDigest.getInstance("SHA-256", "CCJ");
                System.out.println("[OK] SHA-256 algorithm available");
                java.security.KeyPairGenerator.getInstance("RSA", "CCJ");
                System.out.println("[OK] RSA algorithm available");

                try {
                    java.security.MessageDigest.getInstance("MD5", "CCJ");
                    System.err.println("[ERROR] MD5 is available — FIPS mode not fully enforced!");
                    System.exit(1);
                } catch (Exception e) {
                    System.out.println("[OK] MD5 correctly blocked: " + e.getMessage());
                }

                System.out.println("=== FIPS Compliance Test PASSED ===");
            } catch (Exception e) {
                System.err.println("[ERROR] FIPS algorithm test failed: " + e.getMessage());
                System.exit(1);
            }
        }
    }
    ```

    Compile and run:

    ```bash Run theme={null}
    docker run --rm -v $(pwd):/home/build reg.mini.dev/openjdk-fips:21 sh -c \
      "javac /home/build/TestFIPS.java -d /home/build && java -cp /home/build TestFIPS"
    ```

    Expected output:

    ```bash Expected theme={null}
    === FIPS Compliance Test ===
    [OK] SafeLogic CryptoComply provider found at position 1
    [OK] Bouncy Castle JSSE provider found at position 2
    [OK] AES algorithm available
    [OK] SHA-256 algorithm available
    [OK] RSA algorithm available
    [OK] MD5 correctly blocked: ...
    === FIPS Compliance Test PASSED ===
    ```

    If TLS connections return an empty cipher list or throw `NoSuchAlgorithmException`, the CCJ provider is not being registered. Check that `-Xbootclasspath/a:/usr/share/fips-libs/*` is present in your ENTRYPOINT.
  </Step>

  <Step title="Run smoke tests in the new image">
    Validate the following application paths:

    * Service startup
    * Health endpoints
    * TLS client and server connections
    * Authentication, token signing, and password hashing paths
    * Any code paths that invoke cryptographic operations directly

    For deep provider-level and algorithm-level validation, see [Java FIPS Validated Module](/compliance/java-fips).
  </Step>
</Steps>

### Step 4: Roll out to Kubernetes

<Steps>
  <Step title="Create a Minimus registry pull secret">
    Create a pull secret for the Minimus registry (first update the command with your Minimus token and the relevant namespace):

    ```bash Create theme={null}
    kubectl create secret docker-registry minimus-registry \
      --docker-server=reg.mini.dev \
      --docker-username=minimus \
      --docker-password={token} \
      -n my-namespace
    ```
  </Step>

  <Step title="Update your deployment YAML and deploy">
    Update your Deployment to reference `openjre-fips` as the runtime image and add the `imagePullSecrets`:

    <CodeGroup>
      ```yaml Deployment theme={null}
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: myapp
      spec:
        replicas: 2
        selector:
          matchLabels:
            app: myapp
        template:
          metadata:
            labels:
              app: myapp
          spec:
            imagePullSecrets:
              - name: minimus-registry
            containers:
              - name: myapp
                image: reg.mini.dev/openjre-fips:21
                ports:
                  - containerPort: 8080
      ```

      ```yaml Helm theme={null}
      image:
        repository: reg.mini.dev/openjre-fips
        tag: "21"
        pullPolicy: Always

      imagePullSecrets:
        - name: minimus-registry
      ```
    </CodeGroup>

    Apply the rollout and verify:

    ```bash Apply theme={null}
    kubectl apply -f deployment.yaml -n my-namespace
    kubectl rollout status deployment/myapp -n my-namespace
    kubectl logs deploy/myapp -n my-namespace --tail=200
    ```
  </Step>

  <Step title="Handle keystore requirements">
    FIPS mode restricts the keystore formats allowed for private key storage. For example, FIPS mode bans the standard `JKS` and `PKCS12` formats. The Bouncy Castle FIPS KeyStore format (`BCFKS`) is required instead.

    | **Use case**                       | **Allowed formats**         |
    | ---------------------------------- | --------------------------- |
    | Storing private keys               | `bcfks` only                |
    | Truststores (public CA certs only) | `jks`, `pkcs12`, or `bcfks` |

    <Info>
      If your team needs a complete keystore and certificate generation workflow — including `keytool` commands for BCFKS keystore and truststore creation, CA generation, and certificate signing — see the [Keycloak FIPS Tutorial](/advanced-guides/keycloak-fips). The `keytool` patterns shown there apply directly to any Java application using Minimus FIPS images, not just Keycloak.
    </Info>

    If your application currently loads a `.jks` or `.p12` file containing a private key at startup and passes it to an `SSLContext`, that will throw a `KeyStoreException` at runtime in FIPS mode. The keystore must be regenerated in `bcfks` format before deploying.

    <Warning>
      FIPS mode enforces a minimum password length of 14 characters (112 bits) for all BCFKS keystores and truststores. Passwords shorter than this will be rejected with the error `password must be at least 112 bits`. Use passwords of 16–24 characters.
    </Warning>
  </Step>
</Steps>

## Troubleshooting

| **Issue**                                | **Likely cause**                                             | **Action**                                                   |
| ---------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Empty cipher list at startup             | CCJ provider not registered — `java -jar` bypassed CLASSPATH | Add `-Xbootclasspath/a:/usr/share/fips-libs/*` to ENTRYPOINT |
| NoSuchAlgorithmException: RSA KeyFactory | FIPS provider not loaded                                     | Add `-Xbootclasspath/a:/usr/share/fips-libs/*` to ENTRYPOINT |
| Runtime algorithm errors                 | Legacy non-approved algorithm in use                         | Replace with FIPS-approved algorithms and rerun tests        |
| App works in build stage but not runtime | Build-only tools expected at runtime                         | Move compile steps to the `openjdk-fips` stage only          |
| Unexpected Java option behavior          | Default Java options overwritten                             | Extend `JDK_JAVA_OPTIONS` using `${JDK_JAVA_OPTIONS}`        |
| Keystore loading errors                  | Private-key store format mismatch                            | Convert private key keystores to BCFKS                       |
| Password rejected at keystore creation   | Password under 14 characters                                 | Use a password of 16–24 characters (minimum 112 bits)        |
| Cluster pull failures                    | Missing or invalid registry secret                           | Recreate pull secret and verify namespace binding            |

***
