Ephemeral instances for ssh and web#49
Open
Monster0506 wants to merge 89 commits into
Open
Conversation
Monster0506
marked this pull request as ready for review
July 23, 2026 01:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Below is written by copilot 👇 Take it with a grain of salt, let me know if you have any questions on anything :)
This pull request introduces several significant changes focused on modernizing the development environment, improving database schema management, and enhancing infrastructure for container orchestration and image registry. The major themes are migration to Bun as the main JavaScript runtime, database schema updates, and expanded Docker Compose orchestration.
New standalone service (
orchestrator.js+server.js, Bun) that owns the full lifecycle of both SSH and web challenge containers:CreateSSHInstance(uid, image_ref)picks a free port in the configured range, generates a random 20-char password, creates the container withAutoRemove: trueand a real expiry (khi.expires_atlabel + an in-processsetTimeoutthat self-stops the container), and reconciles on boot (ReconcileOnBoot()) so a restart of the orchestrator doesn't leave already-expired containers running forever.CreateWebInstance(challengeId, image_ref)resolves the image's exposed port automatically — readingConfig.ExposedPorts, or akhi.portlabel as an escape hatch for images (e.g. database base images) thatEXPOSEmore than one port across their layer history, which Docker doesn't let you undo. Fails loudly on ambiguity instead of guessing.createInstanceNetwork()/removeInstanceNetwork()), not a shared one — so containers can't reach each other's instances, and orphaned networks get swept on boot (cleanupOrphanedNetworks()).NanoCpus/Memory/PidsLimiton every container, defaulting from env vars but overridable per-image viakhi.pids_limit/khi.mem_byteslabels (getResourceLimits()) — needed once we hit real challenges (Puppeteer-based ones in particular) that legitimately need more headroom than the default.docker-socket-proxy, scoped indocker-compose.ymltoCONTAINERS, POST, IMAGES, NETWORKSonly — the orchestrator has no broader access to the host's Docker socket than it needs.Security hardening
CreateSSHInstanceandCreateWebInstancereject anyimage_refthat doesn't start with the configuredSSH_IMAGE_PREFIX/WEB_IMAGE_PREFIX(defaultkhi-ssh/,khi-web/) before ever touching Docker — an admin (or a compromised admin session) can't point an instance at an arbitrary image.resolveImage()tries a registry pull first (with basic-auth viaX-Registry-Auth), and falls back to a local image of the same name only if that specific pull fails — so dev/local testing doesn't require a registry at all, but production always prefers the authoritative signed source.ssh-challenge-wrapper/Dockerfile.wrapper,web-challenge-wrapper/Dockerfile.wrapper) that adds a realctf-playeruser rather than running challenges as root. The SSH wrapper preserves/etc/passwd's original file mode across theuseradd/adduserstep (captured before, restored after) since some base images ship non-default permissions there and silently loosening them would be its own vuln; it also startscron/crondand/opt/khi-daemonif either happens to be present in the base image, so challenge-specific background services still work under the wrapper.ncinstance or a full SSH instance —CreateInstancechecksGetActiveSSHInstanceand stops it before creating an nc instance, and the SSH path does the mirror check, so switching challenge types can't silently leave a second container running per player./api/cinstanceand/api/sshinstancenow take?cid=and only report a session asactiveif it belongs to the challenge being viewed (withother_activesurfaced separately) — previously a player's connection info could leak across challenges just by having any active instance.ReconcileInstances()(app-side, complementing the orchestrator's ownReconcileOnBoot) purgesssh_instance_sessions/web_instancesrows whose containers no longer actually exist, andEnsureWebInstanceself-heals viaisContainerAlive()before handing back a "your instance is running" response — fixes a real bug where a container that died (OOM-killed, crashed, manually removed) left an orphaned DB row that made the UI claim an instance was live when it wasn't.Docker registry (
registry.hacksu.com/dev.registry.hacksu.com)registry/docker-compose.yml: a self-hostedregistry:2behind htpasswd auth, credentials generated fromREGISTRY_USER/REGISTRY_PASSWORDat container start (refuses to start at all if either is unset), withREGISTRY_STORAGE_DELETE_ENABLED: trueso old/broken image tags can actually be deleted (registry GC needs this enabled to reclaim blob storage, otherwise deletes are rejected outright).registry.hacksu.com) and dev (dev.registry.hacksu.com) hosts with independent credentials, so challenge-authoring iteration never touches the real competition registry.src/lib/server/registry.ts+challenge.form.svelte) now pulls the image dropdown for both SSH and web challenge images directly from the registry's catalog API instead of free-text entry, with manual entry kept as a fallback if the registry is unreachable.CI / build pipeline (
kent-hack-it-challenges/.github/workflows/)Two
workflow_dispatchworkflows (build-and-push-ssh-image.yml,build-and-push-web-image.yml), both taking adockerfile_path(so CI builds one specific challenge, not the whole repo) and aslug:ssh-challenge-wrapper(adds sshd + non-root user, as above) before pushing.khi.portlabel or a singleEXPOSE, failing the build with a clear error otherwise), inspects whether the image already declares a non-rootUSERand only applies the wrapper if it doesn't (avoids double-wrapping images that already do the right thing).environmentchoice (dev/prod, defaulting todev) that switches the target registry host and pulls the matchingDEV_REGISTRY_USER/DEV_REGISTRY_PASSWORDorREGISTRY_USER/REGISTRY_PASSWORDsecrets accordingly — same workflow file, no duplication, and dev runs can't accidentally land in the prod registry.Admin UI
d.instances.svelte) covering both SSH and web instances together, with a chip-style type filter, Stop/Restart actions, and correct per-type ID/time-remaining display (previously SSH instances weren't visible in the admin view at all).DeleteChallengenow tears down any live instance for that challenge before removing the DB row, instead of leaving a running container with no owning challenge behind.Net effect
Players get isolated, resource-capped, time-boxed containers on their own network instead of sharing a fixed pool of processes; challenge images are built, hardened, and versioned through CI instead of hand-pushed; and the admin surface for managing all of this (instances, images, deletion) actually reflects reality instead of drifting from it.
Database Schema and Migrations
drizzle.config.tsto remove debug logging and setssl: falsefor the database connection.Docker Compose Orchestration Enhancements
docker-compose.ymlfor a Docker socket proxy, SSH orchestrator, and migration runner, with appropriate networks and environment variables for orchestration and image registry access.Infrastructure and Build Improvements
UI/Frontend Updates
src/app.css.inferAdditionalFieldsplugin for better type inference.Migration to Bun Runtime
Dockerfile,handler/Dockerfile, and related supervisor configs to use Bun for installing dependencies and running the server.README.mdandpackage.json.