Skip to content

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.

  • 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, not docker-compose)
  • Root or sudo access — KASM’s installer needs to write to /opt and 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 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.

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

DimensionLegacy VDIContainerized Workspace
Isolation boundaryHypervisor (hardware-assisted)Linux namespaces + cgroups (shared kernel)
Startup time30–90 seconds typical2–10 seconds typical
Density per host30–60 VMs100–400 containers depending on workload
Patch modelIn-place on gold image, drift-proneImmutable image rebuild
LicensingPer-seat or per-concurrent, vendor-lockedOpen source core, support optional
GPU passthroughMature, well-supportedImproving but workload-dependent

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:

Terminal window
docker run --rm -it -p 6901:6901 \
-e VNC_PW=change-me-now \
kasmweb/desktop:1.15.0

That 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.

[ 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.

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.

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:

Terminal window
# .env — chmod 600, owned by root, never in git
DATABASE_PASSWORD=generate-a-64-char-random-string-here
MANAGER_HOSTNAME=kasm.example.com
KASM_PORT=443

Standing 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.

/etc/sysctl.d/99-kasm-hardening.conf
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
kernel.unprivileged_userns_clone = 1
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
fs.protected_hardlinks = 1
fs.protected_symlinks = 1

Apply 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.

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.

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

DimensionDocker ComposeKubernetes (Helm)
ComplexityLow — one host, one compose fileHigh — cluster, ingress, storage classes
HANone (single point of failure)Yes, with proper topology
Upgrade pathStop, pull, startRolling or blue/green
Ops burdenYou and systemdA platform team
Best forPilots, [50 users, dev sandboxesRegulated, ]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.

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:

Terminal window
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.0

The --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.

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.0
USER root
RUN 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/.config
USER 1000:1000
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -sf http://localhost:6901/ || exit 1

Sign and scan every build. Not “when you have time” — every build:

Terminal window
cosign sign --key cosign.key registry.example.com/kasm/desktop:1.15.0-custom
trivy image --severity HIGH,CRITICAL --exit-code 1 \
registry.example.com/kasm/desktop:1.15.0-custom
cosign verify --key cosign.pub registry.example.com/kasm/desktop:1.15.0-custom

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/v1
kind: NetworkPolicy
metadata:
name: workspace-egress-deny
namespace: kasm-workspaces
spec:
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: 443

Comparison: Default vs Hardened Posture

DimensionDefault KASMHardened
CapabilitiesDocker default setDrop ALL, add minimum
FilesystemWritable rootfsRead-only + tmpfs
UserOften root in older imagesNon-root UID 1000
NetworkBridge, unrestricted egressDeny-by-default egress
Image provenanceUnsignedcosign-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.

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"
}
}

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 GroupKASM GroupAllowed Images
eng-backenddevelopersubuntu-dev, docker-cli
eng-frontenddevelopersubuntu-dev, node-toolchain
financestandardubuntu-desktop, office-suite
contractorsrestrictedubuntu-desktop (no persistence)

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 24224

FAQ: 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.

The day-two problems are what kill KASM deployments, not the initial install.

Every custom image goes through build → sign → scan → push. A GitHub Actions workflow that does all four:

name: build-kasm-image
on:
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 }}

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: 30s

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:

Terminal window
# Backup
pg_dump -h kasm_db -U kasmapp -Fc kasm > /backup/kasm-$(date +%F).dump
# Restore — run this in a test environment at least monthly
pg_restore -h test_db -U kasmapp -d kasm --clean --if-exists \
/backup/kasm-2026-09-01.dump

Comparison: Upgrade Strategies

StrategyDowntimeRiskComplexity
In-placeMinutes to hoursHigh — rollback is painfulLow
RollingNear-zeroMedium — version skewMedium
Blue/greenZeroLow — instant rollbackHigh

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.

  1. Mounting docker.sock into 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.

  2. Leaving default admin credentials. The installer logs a generated password. Rotate it the same day. Better: disable local auth entirely once SSO works.

  3. Persistent profiles without per-user scoping. A shared home directory is a shared filesystem. Scope by user ID, or don’t persist at all.

  4. Unrestricted egress. Workspaces on the flat LAN can reach your domain controllers, your database tier, and your backup server. Deny by default.

  5. Untested backups. A pg_dump that 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.

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.