Containerization: Secure Deployment with KASM Workspace
The 2019 incident is still the cleanest example I have. A mid-size healthcare org, 400 seats of Citrix, a VPN concentrator that hadn’t been patched in eleven months, and a shadow IT problem nobody wanted to name out loud. A nurse practitioner needed access to a legacy imaging app, IT said “six weeks for a new published app,” so she spun up a rogue VM on a spare workstation, bridged it to the clinical VLAN, and exposed RDP to the internet through a consumer router. It took four months before anyone noticed. The Citrix renewal invoice that landed the same quarter — roughly $180K for concurrent licensing on a platform half the staff had stopped using — was the thing that finally got the CTO’s attention.
If you run desktops, apps, or remote access for a living, you already know the shape of this problem. Traditional VDI was supposed to solve it. For a lot of organizations it just moved the pain from endpoint management to gold-image management, and added a licensing model that punishes you for success. Containerized workspaces — specifically KASM — are the alternative that actually delivers on the promise, provided you understand what you’re buying and what you’re not.
This guide is for sysadmins, security engineers, and platform folks who need to deploy KASM in production, not in a demo. By the end you’ll have a working single-node deployment, a hardened image posture, an SSO integration path, and a clear-eyed view of where containers help you and where they lie to you.
Prerequisites
Section titled “Prerequisites”- A Linux host (Ubuntu 22.04 LTS or Rocky 9 — Debian 12 works too) with at least 4 vCPU, 16 GB RAM, and 100 GB SSD for a small pilot
- Docker Engine 24.x+ and the Compose v2 plugin (
docker compose, notdocker-compose) - Root or sudo access — KASM’s installer needs to write to
/optand manage kernel modules - cgroup v2 enabled (default on modern distros; verify with
stat -fc %T /sys/fs/cgroup) - A DNS name pointing at the host, and outbound 443 for Let’s Encrypt if you’re doing ACME
- Basic familiarity with reverse proxies and PostgreSQL. This is not a tutorial for someone who has never touched NGINX.
The Problem With How You’re Deploying Desktops Right Now
Section titled “The Problem With How You’re Deploying Desktops Right Now”Traditional VDI failed mid-size orgs for a reason that has nothing to do with technology. The economics only work at scale, and the operational model assumes a level of process discipline most shops don’t have. A “golden image” is a lie the moment someone patches it out of band. I’ve walked into environments where three different teams maintained three different gold images for the same department because nobody trusted the other team’s build.
The VDI Hangover
Section titled “The VDI Hangover”The legacy stack looks like this, and if you’ve run it you can feel the weight of every layer:
[ Endpoint ] --VPN--> [ Concentrator ] --RDP--> [ Connection Broker ] | [ Hypervisor ] | [ Gold Image / Pool ] | [ Shared Storage (SAN/NAS) ]Every layer is a patching surface. Every layer is a licensing conversation. The broker alone typically runs you per-concurrent-user, and the hypervisor is per-socket or per-core depending on vendor. Golden image drift is the named sin — the slow divergence between what the image was built as and what it becomes after six months of hotfixes applied by hand at 2 AM.
The Container-Native Alternative
Section titled “The Container-Native Alternative”What changes with containers is the unit of deployment. Instead of a persistent VM that lives for months and accumulates cruft, you get an ephemeral container that boots in seconds, runs one workload, and dies. The isolation boundary moves from the hypervisor to the kernel — which is a real trade-off, not a free lunch, and we’ll get into it in section 4.
The density story is the one that makes CFOs stop crying. A hypervisor running 40 VMs on a 64-core box is doing well. The same box running containers can push several hundred concurrent lightweight workspaces, depending on workload. Startup time drops from “minutes” to “seconds.” Patching becomes “rebuild the image and redeploy” instead of “schedule a maintenance window.”
Comparison: Legacy VDI vs Containerized Workspace
| Dimension | Legacy VDI | Containerized Workspace |
|---|---|---|
| Isolation boundary | Hypervisor (hardware-assisted) | Linux namespaces + cgroups (shared kernel) |
| Startup time | 30–90 seconds typical | 2–10 seconds typical |
| Density per host | 30–60 VMs | 100–400 containers depending on workload |
| Patch model | In-place on gold image, drift-prone | Immutable image rebuild |
| Licensing | Per-seat or per-concurrent, vendor-locked | Open source core, support optional |
| GPU passthrough | Mature, well-supported | Improving but workload-dependent |
Where KASM Fits
Section titled “Where KASM Fits”KASM is not a hypervisor replacement. It’s an orchestration layer that sits on top of Docker (single-node) or Kubernetes (multi-node), manages workspace lifecycle, handles authentication, and streams the display via KasmVNC. If you need strict hardware isolation for regulatory reasons, or GPU-heavy CAD work with certified drivers, this may not be your tool. Be honest with yourself about that before you spend six months migrating.
To ground this, here’s the simplest possible thing that works — pulling a stock workspace image:
docker run --rm -it -p 6901:6901 \ -e VNC_PW=change-me-now \ kasmweb/desktop:1.15.0That runs a single Ubuntu desktop container and exposes KasmVNC on 6901. It is not a deployment. It’s a proof that the image model is real. Production looks nothing like this line, and pretending otherwise is how people end up with unauthenticated desktops on the public internet.
KASM Workspace Architecture: What’s Actually Happening Under the Hood
Section titled “KASM Workspace Architecture: What’s Actually Happening Under the Hood”You cannot secure or scale a system you don’t understand. KASM has five moving parts, and if you can’t draw them from memory, you’re going to have a bad time when something breaks at 3 AM.
The Component Stack
Section titled “The Component Stack”[ Browser ] --HTTPS/WSS--> [ NGINX Reverse Proxy ] | v [ KASM API (Manager) ] | | v v [ Postgres ] [ KASM Agent(s) ] | v [ Docker Engine ] | v [ Workspace Containers ]The KASM API (sometimes called the Manager) is the control plane. It handles auth, scheduling, and the admin UI. The Agent runs on each workspace node and talks to the local Docker daemon. PostgreSQL stores users, groups, sessions, and configuration. The reverse proxy terminates TLS and routes WebSocket traffic to the right workspace. KasmVNC is the display server inside each container — it’s a fork of TigerVNC that speaks WebSocket natively, which is why there’s no client install.
Session Flow
Section titled “Session Flow”When a user logs in, the API validates credentials, issues a session token, and picks a node. The agent starts a container and returns a connection URL. The browser opens a WebSocket to the proxy, which forwards to KasmVNC inside the container. Pixels stream back over that socket. The token lifecycle is short — typically minutes — and the session itself is tracked in Postgres.
Storage and Persistence
Section titled “Storage and Persistence”Ephemeral workspaces are the default and the right answer for most use cases. Persistent profiles are a footgun if you don’t scope them per-user, because a shared home directory across a group is just a shared filesystem with extra steps and worse permissions.
FAQ: Does KASM require Kubernetes, or can I run it on plain Docker?
Plain Docker is fully supported for single-node deployments and is the right starting point for pilots and small teams. Kubernetes is the answer when you need HA, multi-node scheduling, or you’re already running a cluster. The workspace images are identical either way.
Here’s a minimal single-node compose fragment to make the architecture concrete:
services: kasm_db: image: postgres:15-alpine environment: POSTGRES_USER: kasmapp POSTGRES_PASSWORD: ${DATABASE_PASSWORD} POSTGRES_DB: kasm volumes: - kasm_db_data:/var/lib/postgresql/data restart: unless-stopped
kasm_api: image: kasmweb/manager:1.15.0 depends_on: [kasm_db] environment: DATABASE_HOSTNAME: kasm_db DATABASE_PASSWORD: ${DATABASE_PASSWORD} MANAGER_HOSTNAME: ${MANAGER_HOSTNAME} ports: - "443:443" volumes: - kasm_api_data:/opt/kasm/current restart: unless-stopped
volumes: kasm_db_data: kasm_api_data:The .env file that goes with it — and this should never be committed:
# .env — chmod 600, owned by root, never in gitDATABASE_PASSWORD=generate-a-64-char-random-string-hereMANAGER_HOSTNAME=kasm.example.comKASM_PORT=443Standing Up KASM: A Reproducible, Non-Toy Deployment
Section titled “Standing Up KASM: A Reproducible, Non-Toy Deployment”Before you run the installer, harden the host. This is the step everyone skips and then blames the software for.
Host Hardening
Section titled “Host Hardening”net.ipv4.conf.all.rp_filter = 1net.ipv4.conf.default.rp_filter = 1net.ipv4.tcp_syncookies = 1net.ipv4.conf.all.accept_redirects = 0net.ipv4.conf.all.send_redirects = 0net.ipv6.conf.all.accept_redirects = 0kernel.unprivileged_userns_clone = 1kernel.dmesg_restrict = 1kernel.kptr_restrict = 2fs.protected_hardlinks = 1fs.protected_symlinks = 1Apply with sysctl --system. The unprivileged_userns_clone setting is required for rootless container features; the rest are standard CIS-benchmark hygiene. Disable swap entirely — swapoff -a and comment out swap in /etc/fstab — because the kernel’s memory accounting gets weird under container pressure.
The Default Credentials Trap
Section titled “The Default Credentials Trap”KASM’s installer generates an admin password and writes it to the install log. If you don’t rotate it immediately, you’ve just deployed a system with credentials sitting in a file that half your team has read access to. Change it before you do anything else, and wire up SSO in the same week.
TLS and Reverse Proxy
Section titled “TLS and Reverse Proxy”Terminate TLS at the proxy, not inside KASM. You want one place to manage certificates, one place to enforce HSTS, and one place to log. Inside KASM, keep it plain HTTP on localhost.
server { listen 443 ssl http2; server_name kasm.example.com;
ssl_certificate /etc/letsencrypt/live/kasm.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/kasm.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always;
location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }}The Upgrade and Connection headers are what make KasmVNC work. Without them, the WebSocket handshake fails and you get a blank screen with no useful error. The 3600-second timeouts matter because idle sessions will otherwise drop mid-use.
Comparison: Single-node Compose vs Multi-node K8s
| Dimension | Docker Compose | Kubernetes (Helm) |
|---|---|---|
| Complexity | Low — one host, one compose file | High — cluster, ingress, storage classes |
| HA | None (single point of failure) | Yes, with proper topology |
| Upgrade path | Stop, pull, start | Rolling or blue/green |
| Ops burden | You and systemd | A platform team |
| Best for | Pilots, [50 users, dev sandboxes | Regulated, ]100 users, existing K8s shops |
FAQ: How many concurrent users can a single KASM node handle?
In testing, a 16 vCPU / 64 GB node comfortably runs 40–60 concurrent lightweight desktop sessions (browser, office apps, terminal). Heavy workloads — CAD, video editing, anything GPU-bound — drop that to single digits. The bottleneck is almost always memory and disk I/O on /var/lib/docker, not CPU. Size for RAM first, then disk, then cores.
Security Model: Where Containers Help and Where They Lie
Section titled “Security Model: Where Containers Help and Where They Lie”Containers are not VMs. Anyone who tells you otherwise is selling something. The kernel is shared, which means a kernel CVE has a blast radius that includes every workspace on the host.
Isolation Reality
Section titled “Isolation Reality”What you get from containers: namespace isolation for PID, network, mount, and user; cgroup resource limits; and the ability to drop Linux capabilities. What you don’t get: hardware-enforced memory isolation, separate kernels, or protection against kernel privilege escalation. For multi-tenant workloads with adversarial users, this matters. For internal dev teams, it’s usually fine — but “usually” is doing a lot of work in that sentence.
Hardened invocation for a workspace container:
docker run -d \ --name ws-hardened \ --read-only \ --tmpfs /tmp:rw,noexec,nosuid,size=512m \ --tmpfs /run:rw,noexec,nosuid,size=64m \ --cap-drop=ALL \ --cap-add=CHOWN --cap-add=SETUID --cap-add=SETGID --cap-add=DAC_OVERRIDE \ --security-opt no-new-privileges:true \ --security-opt seccomp=/etc/docker/seccomp-kasm.json \ --user 1000:1000 \ --memory 2g --cpus 2 \ kasmweb/desktop:1.15.0The --read-only flag combined with tmpfs mounts means the container filesystem is immutable at runtime. --cap-drop=ALL followed by explicit adds is the only correct way to do capabilities — never start from the default set and subtract. no-new-privileges blocks setuid binaries from escalating.
Hardening the Image
Section titled “Hardening the Image”Your Dockerfile should end with a non-root user and a minimal base. Alpine or distroless where possible, though KASM’s stock images are Ubuntu-based for compatibility.
FROM kasmweb/desktop:1.15.0USER rootRUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates curl && \ rm -rf /var/lib/apt/lists/*COPY --chown=1000:1000 ./config /home/kasm-user/.configUSER 1000:1000HEALTHCHECK --interval=30s --timeout=5s \ CMD curl -sf http://localhost:6901/ || exit 1Sign and scan every build. Not “when you have time” — every build:
cosign sign --key cosign.key registry.example.com/kasm/desktop:1.15.0-customtrivy image --severity HIGH,CRITICAL --exit-code 1 \ registry.example.com/kasm/desktop:1.15.0-customcosign verify --key cosign.pub registry.example.com/kasm/desktop:1.15.0-customNetwork Egress
Section titled “Network Egress”Workspaces on the flat LAN is a breach waiting to happen. Deny egress by default and allow only what’s needed. In Kubernetes:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: workspace-egress-deny namespace: kasm-workspacesspec: podSelector: matchLabels: app: kasm-workspace policyTypes: [Egress] egress: - to: - namespaceSelector: matchLabels: name: kasm-control ports: - protocol: TCP port: 443 - to: - ipBlock: cidr: 10.0.5.0/24 ports: - protocol: TCP port: 443Comparison: Default vs Hardened Posture
| Dimension | Default KASM | Hardened |
|---|---|---|
| Capabilities | Docker default set | Drop ALL, add minimum |
| Filesystem | Writable rootfs | Read-only + tmpfs |
| User | Often root in older images | Non-root UID 1000 |
| Network | Bridge, unrestricted egress | Deny-by-default egress |
| Image provenance | Unsigned | cosign-signed, Trivy-gated |
FAQ: Can a malicious user escape a KASM workspace container?
Yes, if the kernel has an unpatched local privilege escalation and the container has the capabilities needed to exploit it. --cap-drop=ALL plus no-new-privileges plus a current kernel dramatically narrows the window, but it does not close it. If your threat model includes determined attackers with code execution inside the workspace, run those workspaces on dedicated nodes or use a VM-based sandbox like Kata Containers or gVisor.
Identity, Access, and the SSO Conversation
Section titled “Identity, Access, and the SSO Conversation”Local KASM users are fine for a lab. In production they’re a liability — you’ve just created a second identity store that nobody audits, nobody rotates, and nobody deprovisions when someone leaves.
SAML and OIDC
Section titled “SAML and OIDC”KASM supports SAML 2.0 and OIDC. Pick one and commit. If your org already uses Okta, Azure AD, or Keycloak, wire it up. The config is a JSON blob in the admin UI or via the API:
{ "idp_metadata_url": "https://idp.example.com/app/kasm/sso/saml/metadata", "entity_id": "https://kasm.example.com/saml/metadata", "acs_url": "https://kasm.example.com/api/saml/acs", "sp_cert": "/opt/kasm/current/certs/saml.crt", "sp_key": "/opt/kasm/current/certs/saml.key", "attribute_map": { "email": "urn:oid:0.9.2342.19200300.100.1.3", "groups": "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups" }}RBAC and Group Mapping
Section titled “RBAC and Group Mapping”The anti-pattern I see most often is “everyone gets the Ubuntu desktop image.” That’s not access control, that’s a shared account with extra steps. Map IdP groups to KASM groups to specific images:
| IdP Group | KASM Group | Allowed Images |
|---|---|---|
| eng-backend | developers | ubuntu-dev, docker-cli |
| eng-frontend | developers | ubuntu-dev, node-toolchain |
| finance | standard | ubuntu-desktop, office-suite |
| contractors | restricted | ubuntu-desktop (no persistence) |
Session Recording
Section titled “Session Recording”KASM can record sessions. It captures the video stream, not keystrokes, and stores it wherever you point it. Two things to know: storage costs add up fast (a 1080p session is roughly 1–2 GB per hour depending on motion), and in many jurisdictions you need explicit consent or a documented policy before you record employee sessions. Talk to legal before you turn this on.
Ship logs to your SIEM. Fluent Bit is the least painful option:
[INPUT] Name tail Path /opt/kasm/logs/*.log Tag kasm.*
[OUTPUT] Name forward Match kasm.* Host siem.example.com Port 24224FAQ: Does KASM support MFA?
Yes, but via your IdP. KASM itself doesn’t implement TOTP or WebAuthn natively in a way you should rely on. The correct answer is: enforce MFA at the IdP, and use SAML/OIDC so KASM never sees a bare password.
Operations at Scale
Section titled “Operations at Scale”The day-two problems are what kill KASM deployments, not the initial install.
Image Lifecycle
Section titled “Image Lifecycle”Every custom image goes through build → sign → scan → push. A GitHub Actions workflow that does all four:
name: build-kasm-imageon: push: branches: [main]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: registry.example.com username: ${{ secrets.REG_USER }} password: ${{ secrets.REG_PASS }} - uses: docker/build-push-action@v5 with: push: true tags: registry.example.com/kasm/desktop:${{ github.sha }} - name: Scan run: trivy image --exit-code 1 --severity HIGH,CRITICAL \ registry.example.com/kasm/desktop:${{ github.sha }} - name: Sign run: cosign sign --key env://COSIGN_KEY \ registry.example.com/kasm/desktop:${{ github.sha }} env: COSIGN_KEY: ${{ secrets.COSIGN_KEY }}Monitoring
Section titled “Monitoring”Alert on four things: agent health (is the agent reachable from the API?), container churn (are workspaces restarting?), session failures (login-to-pixel success rate), and disk on /var/lib/docker (this is what actually fills up). Prometheus scrape config:
scrape_configs: - job_name: kasm_agent static_configs: - targets: ['kasm-agent-01:9090', 'kasm-agent-02:9090'] metrics_path: /metrics scrape_interval: 30sUpgrades and Backups
Section titled “Upgrades and Backups”Blue/green for the manager, drain-and-upgrade for agents. And back up Postgres — then actually test the restore, because an untested backup is a rumor:
# Backuppg_dump -h kasm_db -U kasmapp -Fc kasm > /backup/kasm-$(date +%F).dump
# Restore — run this in a test environment at least monthlypg_restore -h test_db -U kasmapp -d kasm --clean --if-exists \ /backup/kasm-2026-09-01.dumpComparison: Upgrade Strategies
| Strategy | Downtime | Risk | Complexity |
|---|---|---|---|
| In-place | Minutes to hours | High — rollback is painful | Low |
| Rolling | Near-zero | Medium — version skew | Medium |
| Blue/green | Zero | Low — instant rollback | High |
Real-World Patterns
Section titled “Real-World Patterns”Pattern A — Dev Sandbox. Single node, ephemeral workspaces, no persistence, OIDC to GitHub. Scaling story is “buy a bigger box.” Config differentiator: persistent_profile: false. What breaks: nothing until it does, and then you’re rebuilding on a Friday.
Pattern B — Regulated Enterprise. K8s multi-node, SAML to Okta, session recording to S3, egress lockdown via NetworkPolicy, cosign-signed images only. Config differentiator: session_recording: true and admission webhooks that reject unsigned images. Honest cost: this is a two-engineer platform, minimum.
Pattern C — Training Labs. Burst concurrency, cheap spot nodes, reset-on-logout. The story you’ll hear: a student runs a cryptominer inside a workspace. Prevent it with CPU limits per container, egress deny-by-default, and a monitoring alert on sustained high CPU. Config differentiator: reset_on_logout: true and hard --cpus limits.
Common Mistakes and How to Avoid Them
Section titled “Common Mistakes and How to Avoid Them”-
Mounting
docker.sockinto a workspace. This is root on the host, full stop. If a workspace needs to build containers, use a rootless builder or a dedicated build service. There is no safe version of this. -
Leaving default admin credentials. The installer logs a generated password. Rotate it the same day. Better: disable local auth entirely once SSO works.
-
Persistent profiles without per-user scoping. A shared home directory is a shared filesystem. Scope by user ID, or don’t persist at all.
-
Unrestricted egress. Workspaces on the flat LAN can reach your domain controllers, your database tier, and your backup server. Deny by default.
-
Untested backups. A
pg_dumpthat has never been restored is not a backup, it’s a hope. Schedule monthly restore drills.
Q: Is KASM a replacement for Citrix or VMware Horizon?
For most knowledge-worker and developer use cases, yes. For GPU-heavy CAD with certified drivers, or workloads requiring hardware-level isolation for compliance, not yet. Evaluate against your actual workload profile, not the marketing comparison.
Q: Can I run Windows apps in KASM?
Windows workspaces are supported via Windows Server containers or full Windows VMs managed by KASM, but the licensing and image management story is more complex than Linux. For pure Windows app delivery, traditional RDS or AVD is often still the pragmatic choice.
Q: What’s the real cost per user vs VDI?
Open-source KASM core is free. Your cost is infrastructure plus the labor to run it. In testing, a well-run KASM deployment lands at roughly 20–40% of equivalent VDI licensing costs for comparable workloads, but that assumes you have the engineering capacity to operate it. If you don’t, the savings evaporate.
Q: Does KASM work air-gapped?
Yes, with planning. You need an internal registry, offline copies of the workspace images, and a way to deliver IdP metadata without internet. The installer has an offline mode; check the official docs for the current procedure as it changes between versions.
Q: How does KASM compare to Apache Guacamole?
Guacamole is a protocol gateway — it proxies RDP, VNC, and SSH to existing systems. KASM is a workspace platform — it manages the lifecycle of the containers themselves. They’re complementary, not competing. Some shops run Guacamole in front of legacy RDP targets and KASM for new containerized workloads.
The Opinionated Takeaway
Section titled “The Opinionated Takeaway”Use KASM when you have ephemeral or semi-persistent workloads, a team that can run Linux infrastructure, and a real identity provider. Walk away when you need hardware isolation, certified GPU drivers, or you don’t have anyone who can debug a WebSocket handshake.
Three things will bite you: persistence drift, network egress, and untested backups. Fix all three before you go to production.
Containers don’t make you secure. Discipline does.