Skip to content

Build log · Portfolio case study

Engineering a VDI platform in the open.

LocumView is an open-source virtual desktop platform on RHEL 10. This page documents how it is being built, what has broken along the way, and why each decision was made.

Status · Access layer live

Updated

Live: a RHEL 10 desktop reached at login.locumview.com through Keycloak SSO with enforced MFA, Guacamole on k3s, and an outbound-only Cloudflare Tunnel, plus an isolated guest demo. Not done yet: the desktop is rebuilt from documented scripts, not yet from Terraform and Ansible, and STIG hardening has not been applied. Version one is not complete.

The test

“Can a reviewer clone the repo and rebuild it with no manual steps?”

"I configured a thing" is IT administration. "It rebuilds from code" is engineering. Every phase is judged against that test.

Roadmap

Phased, and judged against a rebuild.

0 of 6 phase groups complete. Each phase has a single, testable definition of done.
  1. Phase 0In progress

    Foundations

    Done when: Repo exists, lints run on commit, secrets handling decided, ADRs written.

    • Self-hosted Git with signed commits
    • Pre-commit: ansible-lint, detect-secrets, gitleaks, tflint, pinned by version and digest
    • ADR 0001: RHEL 10 as the base distribution
    • ADRs written: RHEL 10, access layer on k3s, SOPS + age secrets, Cloudflare Tunnel, Keycloak. Still to write: GNOME + Sway, Guacamole, Terraform vs OpenTofu
    • Not yet: CI that re-runs the checks on every push
  2. Phase 1In progress

    Hand-built reference desktop

    Done when: RDP into a headless session, and the changelog fully reproduces the desktop.

    • RHEL 10.2 installed, registered, baselined, and snapshotted
    • vTPM and guest agent verified
    • GNOME Remote Desktop in system (remote login) mode
    • Headless RDP session validated end to end, then straight-to-desktop sessions (user mode) after SSO
    • Not yet: the Windows-familiar layout profile and the Sway kiosk profile
  3. Phase 2In progress

    Access layer

    Done when: A user reaches the desktop in a browser through SSO + MFA, with no RDP exposed.

    • Done: Apache Guacamole + guacd on k3s, default-deny network policies
    • Done: Keycloak SSO with TOTP enforced; next: WebAuthn
    • Done: public at login.locumview.com via an outbound-only Cloudflare Tunnel
    • Done: isolated guest demo account
    • Next: session recording for audit evidence, encrypted pod-to-pod traffic, Nextcloud
  4. Phase 3Up next

    Automation and hardening

    Done when: destroy, then apply, rebuilds the desktop with zero manual steps.

    • Terraform/OpenTofu on libvirt: UEFI + Secure Boot, vTPM, cloud-init
    • Ansible roles: base, hardening, gnome_desktop, sway_kiosk, remote_access
    • DISA STIG applied, OpenSCAP report committed
    • HIPAA technical safeguard mapping
  5. Phase 9Planned

    Clinical AI on synthetic EHR data

    Done when: A scenario patient triggers a review, and a cited, guardrail-checked recommendation lands in the provider queue, Matrix, and dashboard, with a complete audit log and no PHI anywhere.

    • Synthea population plus 10–20 hand-built stewardship scenarios with expected answers
    • FHIR R4 server (Medplum preferred, HAPI FHIR alternative) in the homelab
    • Agents authenticate through real SMART on FHIR OAuth with scoped, revocable tokens
    • Recommendations written back as FHIR Communication or Task resources
  6. Phase 4–8Planned

    Platform ladder and polish

    Done when: Each rung reuses the same desktop and Ansible layers; only the Terraform provider changes.

    • Signed RPMs built with mock, published via COPR; stretch: RHEL image mode (bootc)
    • Cross-distro roles verified by Molecule on RHEL, Fedora, Debian, and Arch
    • KubeVirt on k3s: merging a PR creates a desktop, with no manual kubectl
    • AWS rung alongside SAA-C03: no public IPs, IAM instance roles, budget alarms first
    • Demo video, HIPAA mapping, and a write-up per phase

Version one is interview-ready when

  • One RHEL 10 desktop provisioned by Terraform/OpenTofu and configured by Ansible, rebuildable from a clean clone.
  • Reached through Guacamole in a browser, with Keycloak MFA in front and no exposed RDP.
  • STIG-hardened, with a committed OpenSCAP report.
  • README with an architecture diagram, a working quick-start, and the HIPAA mapping.

Request path

  1. 1Browser
  2. 2Cloudflare (TLS)
  3. 3Cloudflare Tunnel
  4. 4Guacamole
  5. 5guacd
  6. 6RDP
  7. 7RHEL 10 desktop

Only guacd can reach RDP. Nothing on the path listens publicly.

Launch gate

What stands between the internet and a desktop.

The guest demo account is the riskiest surface: a limited-access mirror of the real workspace. It is treated as hostile by design. Items marked next are in progress.

Identity and access

  • Default Guacamole admin removed; no default or test accounts remain
  • Keycloak is the single identity provider over OIDC, so MFA is never enforced in two places
  • MFA (TOTP) enforced for every organizational account, with enrollment required at first sign-in; the guest demo is password-only by design and contained by the isolation controls below
  • Next: WebAuthn and passkeys, preferred over TOTP for phishing resistance
  • Brute-force lockout in Keycloak and Guacamole and short token lifetimes; next: rate limiting at the edge

Guest isolation

  • Guest egress fenced: internet only, no route to the private network, the cluster API, or other desktops
  • Separate non-admin guest account, no stored credentials, tokens, or kubeconfigs, and no access to other users' files
  • Next: an ephemeral guest VM that resets to a clean state between sessions
  • No patient data on the demo; synthetic Synthea and scenario data only once clinical features exist

Exposure

  • RDP and VNC never exposed directly; only the Guacamole gateway, behind TLS
  • Published only through an outbound-only Cloudflare Tunnel; no open inbound ports, and the identity provider's admin console is not reachable from the internet
  • Changes applied after a known-good snapshot so they stay rollback-able

Clinical AI pipeline

Stewardship logic lives in retrieval and guardrails, not model weights.

Planned design (Phase 9, not built yet). No training or fine-tuning: the base model reasons over retrieved guidelines (IDSA, local antibiogram, protocols) and uses FHIR as a tool, which keeps every recommendation auditable. Swapping in a real EHR later should be mostly configuration.
  1. Stage 1

    Trigger

    Scheduled sweep of active antimicrobial orders, or an event such as a new culture result via CDS Hooks.

  2. Stage 2

    Retrieval

    Patient, MedicationRequest, Observation, Condition, and AllergyIntolerance over FHIR R4.

  3. Stage 3Core build

    Normalization

    Raw FHIR becomes a pharmacist-style picture: drug, dose, renal function, organism, day of therapy.

  4. Stage 4Core build

    Reasoning

    Model plus retrieved guidelines. Every recommendation cites its source and shows its reasoning.

  5. Stage 5Core build

    Guardrails

    Safety-bound checks, citation required, low-confidence output suppressed or flagged for human review.

  6. Stage 6

    Delivery and audit

    Routed to the provider queue, Matrix, or dashboard, with every step logged.

Tuning what surfaces versus what gets suppressed is where clinical judgment matters most. Every scenario output is validated against what a clinical pharmacist would recommend.

Field findings

What the hand-built reference taught the automation.

Phase 1 was deliberately manual. Every surprise became a requirement for the Terraform and Ansible that replace it.
Firmware

Manual builds drift from intent

The reference VM came up on SeaBIOS, the virt-manager default, instead of the UEFI + Secure Boot target. It was rebuilt from a recorded virt-install command with UEFI, Secure Boot, and a vTPM, and the Terraform module will enforce it. It is the argument for building from code, demonstrated.

SELinux

TPM credential sealing vs SELinux

GNOME Remote Desktop 49 can seal RDP credentials to a TPM, but RHEL 10.2 policy denies the confined daemon access to the TPM device. The change was reverted, not patched with a custom module, because a local policy exception is not defensible under STIG. Evidence is committed, and an upstream selinux-policy report is a candidate contribution.

TLS

Certificates need SANs

FreeRDP flagged the CN-only self-signed certificate as a name mismatch. The Ansible remote_access role will issue certificates with DNS and IP SANs, strict file permissions, and the correct SELinux type.

Tooling

Supply-chain-aware tooling without EPEL

pre-commit, ansible-lint, gitleaks, tflint, and Terraform are not in RHEL repos. They are installed with pip --user, as Podman containers pinned by digest, or from vendor-signed RPM repos, so the build machine stays clean.

Kubernetes

Fixing the platform under the platform

Self-hosted Git on k3s lost SSH access. The fix exposed SSH through a LoadBalancer Service, corrected the in-pod listen port, stopped a restart from resetting the install wizard, and replaced a RollingUpdate that deadlocked on a shared volume with Recreate.

Ansible

Never hardcode system IDs

System UIDs and GIDs are allocated dynamically on RHEL 10, so the Ansible roles will look them up at run time instead of assuming them. Credentials are in the repo only as SOPS-encrypted secrets, with a pre-commit check that blocks plain text.

Architecture decisions

Each choice, and the reason behind it.

Base OS
RHEL 10
Dominant enterprise Linux in regulated healthcare: published STIG and CIS baselines, FIPS-validated crypto, a support contract, and a 10-year lifecycle. SLES and Ubuntu LTS were considered; Rocky and Alma suit labs but lack vendor accountability. The distribution is the most swappable layer, so roles can branch to SLES or Debian later.
Desktop
GNOME via headless RDP, Sway kiosk profile
RHEL 10 is Wayland-only; GNOME Remote Desktop is the supported native RDP path. GNOME Classic serves as a Windows-familiar profile.
Broker
Apache Guacamole
Apache 2.0, native RDP, OIDC/SAML with Keycloak, and session recording for audit evidence.
Identity
Keycloak SSO + MFA
Centralized identity, moving to WebAuthn and passkeys for phishing resistance, analogous to clinical badge-tap.
Exposure
Outbound-only Cloudflare Tunnel
Nothing listens on the home network edge. The identity provider admin console is not published. RDP is only reachable from guacd.
Tool split
Terraform outside, Ansible inside
Terraform owns the VM and host settings; Ansible owns everything in the guest. Terraform outputs feed Ansible inventory.
Hardening
scap-security-guide + OpenSCAP
Compliance as code, with the reports committed to the repo and mapped to HIPAA safeguards.
Platform ladder
libvirt, then KubeVirt, then AWS
The desktop and Ansible layers stay constant across all three. KubeVirt tells the OpenShift Virtualization story.

Horizon mapping

Commercial VDI capabilities, rebuilt with open tooling.

The name nods to VMware View, the original name of Horizon, now owned by Omnissa. Each Horizon component has an open, auditable counterpart.
Horizon componentLocumView counterpart
Unified Access GatewayCloudflare Tunnel (Tailscale for administration)
Connection ServerApache Guacamole
Blast / HTML AccessRDP via guacd, in the browser
Identity / MFAKeycloak with TOTP (WebAuthn next)
Instant-clone poolsTerraform + Ansible desktop profiles (planned)

Honest scope

Where it fits, and where it does not.

LocumView targets workloads that are web-based or Linux-native: analysts, data science, pharmacovigilance, developers, and browser-based EHR access. It is not a drop-in replacement for a primary native EHR workstation; native Epic and older Meditech clients remain Windows-first.

There is no clinical-grade Windows compatibility layer. Running patient-care software under Wine or Proton is a compliance and patient-safety risk, so Windows-dependent apps follow a decision hierarchy instead.

EHR presentation layers keep moving into the browser: Meditech Expanse is web-based, and Epic Hyperdrive wraps a Chromium engine. Built on web technology is not the same as runs in any browser, but as the browser becomes the workstation, securing and standardizing it becomes the real infrastructure problem.

Windows-app decision order

  1. 1.Web-based and Linux-native applications
  2. 2.Application publishing from a supported Windows host
  3. 3.Open-source replacements for general productivity
  4. 4.Compatibility layers only for low-risk internal tools, never clinical systems

As clinical computing moves to the browser, hardened and reproducible Linux endpoints let organizations reduce licensing dependence and shrink their attack surface, adopted workload by workload.

Cost and security model

Savings from consolidation, not elimination.

Windows is demoted from a per-seat desktop to a shared, right-sized application service. The savings scale with how much of the workload is web or Linux-native.
Endpoint OS cost goes away
Desktops run community Linux or Red Hat with a support subscription, replacing stacked Windows plus VDI licensing.
Concurrent, not one-to-one
A shared Windows session-host pool is sized to concurrent use of legacy apps, not one idle desktop per person.
A shrinking footprint
As clinical software goes web-native, the Windows farm and its cost curve bend downward year over year.
A smaller attack surface
Locked-down, immutable Linux endpoints sidestep much of the Windows-targeted ransomware landscape common in healthcare.

Honest caveats: Volunteered up front: Windows Server and delivery licensing still needs careful sizing, the publishing tier is real engineering, and retraining and Windows-first peripherals (badge readers, label printers, dictation) scope the thesis rather than kill it.

Beyond the platform

Follow-on work built on the same foundation.

Secure PV analyst workstation

View-but-not-copy desktop profile with fapolicyd and audited sessions for pharmacovigilance AI work.

OpenShift Virtualization port

KubeVirt desktops on OpenShift with Operators, Routes, and OpenShift GitOps.

Workspaces vs desktops

The same environment as Dev Spaces and as full VDI, with a written tradeoff analysis.

Self-healing compliance

OpenSCAP detects drift; Event-Driven Ansible remediates it and logs the change.

Centralized Linux identity

Red Hat IdM for users, host-based access, and sudo rules, federated with Keycloak.

Backup and disaster recovery

Scheduled snapshots, tested restores, and documented recovery targets.

Packaging contributions

Every dependency RHEL 10 does not ship is a candidate Fedora or EPEL contribution, starting with the Python FHIR libraries (fhirclient, fhir.resources) and the Sway toolchain. LocumView-specific builds go to COPR; large services stay as containers.