1
0
Эх сурвалжийг харах

[8.x] [Build] Add FIPS docker image for GovCloud (#117152) (#125684)

* [Build] Add FIPS docker image for GovCloud (#117152)

- Adds docker image based on chainguard base fips image
- x86 only for now as the base image is x86 only
- the image does not provide any elasticsearch.yml configuration. for testing purposes you can follow the elasticsearch fips guide available at https://github.com/elastic/FIPSGuide/tree/main/elasticsearch

The image is shipped with:
- org.bouncycastle:bc-fips:1.0.2.5 and org.bouncycastle:bctls-fips:1.0.19 in Elasticsearch libs folder
- config/jvm.options.d/fips.options for fips specific JVM options
- fips_java.security file
- fips_java.policy

Out of scope:
- Add packaging test coverage (part of later PR as we want to provide that image for testing early and packaging tests require more general restructuring for support fips scenarios)

(cherry picked from commit 653c179b08b19a46a3e2d052d4f887beab759978)

# Conflicts:
#	build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/distribution/InternalElasticsearchDistributionTypes.java
#	distribution/docker/build.gradle
#	distribution/docker/src/docker/Dockerfile

* Fix merge conflict while back porting

* Fix another merge conflict

* Fix fips tests

reported as broken due to issue in gradle setup
Rene Groeschke 6 сар өмнө
parent
commit
ce367e9498

+ 2 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/DockerBase.java

@@ -28,6 +28,8 @@ public enum DockerBase {
         "-wolfi",
         "apk"
     ),
+
+    FIPS("docker.elastic.co/wolfi/chainguard-base-fips:sha256-feb7aeb1bbcb331afa089388f2fa1e81997fc24642ca4fa06b7e502ff599a4cf", "-fips", "apk"),
     // spotless:on
 
     // Based on WOLFI above, with more extras. We don't set a base image because

+ 27 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/distribution/DockerFipsElasticsearchDistributionType.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the "Elastic License
+ * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+package org.elasticsearch.gradle.internal.distribution;
+
+import org.elasticsearch.gradle.ElasticsearchDistributionType;
+
+public class DockerFipsElasticsearchDistributionType implements ElasticsearchDistributionType {
+
+    DockerFipsElasticsearchDistributionType() {}
+
+    @Override
+    public String getName() {
+        return "dockerFips";
+    }
+
+    @Override
+    public boolean isDocker() {
+        return true;
+    }
+}

+ 10 - 8
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/distribution/InternalElasticsearchDistributionTypes.java

@@ -14,13 +14,14 @@ import org.elasticsearch.gradle.ElasticsearchDistributionType;
 import java.util.List;
 
 public class InternalElasticsearchDistributionTypes {
-    public static ElasticsearchDistributionType DEB = new DebElasticsearchDistributionType();
-    public static ElasticsearchDistributionType RPM = new RpmElasticsearchDistributionType();
-    public static ElasticsearchDistributionType DOCKER = new DockerElasticsearchDistributionType();
-    public static ElasticsearchDistributionType DOCKER_UBI = new DockerUbiElasticsearchDistributionType();
-    public static ElasticsearchDistributionType DOCKER_IRONBANK = new DockerIronBankElasticsearchDistributionType();
-    public static ElasticsearchDistributionType DOCKER_CLOUD_ESS = new DockerCloudEssElasticsearchDistributionType();
-    public static ElasticsearchDistributionType DOCKER_WOLFI = new DockerWolfiElasticsearchDistributionType();
+    public static final ElasticsearchDistributionType DEB = new DebElasticsearchDistributionType();
+    public static final ElasticsearchDistributionType RPM = new RpmElasticsearchDistributionType();
+    public static final ElasticsearchDistributionType DOCKER = new DockerElasticsearchDistributionType();
+    public static final ElasticsearchDistributionType DOCKER_UBI = new DockerUbiElasticsearchDistributionType();
+    public static final ElasticsearchDistributionType DOCKER_IRONBANK = new DockerIronBankElasticsearchDistributionType();
+    public static final ElasticsearchDistributionType DOCKER_CLOUD_ESS = new DockerCloudEssElasticsearchDistributionType();
+    public static final ElasticsearchDistributionType DOCKER_WOLFI = new DockerWolfiElasticsearchDistributionType();
+    public static final ElasticsearchDistributionType DOCKER_FIPS = new DockerFipsElasticsearchDistributionType();
 
     public static List<ElasticsearchDistributionType> ALL_INTERNAL = List.of(
         DEB,
@@ -29,6 +30,7 @@ public class InternalElasticsearchDistributionTypes {
         DOCKER_UBI,
         DOCKER_IRONBANK,
         DOCKER_CLOUD_ESS,
-        DOCKER_WOLFI
+        DOCKER_WOLFI,
+        DOCKER_FIPS
     );
 }

+ 2 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/test/DistroTestPlugin.java

@@ -51,6 +51,7 @@ import static org.elasticsearch.gradle.internal.distribution.InternalElasticsear
 import static org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes.DEB;
 import static org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes.DOCKER;
 import static org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes.DOCKER_CLOUD_ESS;
+import static org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes.DOCKER_FIPS;
 import static org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes.DOCKER_IRONBANK;
 import static org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes.DOCKER_UBI;
 import static org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes.DOCKER_WOLFI;
@@ -153,6 +154,7 @@ public class DistroTestPlugin implements Plugin<Project> {
         lifecyleTasks.put(DOCKER_IRONBANK, project.getTasks().register(taskPrefix + ".docker-ironbank"));
         lifecyleTasks.put(DOCKER_CLOUD_ESS, project.getTasks().register(taskPrefix + ".docker-cloud-ess"));
         lifecyleTasks.put(DOCKER_WOLFI, project.getTasks().register(taskPrefix + ".docker-wolfi"));
+        lifecyleTasks.put(DOCKER_FIPS, project.getTasks().register(taskPrefix + ".docker-fips"));
         lifecyleTasks.put(ARCHIVE, project.getTasks().register(taskPrefix + ".archives"));
         lifecyleTasks.put(DEB, project.getTasks().register(taskPrefix + ".packages"));
         lifecyleTasks.put(RPM, lifecyleTasks.get(DEB));

+ 56 - 12
distribution/docker/build.gradle

@@ -2,6 +2,7 @@ import org.elasticsearch.gradle.LoggedExec
 import org.elasticsearch.gradle.VersionProperties
 import org.elasticsearch.gradle.internal.DockerBase
 import org.elasticsearch.gradle.internal.distribution.InternalElasticsearchDistributionTypes
+import org.elasticsearch.gradle.internal.ExportElasticsearchBuildResourcesTask
 import org.elasticsearch.gradle.internal.docker.DockerBuildTask
 import org.elasticsearch.gradle.internal.docker.DockerSupportPlugin
 import org.elasticsearch.gradle.internal.docker.DockerSupportService
@@ -17,6 +18,8 @@ apply plugin: 'elasticsearch.legacy-yaml-rest-test'
 apply plugin: 'elasticsearch.test.fixtures'
 apply plugin: 'elasticsearch.internal-distribution-download'
 apply plugin: 'elasticsearch.dra-artifacts'
+apply plugin: 'elasticsearch.jdk-download'
+apply plugin: 'elasticsearch.repositories'
 
 String buildId = providers.systemProperty('build.id').getOrNull()
 boolean useLocalArtifacts = buildId != null && buildId.isBlank() == false && useDra == false
@@ -93,6 +96,7 @@ configurations {
   filebeat_x86_64
   metricbeat_aarch64
   metricbeat_x86_64
+  fips
 }
 
 String tiniArch = Architecture.current() == Architecture.AARCH64 ? 'arm64' : 'amd64'
@@ -109,6 +113,8 @@ dependencies {
   filebeat_x86_64 "beats:filebeat:${VersionProperties.elasticsearch}:linux-x86_64@tar.gz"
   metricbeat_aarch64 "beats:metricbeat:${VersionProperties.elasticsearch}:linux-arm64@tar.gz"
   metricbeat_x86_64 "beats:metricbeat:${VersionProperties.elasticsearch}:linux-x86_64@tar.gz"
+  fips "org.bouncycastle:bc-fips:1.0.2.5"
+  fips "org.bouncycastle:bctls-fips:1.0.19"
 }
 
 ext.expansions = { Architecture architecture, DockerBase base ->
@@ -286,6 +292,34 @@ void addBuildDockerContextTask(Architecture architecture, DockerBase base) {
           filter TransformLog4jConfigFilter
         }
       }
+      if(base == DockerBase.FIPS) {
+
+        // If we're performing a release build, but `build.id` hasn't been set, we can
+        // infer that we're not at the Docker building stage of the build, and therefore
+        // we should skip the beats part of the build.
+        String buildId = providers.systemProperty('build.id').getOrNull()
+        boolean includeBeats = VersionProperties.isElasticsearchSnapshot() == true || buildId != null || useDra
+
+        if (includeBeats) {
+          from configurations.getByName("filebeat_${architecture.classifier}")
+          from configurations.getByName("metricbeat_${architecture.classifier}")
+          // For some reason, the artifact name can differ depending on what repository we used.
+          rename ~/((?:file|metric)beat)-.*\.tar\.gz$/, "\$1-${VersionProperties.elasticsearch}.tar.gz"
+        }
+
+        into("plugins") {
+          from configurations.allPlugins
+        }
+
+        into("fips") {
+          into("libs") {
+            from configurations.fips
+          }
+          into("resources") {
+            from tasks.named('fipsDockerResources')
+          }
+        }
+      }
 
       Provider<DockerSupportService> serviceProvider = GradleUtils.getBuildService(
         project.gradle.sharedServices,
@@ -431,7 +465,7 @@ void addBuildDockerImageTask(Architecture architecture, DockerBase base) {
   }
 }
 
-void addBuildEssDockerImageTask(Architecture architecture) {
+void addBuildCloudDockerImageTasks(Architecture architecture) {
   DockerBase dockerBase = DockerBase.CLOUD_ESS
   String arch = architecture == Architecture.AARCH64 ? '-aarch64' : ''
   String contextDir = "${project.buildDir}/docker-context/elasticsearch${dockerBase.suffix}-${VersionProperties.elasticsearch}-docker-build-context${arch}"
@@ -463,10 +497,10 @@ void addBuildEssDockerImageTask(Architecture architecture) {
       from(projectDir.resolve("src/docker/Dockerfile.ess")) {
         expand(
           [
-            base_image: "elasticsearch${baseSuffix}:${architecture.classifier}",
+            base_image : "elasticsearch${baseSuffix}:${architecture.classifier}",
             docker_base: "${dockerBase.name().toLowerCase()}",
-            version: "${VersionProperties.elasticsearch}",
-            retry: ShellRetry
+            version    : "${VersionProperties.elasticsearch}",
+            retry      : ShellRetry
           ]
         )
         filter SquashNewlinesFilter
@@ -501,17 +535,24 @@ void addBuildEssDockerImageTask(Architecture architecture) {
   }
 }
 
+// fips
+TaskProvider<ExportElasticsearchBuildResourcesTask> fipsResourcesTask = tasks.register('fipsDockerResources', ExportElasticsearchBuildResourcesTask)
+fipsResourcesTask.configure {
+  outputDir = project.layout.buildDirectory.dir('fips-docker-resources').get().asFile
+  copy 'fips_java.security'
+  copy 'fips_java.policy'
+}
+
 for (final Architecture architecture : Architecture.values()) {
   for (final DockerBase base : DockerBase.values()) {
     if (base == DockerBase.CLOUD_ESS) {
-      continue
+      addBuildCloudDockerImageTasks(architecture)
+    } else {
+      addBuildDockerContextTask(architecture, base)
+      addTransformDockerContextTask(architecture, base)
+      addBuildDockerImageTask(architecture, base)
     }
-    addBuildDockerContextTask(architecture, base)
-    addTransformDockerContextTask(architecture, base)
-    addBuildDockerImageTask(architecture, base)
   }
-
-  addBuildEssDockerImageTask(architecture)
 }
 
 def exportDockerImages = tasks.register("exportDockerImages")
@@ -535,14 +576,17 @@ subprojects { Project subProject ->
       base = DockerBase.CLOUD_ESS
     } else if (subProject.name.contains('wolfi-')) {
       base = DockerBase.WOLFI
+    } else if (subProject.name.contains('fips-')) {
+      base = DockerBase.FIPS
     }
 
     final String arch = architecture == Architecture.AARCH64 ? '-aarch64' : ''
     final String extension = base == DockerBase.UBI ? 'ubi.tar' :
       (base == DockerBase.IRON_BANK ? 'ironbank.tar' :
-          (base == DockerBase.CLOUD_ESS ? 'cloud-ess.tar' :
+        (base == DockerBase.CLOUD_ESS ? 'cloud-ess.tar' :
+          (base == DockerBase.FIPS ? 'fips.tar' :
             (base == DockerBase.WOLFI ? 'wolfi.tar' :
-                'docker.tar')))
+              'docker.tar'))))
     final String artifactName = "elasticsearch${arch}${base.suffix}_test"
 
     final String exportTaskName = taskName("export", architecture, base, 'DockerImage')

+ 0 - 0
distribution/docker/fips-docker-export/build.gradle


+ 56 - 40
distribution/docker/src/docker/Dockerfile

@@ -41,15 +41,15 @@ RUN chmod 0555 /bin/tini
 <% } else { %>
 
 # Install required packages to extract the Elasticsearch distribution
-<% if (docker_base == 'default' || docker_base == 'cloud') { %>
+<% if (docker_base == 'default') { %>
 RUN <%= retry.loop(package_manager, "${package_manager} update && DEBIAN_FRONTEND=noninteractive ${package_manager} install -y curl ") %>
-<% } else if (docker_base == "wolfi") { %>
+<% } else if (docker_base == "wolfi" || docker_base == "fips") { %>
 RUN <%= retry.loop(package_manager, "export DEBIAN_FRONTEND=noninteractive && ${package_manager} update && ${package_manager} update && ${package_manager} add --no-cache curl") %>
 <% } else { %>
 RUN <%= retry.loop(package_manager, "${package_manager} install -y findutils tar gzip") %>
 <% } %>
 
-<% if (docker_base != 'wolfi') { %>
+<% if (docker_base != 'wolfi' && docker_base != 'fips' ) { %>
     # `tini` is a tiny but valid init for containers. This is used to cleanly
     # control how ES and any child processes are shut down.
     # For wolfi we pick it from the blessed wolfi package registry.
@@ -117,25 +117,46 @@ RUN sed -i -e 's/ES_DISTRIBUTION_TYPE=tar/ES_DISTRIBUTION_TYPE=docker/' bin/elas
     chmod 0775 bin config config/jvm.options.d data logs plugins && \\
     find config -type f -exec chmod 0664 {} +
 
-<% if (docker_base == "cloud") { %>
-COPY filebeat-${version}.tar.gz metricbeat-${version}.tar.gz /tmp/
-RUN set -eux ; \\
-    for beat in filebeat metricbeat ; do \\
-      if [ ! -s /tmp/\$beat-${version}.tar.gz ]; then \\
-        echo "/tmp/\$beat-${version}.tar.gz is empty - cannot uncompress" 2>&1 ; \\
-        exit 1 ; \\
-      fi ; \\
-      if ! tar tf /tmp/\$beat-${version}.tar.gz >/dev/null; then \\
-        echo "/tmp/\$beat-${version}.tar.gz is corrupt - cannot uncompress" 2>&1 ; \\
-        exit 1 ; \\
-      fi ; \\
-      mkdir -p /opt/\$beat ; \\
-      tar xf /tmp/\$beat-${version}.tar.gz -C /opt/\$beat --strip-components=1 ; \\
-    done
-
-# Add plugins infrastructure
-RUN mkdir -p /opt/plugins/archive
-RUN chmod -R 0555 /opt/plugins
+<% if (docker_base == "fips") { %>
+    # Add plugins infrastructure
+    RUN mkdir -p /opt/plugins/archive
+    RUN chmod -R 0555 /opt/plugins
+
+    RUN mkdir -p /fips/libs
+    COPY fips/libs/*.jar /fips/libs/
+
+    COPY filebeat-${version}.tar.gz metricbeat-${version}.tar.gz /tmp/
+    RUN set -eux ; \\
+        for beat in filebeat metricbeat ; do \\
+          if [ ! -s /tmp/\$beat-${version}.tar.gz ]; then \\
+            echo "/tmp/\$beat-${version}.tar.gz is empty - cannot uncompress" 2>&1 ; \\
+            exit 1 ; \\
+          fi ; \\
+          if ! tar tf /tmp/\$beat-${version}.tar.gz >/dev/null; then \\
+            echo "/tmp/\$beat-${version}.tar.gz is corrupt - cannot uncompress" 2>&1 ; \\
+            exit 1 ; \\
+          fi ; \\
+          mkdir -p /opt/\$beat ; \\
+          tar xf /tmp/\$beat-${version}.tar.gz -C /opt/\$beat --strip-components=1 ; \\
+        done
+
+    COPY plugins/*.zip /opt/plugins/archive/
+
+    RUN chown 1000:1000 /opt/plugins/archive/*
+    RUN chmod 0444 /opt/plugins/archive/*
+
+    COPY fips/resources/fips_java.security /usr/share/elasticsearch/config/fips_java.security
+    COPY fips/resources/fips_java.policy /usr/share/elasticsearch/config/fips_java.policy
+
+    WORKDIR /usr/share/elasticsearch/config
+
+    ## Add fips specific JVM options
+    RUN cat <<EOF > /usr/share/elasticsearch/config/jvm.options.d/fips.options
+    -Djavax.net.ssl.keyStoreType=BCFKS
+    -Dorg.bouncycastle.fips.approved_only=true
+    -Djava.security.properties=config/fips_java.security
+    -Djava.security.policy=config/fips_java.policy
+    EOF
 <% } %>
 
 ################################################################################
@@ -157,7 +178,7 @@ RUN ${package_manager} update --setopt=tsflags=nodocs -y && \\
       nc shadow-utils zip findutils unzip procps-ng && \\
     ${package_manager} clean all
 
-<% } else if (docker_base == "wolfi") { %>
+<% } else if (docker_base == "wolfi" || docker_base == "fips") { %>
 RUN <%= retry.loop(package_manager,
           "export DEBIAN_FRONTEND=noninteractive && \n" +
           "      ${package_manager} update && \n" +
@@ -201,20 +222,16 @@ RUN <%= retry.loop(
 <% } %>
 
 
-<% if (docker_base == "default" || docker_base == "cloud") { %>
+<% if (docker_base == "default") { %>
 RUN groupadd -g 1000 elasticsearch && \\
     adduser --uid 1000 --gid 1000 --home /usr/share/elasticsearch elasticsearch && \\
     adduser elasticsearch root && \\
     chown -R 0:0 /usr/share/elasticsearch
-<% } else if (docker_base == "wolfi") { %>
+<% } else if (docker_base == "wolfi" || docker_base == "fips") { %>
 RUN groupadd -g 1000 elasticsearch && \
     adduser -G elasticsearch -u 1000 elasticsearch -D --home /usr/share/elasticsearch elasticsearch && \
     adduser elasticsearch root && \
     chown -R 0:0 /usr/share/elasticsearch
-<% } else { %>
-RUN groupadd -g 1000 elasticsearch && \\
-    adduser -u 1000 -g 1000 -G 0 -d /usr/share/elasticsearch elasticsearch && \\
-    chown -R 0:0 /usr/share/elasticsearch
 <% } %>
 
 ENV ELASTIC_CONTAINER true
@@ -222,7 +239,7 @@ ENV ELASTIC_CONTAINER true
 WORKDIR /usr/share/elasticsearch
 
 COPY --from=builder --chown=0:0 /usr/share/elasticsearch /usr/share/elasticsearch
-<% if (docker_base != "wolfi") { %>
+<% if (docker_base != "wolfi" && docker_base != "fips") { %>
 COPY --from=builder --chown=0:0 /bin/tini /bin/tini
 <% } %>
 
@@ -251,12 +268,12 @@ RUN chmod g=u /etc/passwd && \\
     chmod 0775 /usr/share/elasticsearch && \\
     chown elasticsearch bin config config/jvm.options.d data logs plugins
 
-<% if (docker_base == 'default' || docker_base == 'cloud') { %>
+<% if (docker_base == 'default') { %>
 # Update "cacerts" bundle to use Ubuntu's CA certificates (and make sure it
 # stays up-to-date with changes to Ubuntu's store)
 COPY bin/docker-openjdk /etc/ca-certificates/update.d/docker-openjdk
 RUN /etc/ca-certificates/update.d/docker-openjdk
-<% } else if (docker_base == 'wolfi') { %>
+<% } else if (docker_base == 'wolfi' || docker_base == "fips") { %>
 RUN ln -sf /etc/ssl/certs/java/cacerts /usr/share/elasticsearch/jdk/lib/security/cacerts
 <% } else { %>
 RUN ln -sf /etc/pki/ca-trust/extracted/java/cacerts /usr/share/elasticsearch/jdk/lib/security/cacerts
@@ -303,14 +320,7 @@ RUN mkdir /licenses && cp LICENSE.txt /licenses/LICENSE
 COPY LICENSE /licenses/LICENSE.addendum
 <% } %>
 
-<% if (docker_base == "cloud") { %>
-ENTRYPOINT ["/bin/tini", "--"]
-CMD ["/app/elasticsearch.sh"]
-# Generate a stub command that will be overwritten at runtime
-RUN mkdir /app && \\
-    echo -e '#!/bin/bash\\nexec /usr/local/bin/docker-entrypoint.sh eswrapper' > /app/elasticsearch.sh && \\
-    chmod 0555 /app/elasticsearch.sh
-<% } else if (docker_base == "wolfi") { %>
+<% if (docker_base == "wolfi" || docker_base == "fips") { %>
 # Our actual entrypoint is `tini`, a minimal but functional init program. It
 # calls the entrypoint we provide, while correctly forwarding signals.
 ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/docker-entrypoint.sh"]
@@ -330,6 +340,12 @@ USER 1000:0
 HEALTHCHECK --interval=10s --timeout=5s --start-period=1m --retries=5 CMD curl -I -f --max-time 5 http://localhost:9200 || exit 1
 <% } %>
 
+<% if (docker_base == 'fips') { %>
+COPY --from=builder --chown=0:0 /opt /opt
+ENV ES_PLUGIN_ARCHIVE_DIR /opt/plugins/archive
+WORKDIR /usr/share/elasticsearch
+COPY --from=builder --chown=0:0 /fips/libs/*.jar /usr/share/elasticsearch/lib/
+<% } %>
 ################################################################################
 # End of multi-stage Dockerfile
 ################################################################################

+ 1 - 0
settings.gradle

@@ -72,6 +72,7 @@ List projects = [
   'distribution:docker:ubi-docker-export',
   'distribution:docker:wolfi-docker-aarch64-export',
   'distribution:docker:wolfi-docker-export',
+  'distribution:docker:fips-docker-export',
   'distribution:packages:aarch64-deb',
   'distribution:packages:deb',
   'distribution:packages:aarch64-rpm',