Security

What isolates your branch. And what doesn't yet.

Each branch runs in its own Firecracker micro-VM, with its own database inside. Here is what is in place, what we are still verifying, and what we don't promise.

Where things stand · 27 Sep 2026

Our runs so far used a development mode: the host daemon runs as an ordinary user, with no jailer and no per-VM firewall rules. The production install, with the jailer, is in place and waiting for a redeploy. Then we repeat the runs in production mode and run the escape tests. Until then, only our own team gets environments.

In place

  • One micro-VM per environment. Your app and its database run in a Firecracker VM, not a container on a shared kernel.
  • One database per environment. Each database (Postgres, MySQL or SQLite) is a copy-on-write clone of a template disk. Branches share no database server.
  • Secrets out of the repository. Values set with karts secret set are stored encrypted, reach only the processes that list them, and show as ‹secret› in logs.
  • Masked data is made on your machine. karts db snapshot masks personal data before anything leaves your machine, and the masked file is stored encrypted.
  • Checks before anything runs. A migration number another branch claimed, or a migration that drops or rewrites data, is refused before it reaches the database.
  • Revisions, not edits. A repeat karts up builds a new VM and database beside the old one, and switches when the new one is ready. A failed build leaves the running one alone.
  • HTTPS on every URL. With a Let's Encrypt certificate.

Being verified before other teams join

  • Jailer. Each VM under Firecracker's jailer: its own chroot, user id, seccomp filter and cgroup.
  • Network rules per VM. No route to other VMs, to the host's ports, to cloud metadata or to private ranges; mail port 25 blocked; inbound only from the proxy to the app's port.
  • Escape tests. Tests that try each of those from inside a VM, run on the host.
  • Separate sites for previews. Environment URLs move to a domain on the Public Suffix List, so a browser treats each environment as its own site. Until then, Karts creates environments only for teams it approves by hand.

Inside an environment

The VM holds your uploaded files, the dependencies, your app and its database. The app reaches the database inside the VM. Nothing outside the VM can connect to the database. A guest agent in the VM takes commands from the host over vsock, on connections the host opens.

The VM has no internet access unless your project turns it on. During a build, a proxy on the host fetches packages for it from the npm, PyPI, Go, RubyGems and Packagist registries and a fixed list of download hosts, and an HTTPS tunnel reaches public github.com and a few more fixed hosts while packages install. Nothing else, even with internet access on.

Your running app reaches the environments its karts.yml links to, over a private path that reaches only those. Unless your project turns on internet access, it reaches nothing else outside its VM.

Every environment is restored from the same snapshot, so each one needs its own randomness. In our measurements, all 110 restored clones reseeded the kernel's random number generator, and /dev/urandom differed in every one. Random state inside processes that were already running at snapshot time was copied, so Karts gives each clone a fresh seed, machine id and database password before your app starts.

Internet access

A project can let its running app reach the internet with internet: in karts.yml: named presets of API and sandbox hosts, hosts you list, or every public host. It is off unless the project turns it on, and build steps never get it. When it is on:

  • The app's connections leave through an exit server that Karts runs. They never go out directly from the machine that runs your VM.
  • The app still cannot reach other environments, the machine that runs its VM, or private and reserved addresses such as cloud metadata. Karts refuses IP addresses and its own domains, and checks the address each name resolves to before it connects.
  • Mail port 25 and DNS port 53 are always refused. Mail ports 465 and 587 are rate-limited.
  • Known production hosts, such as Plaid's, stay refused even when every other host is allowed. Reaching one needs the project's karts.yml to ask for it and a team owner to approve it. An approval lasts 7 days unless the owner sets another time, from 1 hour to 30 days. Removing it closes open connections at once.
  • karts up is refused when a secret, env: or a .env file holds a live Stripe secret key.
  • Each environment and each team has limits on connections and on bytes per day.
  • Karts logs every connection: the time, the environment, the host, address and port, the bytes each way, and whether it was allowed. Past a daily size, it sums them per hour instead. The log never holds what was sent or received. It is kept for up to 30 days, and your team reads it with karts egress log.
  • A team owner can turn the team's internet off at once with karts egress off.
  • Recordings of real calls are stored encrypted, per project, with your secrets' values and the keys, tokens, passwords, cookies and card numbers Karts recognises removed.

Environment URLs are public

  • Anyone with the URL can open it. There is no sign-in yet. Use seed data or a masked copy, never raw personal data.
  • The four-character suffix (feature-refunds-fc00) is not a secret and not a security control.
  • Each environment gets its own certificate, and certificates go into public Certificate Transparency logs. So the environment's name, which includes the branch name, is public. Keep branch names free of anything sensitive.

Your code and data

  • karts up uploads the working tree: tracked files that exist on disk, and untracked files git does not ignore. It never uploads .git/. karts up --commit uploads exactly one commit's tree instead.
  • Uploads are stored per project. Another project never sees them.
  • env: in karts.yml is for plain, non-secret configuration that you commit with the repository. Keys and passwords go in karts secret set.
  • Secret values are encrypted at rest. Logs and karts exec output show ‹secret› where a value would appear. Hiding works by exact match: anyone who can run karts exec in an environment can still read its secrets. So use test keys.
  • karts db snapshot reads your database read-only and masks it on your machine. The raw data never leaves it. The masked file is uploaded and stored encrypted. Masking follows rules; check its plan with --check.
  • karts up --local runs a branch on your own Mac and uploads nothing.
  • When an environment is torn down, its VM and database are destroyed. Nothing carries over to the next revision.

Fake outside services

With fakes:, Karts answers your app's calls to services such as Stripe, SendGrid and Google with test stand-ins.

  • The fakes never call the real services. They hand out test keys only.
  • To answer your app's HTTPS calls, each environment gets its own certificate. Only your app trusts it: inside the VM, or on your Mac only in the app's own processes. Karts never adds it to your Mac's keychain or system trust store, and its key is never printed.
  • Recorded calls have passwords, keys, tokens, cookies and card numbers removed before they are stored. They are deleted with the environment, and only your team can read them.
  • The test sign-in and checkout pages are public, like the app's own URL. They involve no real accounts and no real money.
  • On your Mac, an app or library that ignores HTTPS_PROXY can still reach the real internet. karts up --local warns you.

Accounts and tokens

  • karts login prints a code; you approve it in the browser. It works over SSH and on headless machines.
  • The CLI keeps its token in the OS keychain where there is one. For CI and agents, set KARTS_TOKEN.
  • karts logout revokes this device's token on the server, not just locally.
  • Every request checks that you belong to the team that owns the project. Another team's projects and environments answer "not found".

What we don't promise

  • No uptime or durability guarantee during early access. Environments are disposable. Each database is a copy, not a backup.
  • Migration-number claims stop collisions among branches that run karts up. Karts can't see branches that never ran it. So the CLI says "numbers claimed", not "safe".
  • The destructive-change check is a set of rules over your SQL. It catches dropped tables and columns, TRUNCATE, DELETE without WHERE, and type changes that can lose values. It does not prove a migration keeps every row.
  • The internet guards stop mistakes, not a determined team member. Karts reads the server name an HTTPS connection asks for, but does not decrypt it, so it cannot see which host a request inside names. The live-key check finds keys written out in full, not keys an app builds or fetches while it runs.
  • Karts Cloud runs on hosts we operate. You can't install it on your own servers yet; to talk about that, email hello@karts.kartikey.fyi.

Report a problem

Email hello@karts.kartikey.fyi with what you found and how to reproduce it. Don't test against other people's environments. For a bug in Karts that is not a security problem, karts report sends us a plain-text report.