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:
- 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.
- 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.
- Airgap. IL5 and IL6 deployments have no path to external package repositories. Desktop packages must be in the image.
- 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:
- Base OS install — Packer boots a build VM from a cloud image in Glance
- Desktop environment — Install GNOME Classic (Rocky) or XFCE (Ubuntu)
- XRDP — Install and configure the RDP server for remote display
- Cloud-init — Ensure cloud-init is installed for password injection at boot
- Persistent home service — Install the systemd oneshot that mounts Cinder volumes at /home/vdi-user
- Hardening — FIPS mode, STIG controls, CUI banner (Rocky only)
- Cleanup — Remove SSH host keys, cloud-init state, machine-id, package cache, logs
- 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=1on 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:
- Update the Packer scripts with new package versions or security fixes
- Run
packer buildto produce a new dated image - Update the deploy script’s
CUI_IMAGEvariable to point to the new image name - Existing workspaces continue running on the old image
- New workspaces provision on the new image
- 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.