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-dataconfiguration. 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_forwardandnet.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:
-
Palette eXtended Kubernetes Edge (PXK-E) - The
K8S_DISTRIBUTION=kubeadmvalue in the.argfile for non-FIPS orK8S_DISTRIBUTION=kubeadm-fipsfor FIPS. -
Palette Optimized RKE2 - The
K8S_DISTRIBUTION=rke2value in the.argfile.
-
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
sudoprivileges. -
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:
-
Docker Engine installed from the official Docker repository with BuildKit enabled. Default in Docker 23+; set
DOCKER_BUILDKIT=1for older versions.warningDo 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
-
Check out the CanvOS GitHub repository, which contains the starter code.
git clone https://github.com/spectrocloud/CanvOS.git -
Navigate to the
CanvOSdirectory.cd CanvOS -
View the available git tags.
git tag -
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> -
(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.
warningOnly 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_nameVerify that the command generates static remediation artifacts
ssg-rhel9-ds.xmlandstig-fix.shin thestaticdirectory. These files are copied into the image during the build.ls rhel-stig/staticExample OutputVERSION ssg-rhel9-ds.xml stig-fix.shIf 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.
-
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.- Non-FIPS
- FIPS
bash rhel-stig/build.sh.rhel9 <username> <password> <base-image-name> falsebash rhel-stig/build.sh.rhel9 <username> <password> <base-image-name> trueThe 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 -
Confirm the RHEL 9 STIG image was built successfully. Replace
<base-image-name>with the image name.docker images | grep <base-image-name>Example OutputWARNING: This output is designed for human readability. For machine-readable output, please use --format.
<base-image-name>:latest 29814a348637 1.9GB 0B -
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 loginLogin SucceededTag 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> -
Issue the command below to create an
.argfile. Configure the RHEL OS (OS_DISTRIBUTION=rhel) and the AMD64 architecture (ARCH=amd64). Replace the placeholders with the desired values.- Non-FIPS
- FIPS
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>
EOFcat << EOF > .arg
IMAGE_REGISTRY=<image-registry>
OS_DISTRIBUTION=rhel
IMAGE_REPO=<image-repository>
K8S_DISTRIBUTION=<k8s-distribution> # the supported values are `rke2` and `kubeadm-fips`
K8S_VERSION=<k8s-version>
FIPS_ENABLED=true
ARCH=amd64
BASE_IMAGE=<base-image-name>
IMAGE_NAME=<image-name>
EOFRefer to Edge Artifact Build Configurations for a complete list of supported configuration parameters.
-
Prepare the
user-datafile. 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 / Range Protocol Direction Description Notes 6443 Transmission Control Protocol (TCP) Inbound API server 6444 TCP Inbound Alternative API server (RKE2 or K3s) 9345 TCP Both RKE2 supervisor API RKE2-specific 2379–2380 TCP Inbound etcd client and peer API 2381 TCP Inbound etcd metrics endpoint 10250 TCP Both Kubelet API 10257 TCP Inbound kube-controller-manager (secure) 10259 TCP Inbound kube-scheduler (secure) 30000–32767 TCP / User Datagram Protocol (UDP) Inbound NodePort service range CNI: Cilium, Calico, Flannel, Weave
Port / Range Protocol Direction Description Notes 8472 UDP Both Virtual Extensible Local Area Network (VXLAN) overlay Cilium, Flannel 4789 UDP Both VXLAN overlay (alternative port) Calico, Flannel 179 TCP Both Border Gateway Protocol (BGP) peering Calico, Cilium 4240 TCP Both Cilium health checks Cilium 4244 TCP Both Hubble server (flow visibility) Cilium 4245 TCP Both Hubble Relay Cilium 51871 UDP Both WireGuard tunnel Cilium or Flannel if enabled 5473 TCP Both Calico Typha proxy Calico 9099 TCP Inbound Felix health check Calico 6783–6784 TCP / UDP Both Weave Net control and data plane Weave 12345 UDP Both Weave Net gossip protocol (node communication) Weave Protocol 4 IP in IP (IPIP) Both IPIP encapsulation Calico IPIP mode MetalLB
Port / Range Protocol Direction Description Notes 7472 TCP Inbound Speaker Prometheus metrics 7473 TCP Inbound Controller Prometheus metrics 7946 TCP/UDP Both memberlist (Layer 2 speaker coordination) 179 TCP Both BGP mode (upstream router) Shared with CNI BGP Longhorn
Port / Range Protocol Direction Description Notes 9500 TCP Both Manager API and manager–replica communication 9501 TCP Both Conversion Webhook v1.5.0+ 9502 TCP Both Admission Webhook v1.5.0+ 9503 TCP Both Recovery Backend v1.5.0+ 9504 TCP Both Instance Manager (data plane) 9505 TCP Both Share Manager (shared read/write access via a Network File System (NFS)) 9506 TCP Both Backing Image Manager 10000–12000 TCP Both Instance manager pod replication Required per Longhorn documenttion 8080 TCP Inbound Longhorn UI Via Ingress or port-forward Rook-Ceph
Port / Range Protocol Direction Description 6789 TCP Both Ceph Monitor (Messenger v1) 3300 TCP Both Ceph Monitor (Messenger v2) 6800–7300 TCP Both Object Storage Daemon (OSD) ports (up to 3 per OSD) 7480 TCP Inbound Reliable Autonomic Distributed Object Store (RADOS) Gateway (RGW) object storage endpoint over Hypertext Transfer Protocol (HTTP) 9283 TCP Inbound Ceph Manager Prometheus metrics 8443 TCP Inbound Ceph dashboard over Hypertext Transfer Protocol Secure (HTTPS) Prometheus, Grafana, Alertmanager
Port / Range Protocol Direction Description 9090 TCP Inbound Prometheus server UI and API 9091 TCP Inbound Pushgateway 9093 TCP Inbound Alertmanager HTTP endpoint 9094 TCP/UDP Both Alertmanager cluster high availability (HA) mesh 9100 TCP Inbound Node Exporter metrics endpoint 3000 TCP Inbound Grafana UI 6060 TCP Inbound Profiling endpoint ( pprof)Nginx Ingress
Port / Range Protocol Direction Description 80 TCP Inbound HTTP ingress 443 TCP Inbound HTTPS ingress 8443 TCP Inbound Admission webhook 10254 TCP Inbound Health check and metrics endpoint Vault
Port / Range Protocol Direction Description Notes 8200 TCP Inbound Vault API and UI 8201 TCP Both Vault cluster communication (Raft and HA) 8202 TCP Both Vault replication (disaster recovery (DR) and performance) Enterprise Misc, System
Port / Range Protocol Direction Description Notes 22 TCP Inbound Secure Shell (SSH) 5080 TCP Inbound Local UI 8500 TCP Inbound Consul HTTP API If used 5355 UDP Both Link-Local Multicast Name Resolution (LLMNR) and Multicast Domain Name System (mDNS) ( systemd-resolved)Can usually be restricted Protocol 4 IPIP Both IP-in-IP encapsulation Calico IPIP mode The following example shows how to configure
firewalldinuser-datato create a dedicatedk8szone 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 --reloadwarningConfigure 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.
-
(Optional) To enable FIPS, add the following to your
user-datacloud-configto set the required kernel boot option.#cloud-config
install:
grub_options:
extra_cmdline: "fips=1 selinux=0" -
Once the
user-datafile is ready, issue the following command to build the ISO image.sudo ./earthly.sh isoThe 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
buildfolder.
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.