Skip to main content

Build RHEL 9 STIG Images

Security Technical Implementation Guides (STIGs) are standardized security hardening guidelines developed by the Defense Information Systems Agency (DISA) to help secure OS and applications in regulated environments. With Palette Edge, you can build Red Hat Enterprise Linux (RHEL) 9 STIG-compliant images for your Edge hosts using the EdgeForge workflow.

In this guide, you will use the CanvOS utility to build a base RHEL 9 STIG image, then create an ISO image from it for your Palette Edge deployment.

Limitations​

  • The RHEL 9 STIG implementation in Palette Edge deviates from certain STIG compliance requirements due to platform design and Kubernetes operational needs:

    • Kairos-based systems use an A/B partitioning scheme for system recovery and upgrades. This does not align with STIG partitioning requirements.

    • Mount points are managed by Kairos and cannot be adjusted to fully match STIG guidelines.

    • Federal Information Processing Standards (FIPS) mode is enabled in Palette Edge via user-data configuration. Although the system operates in FIPS mode, it does not meet the criteria used by STIG validation checks, resulting in false negatives.

    • Kairos-based images do not include all STIG-recommended packages, for example, AIDE and USBGuard. These components are not required for Kubernetes operations, but you can add them by customizing the Dockerfile. However, only the modules included by default are verified by Spectro Cloud.

    • Security-Enhanced Linux (SELinux) is not enabled on Kairos-based systems.

    • IP forwarding (net.ipv4.ip_forward and net.ipv4.conf.all.forwarding) is enabled to support Kubernetes networking, which deviates from STIG requirements.

    • Reverse path filtering (rp_filter) is disabled to prevent interference with Kubernetes overlay networking.

  • The RHEL 9 STIG implementation in Palette Edge only supports the following Kubernetes distributions:

Prerequisites​

  • Valid Red Hat subscription credentials (username and password).

  • A Palette registration token for pairing Edge hosts with Palette. You need Tenant Admin access to Palette to generate a new registration token. For detailed instructions, refer to the Create Registration Token guide.

  • A physical or virtual Linux machine with an AMD64 (also known as x86_64) processor architecture and the following minimum hardware configuration:

    • 4 CPUs
    • 8 GB memory
    • 150 GB storage
  • A user account with permission to run commands using sudo privileges.

  • Access to a public or private image registry and permissions to push images. This page uses a public Docker Hub registry as an example. If you need to use a private registry, refer to the Deploy Cluster with a Private Provider Registry guide for instructions on how to configure the credentials.

  • The following software installed on the Linux machine:

    • Git

    • Docker Engine installed from the official Docker repository with BuildKit enabled. Default in Docker 23+; set DOCKER_BUILDKIT=1 for older versions.

      warning

      Do not use the snap-packaged Docker. The snap confinement causes GPG signature verification failures during the Earthly build.

    • (Optional, required for step 5) cmake, make, openscap-utils, openscap-scanner, python3, pip

Build RHEL 9 STIG-based Images​

  1. Check out the CanvOS GitHub repository, which contains the starter code.

    git clone https://github.com/spectrocloud/CanvOS.git
  2. Navigate to the CanvOS directory.

    cd CanvOS
  3. View the available git tags.

    git tag
  4. Check out the CanvOS tag that corresponds to your Palette release. Refer to the Edge Compatibility Matrix to identify the correct CanvOS, Stylus, and Edge host version.

    git checkout <newest-available-tag>
  5. (Optional) To ensure reproducible builds and consistent compliance behavior, you can pin a specific STIG content version before building the base RHEL 9 STIG image.

    warning

    Only the static STIG content included in the CanvOS repository is validated by Spectro Cloud. Using a different or newer STIG version is not validated and may result in issues during cluster creation or operation.

    bash rhel-stig/scripts/update-stig-content.sh <stig-content-version>
    How to check the latest STIG content version

    You can find the latest STIG content version via the GitHub API using the following command (requires jq).

    curl --silent https://api.github.com/repos/ComplianceAsCode/content/releases/latest | jq --raw-output .tag_name

    Verify that the command generates static remediation artifacts ssg-rhel9-ds.xml and stig-fix.sh in the static directory. These files are copied into the image during the build.

    ls rhel-stig/static
    Example Output
    VERSION  ssg-rhel9-ds.xml  stig-fix.sh

    If you skip this step, the build uses the STIG content available in the system repositories at build time and generates remediation dynamically. As a result, different STIG versions may be applied across builds.

  6. Build the base RHEL 9 STIG image. Replace <username> and <password> with your Red Hat credentials and <base-image-name> with the desired image name.

    bash rhel-stig/build.sh.rhel9 <username> <password> <base-image-name> false

    The build process takes some time to complete. You can monitor the progress in the terminal.

    Building RHEL 9 STIG image...
    Building 510.1s (41/41) FINISHED
  7. Confirm the RHEL 9 STIG image was built successfully. Replace <base-image-name> with the image name.

    docker images | grep <base-image-name>
    Example Output
    WARNING: This output is designed for human readability. For machine-readable output, please use --format.
    <base-image-name>:latest 29814a348637 1.9GB 0B
  8. After you have built the image, push it to a remote container registry so Earthly can access it. This guide uses Docker as an example. Issue the following command to log in to Docker Hub. Provide your Docker ID and password when prompted.

    docker login
    Login Succeeded

    Tag the image. Replace <registry> and <tag> with values appropriate for your environment.

    docker tag <base-image-name> <registry>/<base-image-name>:<tag>

    Push the image to the registry.

    docker push <registry>/<base-image-name>:<tag>
  9. Issue the command below to create an .arg file. Configure the RHEL OS (OS_DISTRIBUTION=rhel) and the AMD64 architecture (ARCH=amd64). Replace the placeholders with the desired values.

    cat << EOF > .arg
    IMAGE_REGISTRY=<image-registry>
    OS_DISTRIBUTION=rhel
    IMAGE_REPO=<image-repository>
    K8S_DISTRIBUTION=<k8s-distribution> # the supported values are `rke2` and `kubeadm`
    K8S_VERSION=<k8s-version>
    ARCH=amd64
    BASE_IMAGE=<base-image-name>
    IMAGE_NAME=<image-name>
    EOF

    Refer to Edge Artifact Build Configurations for a complete list of supported configuration parameters.

  10. Prepare the user-data file. Refer to Prepare User Data and Argument Files for instructions. Additionally, you must configure firewall rules. Expand the applicable sections below to display the list of required configurations.

    Kubernetes Core
    Port / RangeProtocolDirectionDescriptionNotes
    6443Transmission Control Protocol (TCP)InboundAPI server
    6444TCPInboundAlternative API server (RKE2 or K3s)
    9345TCPBothRKE2 supervisor APIRKE2-specific
    2379–2380TCPInboundetcd client and peer API
    2381TCPInboundetcd metrics endpoint
    10250TCPBothKubelet API
    10257TCPInboundkube-controller-manager (secure)
    10259TCPInboundkube-scheduler (secure)
    30000–32767TCP / User Datagram Protocol (UDP)InboundNodePort service range
    CNI: Cilium, Calico, Flannel, Weave
    Port / RangeProtocolDirectionDescriptionNotes
    8472UDPBothVirtual Extensible Local Area Network (VXLAN) overlayCilium, Flannel
    4789UDPBothVXLAN overlay (alternative port)Calico, Flannel
    179TCPBothBorder Gateway Protocol (BGP) peeringCalico, Cilium
    4240TCPBothCilium health checksCilium
    4244TCPBothHubble server (flow visibility)Cilium
    4245TCPBothHubble RelayCilium
    51871UDPBothWireGuard tunnelCilium or Flannel if enabled
    5473TCPBothCalico Typha proxyCalico
    9099TCPInboundFelix health checkCalico
    6783–6784TCP / UDPBothWeave Net control and data planeWeave
    12345UDPBothWeave Net gossip protocol (node communication)Weave
    Protocol 4IP in IP (IPIP)BothIPIP encapsulationCalico IPIP mode
    MetalLB
    Port / RangeProtocolDirectionDescriptionNotes
    7472TCPInboundSpeaker Prometheus metrics
    7473TCPInboundController Prometheus metrics
    7946TCP/UDPBothmemberlist (Layer 2 speaker coordination)
    179TCPBothBGP mode (upstream router)Shared with CNI BGP
    Longhorn
    Port / RangeProtocolDirectionDescriptionNotes
    9500TCPBothManager API and manager–replica communication
    9501TCPBothConversion Webhookv1.5.0+
    9502TCPBothAdmission Webhookv1.5.0+
    9503TCPBothRecovery Backendv1.5.0+
    9504TCPBothInstance Manager (data plane)
    9505TCPBothShare Manager (shared read/write access via a Network File System (NFS))
    9506TCPBothBacking Image Manager
    10000–12000TCPBothInstance manager pod replicationRequired per Longhorn documenttion
    8080TCPInboundLonghorn UIVia Ingress or port-forward
    Rook-Ceph
    Port / RangeProtocolDirectionDescription
    6789TCPBothCeph Monitor (Messenger v1)
    3300TCPBothCeph Monitor (Messenger v2)
    6800–7300TCPBothObject Storage Daemon (OSD) ports (up to 3 per OSD)
    7480TCPInboundReliable Autonomic Distributed Object Store (RADOS) Gateway (RGW) object storage endpoint over Hypertext Transfer Protocol (HTTP)
    9283TCPInboundCeph Manager Prometheus metrics
    8443TCPInboundCeph dashboard over Hypertext Transfer Protocol Secure (HTTPS)
    Prometheus, Grafana, Alertmanager
    Port / RangeProtocolDirectionDescription
    9090TCPInboundPrometheus server UI and API
    9091TCPInboundPushgateway
    9093TCPInboundAlertmanager HTTP endpoint
    9094TCP/UDPBothAlertmanager cluster high availability (HA) mesh
    9100TCPInboundNode Exporter metrics endpoint
    3000TCPInboundGrafana UI
    6060TCPInboundProfiling endpoint (pprof)
    Nginx Ingress
    Port / RangeProtocolDirectionDescription
    80TCPInboundHTTP ingress
    443TCPInboundHTTPS ingress
    8443TCPInboundAdmission webhook
    10254TCPInboundHealth check and metrics endpoint
    Vault
    Port / RangeProtocolDirectionDescriptionNotes
    8200TCPInboundVault API and UI
    8201TCPBothVault cluster communication (Raft and HA)
    8202TCPBothVault replication (disaster recovery (DR) and performance)Enterprise
    Misc, System
    Port / RangeProtocolDirectionDescriptionNotes
    22TCPInboundSecure Shell (SSH)
    5080TCPInboundLocal UI
    8500TCPInboundConsul HTTP APIIf used
    5355UDPBothLink-Local Multicast Name Resolution (LLMNR) and Multicast Domain Name System (mDNS) (systemd-resolved)Can usually be restricted
    Protocol 4IPIPBothIP-in-IP encapsulationCalico IPIP mode

    The following example shows how to configure firewalld in user-data to create a dedicated k8s zone and open the ports and protocols required for Kubernetes and commonly used add-ons.

    Example
    #cloud-config
    stylus:
    site:
    paletteEndpoint: api.spectrocloud.com
    edgeHostToken: <your-registration-token>

    install:
    poweroff: true

    stages:
    initramfs:
    - name: Create user and assign to sudo group
    users:
    kairos:
    groups:
    - sudo
    passwd: kairos

    boot:
    - name: configure firewalld baseline for k8s
    commands:
    - |-
    firewall-cmd --set-default-zone=k8s || true
    firewall-cmd --reload || true

    network.after:
    - name: configure firewalld for k8s
    commands:
    - |-
    firewall-cmd --permanent --new-zone=k8s || true
    firewall-cmd --reload
    firewall-cmd --permanent --zone=k8s --add-service=ssh
    firewall-cmd --permanent --zone=k8s --add-forward
    firewall-cmd --permanent --zone=k8s --add-masquerade
    firewall-cmd --permanent --zone=k8s --add-source=192.168.0.0/16
    firewall-cmd --zone=k8s --add-source=10.10.0.0/16 --permanent
    firewall-cmd --permanent --zone=k8s --add-port=6443/tcp
    firewall-cmd --permanent --zone=k8s --add-port=4443/tcp
    firewall-cmd --permanent --zone=k8s --add-port=6444/tcp
    firewall-cmd --permanent --zone=k8s --add-port=7472/tcp
    firewall-cmd --permanent --zone=k8s --add-port=7473/tcp
    firewall-cmd --permanent --zone=k8s --add-port=2379-2380/tcp
    firewall-cmd --permanent --zone=k8s --add-port=10250/tcp
    firewall-cmd --permanent --zone=k8s --add-port=10259/tcp
    firewall-cmd --permanent --zone=k8s --add-port=10257/tcp
    firewall-cmd --permanent --zone=k8s --add-port=30000-32767/tcp
    firewall-cmd --permanent --zone=k8s --add-port=30000-32767/udp
    firewall-cmd --permanent --zone=k8s --add-port=2381/tcp
    firewall-cmd --permanent --zone=k8s --add-port=7946/udp
    firewall-cmd --permanent --zone=k8s --add-port=9100/tcp
    firewall-cmd --permanent --zone=k8s --add-port=8472/udp
    firewall-cmd --permanent --zone=k8s --add-port=5355/udp
    firewall-cmd --permanent --zone=k8s --add-port=12345/udp
    firewall-cmd --permanent --zone=k8s --add-port=4789/udp
    firewall-cmd --permanent --zone=k8s --add-port=179/tcp
    firewall-cmd --permanent --zone=k8s --add-port=4240/tcp
    firewall-cmd --permanent --zone=k8s --add-port=4244-4245/tcp
    firewall-cmd --permanent --zone=k8s --add-port=51871/udp
    firewall-cmd --permanent --zone=k8s --add-port=5473/tcp
    firewall-cmd --permanent --zone=k8s --add-port=6783-6784/tcp
    firewall-cmd --permanent --zone=k8s --add-port=6783-6784/udp
    firewall-cmd --permanent --zone=k8s --add-port=9500-9506/tcp
    firewall-cmd --permanent --zone=k8s --add-port=10000-12000/tcp
    firewall-cmd --permanent --zone=k8s --add-port=443/tcp
    firewall-cmd --permanent --zone=k8s --add-port=6789/tcp
    firewall-cmd --permanent --zone=k8s --add-port=3300/tcp
    firewall-cmd --permanent --zone=k8s --add-port=6800-7300/tcp
    firewall-cmd --permanent --zone=k8s --add-port=7480/tcp
    firewall-cmd --permanent --zone=k8s --add-port=9283/tcp
    firewall-cmd --permanent --zone=k8s --add-port=8443/tcp
    firewall-cmd --permanent --zone=k8s --add-port=8500/tcp
    firewall-cmd --permanent --zone=k8s --add-port=9345/tcp
    firewall-cmd --permanent --zone=k8s --add-port=7946/tcp
    firewall-cmd --permanent --zone=k8s --add-protocol=4
    firewall-cmd --permanent --zone=k8s --add-port=3000/tcp
    firewall-cmd --permanent --zone=k8s --add-port=6060/tcp
    firewall-cmd --permanent --zone=k8s --add-port=9090/tcp
    firewall-cmd --permanent --zone=k8s --add-port=9091/tcp
    firewall-cmd --permanent --zone=k8s --add-port=8080/tcp
    firewall-cmd --permanent --zone=k8s --add-port=9094/tcp
    firewall-cmd --permanent --zone=k8s --add-port=9094/udp
    firewall-cmd --permanent --zone=k8s --add-port=9093/tcp
    firewall-cmd --permanent --zone=k8s --add-port=9093/udp
    firewall-cmd --permanent --zone=k8s --add-port=80/tcp
    firewall-cmd --permanent --zone=k8s --add-port=10254/tcp
    firewall-cmd --permanent --zone=k8s --add-port=8200-8202/tcp
    firewall-cmd --permanent --zone=k8s --add-port=5080/tcp
    firewall-cmd --zone=k8s --add-source=100.64.192.0/23 --permanent
    firewall-cmd --zone=k8s --add-rich-rule='rule family="ipv4" source address="0.0.0.0/0" destination address="255.255.255.255" protocol value="udp" accept' --permanent

    IFACE="$(ip --oneline route show default | awk '{print $5; exit}')"
    if [ -n "$IFACE" ]; then
    firewall-cmd --zone=public --remove-interface="$IFACE" || true
    firewall-cmd --zone=k8s --change-interface="$IFACE"
    fi

    firewall-cmd --set-default-zone=k8s
    firewall-cmd --reload
    warning

    Configure the firewall through user-data, as some rules are required during overlay cluster creation. If you add them later, such as in a cluster profile, overlay clusters may fail to come up.

    The provided firewall configuration is an example. You may not need all listed ports. Add, remove, or modify rules based on your cluster profile, enabled packs, and application requirements. Refer to the official documentation for those components to determine the required network settings.

  11. (Optional) To enable FIPS, add the following to your user-data cloud-config to set the required kernel boot option.

    #cloud-config
    install:
    grub_options:
    extra_cmdline: "fips=1 selinux=0"
  12. Once the user-data file is ready, issue the following command to build the ISO image.

    sudo ./earthly.sh iso

    The build process takes some time to finish.

    # Output condensed for readability
    ===================== Earthly Build SUCCESS =====================
    Share your logs with an Earthly account (experimental)! Register for one at https://ci.earthly.dev.

    You can find the ISO image in the build folder.

Validate​

You can validate that the ISO image has not been corrupted by attempting to flash a bootable device. Most software that creates a bootable device will validate the ISO image before the flash process.