VDI Image Build Process — Desktop Images for Frontier Workspace

How Frontier Workspace builds reproducible, hardened VDI desktop images with Packer for provisioning through the self-service workspace flow.

This page describes how Federal Frontier Platform builds the OpenStack cloud images that Frontier Workspace uses to provision VDI desktops. The same immutable-infrastructure pattern used for Kubernetes node images applies here: every desktop VM boots from a Packer-built Glance image with the desktop environment, display server, and hardening controls pre-installed. No first-boot package installs, no SSH-and-configure, no snowflakes.

Why Packer for VDI images?

The temptation with VDI is to start from a stock cloud image and use cloud-init to install the desktop environment, XRDP, and security controls at first boot. This fails for the same reasons it fails for Kubernetes nodes:

  1. Boot time. Installing GNOME or XFCE at first boot adds 10-20 minutes. Users waiting for a workspace after admin approval should wait 60 seconds, not 20 minutes.
  2. Drift. Cloud-init installs pull whatever package versions are available that day. Two workspaces provisioned a week apart may have different desktop configurations, different security patches, different XRDP versions.
  3. Airgap. IL5 and IL6 deployments have no path to external package repositories. Desktop packages must be in the image.
  4. STIG compliance. DISA STIG hardening must be applied consistently to every desktop instance. Running Ansible at first boot on every workspace is fragile and slow. Baking it into the image guarantees every instance is identically hardened.

The rule: if you find yourself SSHing into a VDI VM to install packages, STOP. Fix the Packer template instead.

Image catalog

Frontier Workspace ships two golden images. Both are built with Packer and stored in OpenStack Glance.

Image OS Desktop Use Case FIPS STIG
ffp-vdi-desktop-v2.2 Ubuntu 22.04 LTS XFCE General knowledge worker, demos No No
ffp-vdi-rocky9-cui Rocky Linux 9 GNOME Classic (X11) CUI enclave, federal workstations Yes Yes

The standard Ubuntu image is the default for all non-CUI workspaces. The Rocky Linux 9 CUI image is provisioned when an administrator enables the CUI Enclave toggle during workspace approval.

Pipeline overview

Both images follow the same pipeline stages:

  1. Base OS install — Packer boots a build VM from a cloud image in Glance
  2. Desktop environment — Install GNOME Classic (Rocky) or XFCE (Ubuntu)
  3. XRDP — Install and configure the RDP server for remote display
  4. Cloud-init — Ensure cloud-init is installed for password injection at boot
  5. Persistent home service — Install the systemd oneshot that mounts Cinder volumes at /home/vdi-user
  6. Hardening — FIPS mode, STIG controls, CUI banner (Rocky only)
  7. Cleanup — Remove SSH host keys, cloud-init state, machine-id, package cache, logs
  8. Snapshot — Packer snapshots the root volume to Glance as the golden image

CUI enclave image: ffp-vdi-rocky9-cui

Build stages

Stage Script What it does Why
FIPS mode 01-enable-fips.sh Installs crypto-policies-scripts, runs fips-mode-setup –enable NIST 800-171 SC-13 requires FIPS-validated cryptography. Kernel boots with fips=1.
Desktop 02-install-desktop.sh Installs “Server with GUI” group (GNOME Classic), EPEL, XRDP, xorgxrdp, cloud-init, Firefox GNOME Classic on X11 is the standard RHEL desktop. Wayland does not work with XRDP.
XRDP config 03-configure-xrdp.sh Writes startwm.sh for GNOME Classic, sets cliprdr=false, rdpdr=false, rdpsnd=false in xrdp.ini CUI enclave requires clipboard and drive redirection disabled at the protocol level.
VDI user 04-vdi-user-setup.sh Creates vdi-user, installs vdi-persistent-home.service and mount script Cloud-init sets the password at boot. The systemd service mounts Cinder volumes for persistent home directories.
STIG 05-stig-hardening.sh Applies DISA STIG controls: session lock (AC-11), audit logging (AU-2/AU-3), account lockout (AC-7), password complexity (IA-5), SSH hardening (SC-8), USB storage disabled (CM-6), core dumps disabled (SC-4) NIST 800-171 requires these controls for any system processing CUI.
CUI banner 06-cui-banner.sh Sets /etc/motd, /etc/issue, /etc/issue.net, and GNOME GDM banner to “CUI Enclave — Authorized Use Only — All sessions are monitored and recorded” AC-8 requires a system use notification before login.
Cleanup 99-cleanup.sh Removes SSH host keys, cloud-init state, machine-id, package cache, logs, bash history Generalizes the image so each instance gets unique identity at first boot.

FIPS mode

FIPS mode is enabled at build time via fips-mode-setup --enable, which:

  • Sets fips=1 on the kernel command line (GRUB)
  • Switches the system crypto policy to FIPS
  • Configures OpenSSL, libgcrypt, NSS, and GnuTLS to use only FIPS-approved algorithms

To verify FIPS mode on a running workspace:

fips-mode-setup --check
# Expected output: FIPS mode is enabled.

The NIST CMVP validation certificate is the customer’s responsibility, matched to their existing procurement relationship (Red Hat subscription, CIQ RLC Pro, TuxCare, or self-attestation). Eupraxia Labs builds the image with FIPS mode enabled; the customer provides the validation path for their ATO package.

STIG controls applied

The following NIST 800-171 / DISA STIG controls are applied at build time:

Control NIST 800-171 Implementation
Session lock AC-11 GNOME screensaver locks after 15 minutes, settings locked via dconf
Audit logging AU-2, AU-3 auditd enabled with rules for logins, privilege escalation, file deletion, sudo, SSH config
Account lockout AC-7 faillock: 3 failed attempts, 15-minute lockout
Password complexity IA-5 15-character minimum, upper/lower/digit/special required
SSH hardening SC-8 No root login, max 4 auth tries, idle timeout 600s, no X11 forwarding, banner
USB storage disabled CM-6 usb-storage module blacklisted
Core dumps disabled SC-4 Hard limit 0, kernel.core_pattern set to /bin/false
Login banner AC-8 CUI enclave notice on console, SSH, and GDM

CUI enclave vs standard image

Feature ffp-vdi-desktop-v2.2 (Ubuntu) ffp-vdi-rocky9-cui (Rocky)
OS Ubuntu 22.04 LTS Rocky Linux 9
Desktop XFCE GNOME Classic (X11)
FIPS mode No Yes
STIG hardening No Yes
Clipboard (xrdp) Enabled Disabled
Drive redirection Enabled Disabled
Sound redirection Enabled Disabled
Login banner None CUI enclave notice
Guacamole clipboard Enabled Disabled (per-connection)
Guacamole file transfer Enabled Disabled (per-connection)
Session recording No Yes (guacd recordings volume)
Use case Demos, non-CUI work CUI processing, federal workstations

Standard image: ffp-vdi-desktop-v2.2

The Ubuntu image is simpler — no FIPS, no STIG, no channel hardening. It includes:

  • Ubuntu 22.04 LTS with XFCE desktop
  • XRDP 0.9.17 with all channels enabled
  • cloud-init for password injection
  • vdi-persistent-home.service for Cinder volume mounting
  • Screensaver and screen lock disabled (Guacamole sessions don’t need desktop lock)

This image is used for demos, development workstations, and non-CUI workspaces.

Operator workflows

Build a new CUI enclave image

cd packer/rocky9-cui

packer init .

packer build \
  -var "openstack_password=<admin-password>" \
  -var "network_id=<frontier-net-uuid>" \
  .

The build takes approximately 15-20 minutes. The output image appears in Glance with the name ffp-vdi-rocky9-cui-YYYYMMDD.

Verify a built image

Boot a test VM from the image and check:

# FIPS mode
fips-mode-setup --check

# STIG controls
auditctl -l                    # Audit rules loaded
faillock                       # Faillock configured
grep -c "^minlen" /etc/security/pwquality.conf  # Password policy

# XRDP channels
grep cliprdr /etc/xrdp/xrdp.ini   # Should show false

# CUI banner
cat /etc/motd

Image lifecycle

Images are versioned by build date: ffp-vdi-rocky9-cui-20260515. To patch:

  1. Update the Packer scripts with new package versions or security fixes
  2. Run packer build to produce a new dated image
  3. Update the deploy script’s CUI_IMAGE variable to point to the new image name
  4. Existing workspaces continue running on the old image
  5. New workspaces provision on the new image
  6. Users with persistent Cinder volumes can be migrated by destroying the old workspace and provisioning a new one — the persistent volume carries their data across images

Troubleshooting

GNOME Classic doesn’t load over XRDP

The XRDP session file (/etc/xrdp/startwm.sh or /home/vdi-user/.xsession) must specify gnome-session --session=gnome-classic, not gnome-session alone. The gnome-classic session type uses X11; the default GNOME session may attempt Wayland, which XRDP does not support.

FIPS mode not enabled after boot

Check that fips=1 is on the kernel command line:

cat /proc/cmdline | grep fips

If missing, fips-mode-setup --enable was not run during the Packer build, or the GRUB config was overwritten by a later step.

XRDP “login failed for display 0”

This means XRDP connected to xrdp-sesman but PAM authentication failed. Check:

  • The vdi-user password was set correctly by cloud-init (verify /var/log/cloud-init-output.log)
  • The Guacamole connection has the same password as cloud-init injected
  • xrdp-sesman logs: journalctl -u xrdp-sesman --no-pager -n 30

Session recording files not appearing

Verify the guacd pod has the session recordings PVC mounted:

kubectl -n f3iai exec deploy/guacd -- ls -la /var/lib/guacamole/recordings/

Verify the Guacamole connection has recording-path set in its parameters.