Security: how ox hardens your VPS and isolates apps

How ox secures your VPS: SSH keys only, a firewall, fail2ban, each app as its own sandboxed user, signed updates, and an audit you can run any time.

Last updated 2026-10-25

On this page

When you add a server, ox locks it down before it joins your account. Each app then runs as its own user in a sandbox, and the server takes only updates signed with ox's release key. This page lists what ox sets, how to check it yourself, and what stays your job.

What ox sets on your server

  • SSH takes keys only. Root login and passwords are off, even when the cloud image turns passwords on, and weak MACs are off. Before it changes anything, ox checks that an SSH key from outside the server can still log in. If none can, it stops and says how to add yours. It also logs in again through the new settings before it keeps them, so a broken SSH setting cannot lock you out.
  • The users ox makes for your projects, its toolchains and its model downloads can never log in over SSH, even with a key in their home folder, so an app cannot open an SSH tunnel out of its sandbox. Your own login is not touched.
  • The ufw firewall refuses incoming connections and opens SSH, 80 and 443. Rules you had in ufw before ox, such as a port you opened yourself, stay open.
  • fail2ban bans addresses that keep guessing at SSH. The first ban lasts an hour, the next 4 hours, and each one after that a day. Trying a wrong key before the right one does not count. The firewall writes nothing to the log for the packets it blocks.
  • Ubuntu's security updates install themselves every day, and the audit checks that they ran. A kernel or library fix loads only after a restart. The server's page then says "Restart needed since" with what was updated, and you restart it with the Restart now button or ox servers restart. ox never restarts your server on its own and does not offer Livepatch.
  • io_uring is off for every process. This part of the kernel has a long record of privilege bugs, and no app ox runs needs it.
  • Your server writes a line to its own journal each time ox reads a variable's value, saves variables, deploys, backs up, runs a command, opens a backup, restores, or uses Explore. A deploy line names the repository and ref it was asked to build, and a backup line names the bucket. A line names the project and the keys, never a value, a query or a command. Read them with journalctl -u ox-agent -g 'security:'. The server also refuses more than 30 value reads a minute, and writes a line for each refusal.
  • Every site sends nosniff and a referrer policy, and HSTS once it has a domain, unless your app sets its own. Only the same site may frame its static files. There is no default Content-Security-Policy, because a wrong one breaks the app.

How your apps are kept apart

Each project runs as its own system user, in a systemd unit with no extra privileges. An app cannot see other users' processes, load kernel modules or trace other processes. It can write only its own directories. Only its own project and Caddy can reach its ports. It cannot read the cloud's metadata service, where a droplet keeps its setup data. Your variables and secrets sit in files only root and that project can read.

A process set to sandbox = "relaxed" in ox.toml can write wherever its user may. It keeps everything else: its own user, no extra privileges, hidden processes, the kernel limits, and a filter that refuses rarely used calls (swap, reboot, kernel modules, raw I/O, CPU emulation, old calls, bpf and perf).

Builds run as the same user, with the same limits on processes, the kernel and those calls. Each build step gets its own temporary folder that no other project can see. No unit or build step can write a core file, which would hold your secrets. One project cannot use up the server's processes and threads either: a project's units together stop at 40% of the host's limit, and all previews together at 20%. A project gets this limit at its next deploy.

Signed updates

ox updates the agent on your server between jobs. A new build first runs on ox's own servers for a day without trouble. If an updated agent does not answer ox within two minutes, the server goes back to the old build.

The agent installs only a build that carries two signatures: one from ox Cloud and one from ox's release key. The ox team keeps that key on its own machine, with one backup in a password manager, and never on ox Cloud or a server. So someone who took over ox Cloud could not put a program of their own on a server you already added. They could still run jobs on your server as your projects' users and read your projects' variables, as ox does to deploy. A server added while they held ox Cloud would run whatever install script they served.

The agent talks to ox only over HTTPS, and only outward: ox never connects in. It refuses any request ox did not sign, and any old or repeated one. It answers only ox's fixed list of requests. The one that runs a command you type, ox exec, runs as your app's own user in its sandbox, never as root. Only your account, with full access, can start one, and each one is in your audit log.

Check your server yourself

Before a server enrolls, ox reads back what it set and refuses to go on if any row fails. Run the same check on an enrolled server at any time; it changes nothing:

Security self-audit
sudo ox install --audit
The rows sudo ox install --audit prints on a server ox set up: each check, what it must be, and what the host has. The same table runs before a server enrolls, and any failed row stops the install.

It prints a pass or fail row for each setting: SSH root, password and key login, the firewall, fail2ban, automatic updates, the kernel's network and crash settings (redirects, source routes, multipath TCP off, setuid core dumps, Ubuntu's crash reporter off), the modes of ox's own folders and secret files, and that SSH turns ox's own users away. Two rows look for signs that something broke out of an app: an SSH key or a startup unit in a project's home folder, and a library forced into every program (/etc/ld.so.preload). ox reports these and deletes nothing: look at the file the row names, remove it if you did not put it there, then press Recheck. An up-to-date server also runs the audit every day, and its page shows any row that drifted, with the fix. The agent applies its kernel settings each time it starts, so a restart cannot undo them. If a kernel or hardening row stays failed on an older server, press Update on its page, then Recheck. When no update is offered, restarting the server does the same, but its projects are down for about a minute. The same goes for the row that keeps ox's own users out of SSH. Update does not change your SSH login settings. A failed row about root, passwords or keys means the SSH settings on the server changed since ox set them: set them back, then press Recheck.

sudo ox install --harden applies the settings by hand. It turns io_uring off, lets Redis fork for its snapshots (memory overcommit), writes the rules that keep apps apart and away from the cloud's metadata services, sets the kernel's network settings, keeps ox's own users out of SSH, writes the fail2ban settings, turns the firewall's logging off, and runs the audit. It leaves your SSH login settings, the firewall's rules and updates as they are.

Where your data lives

ox keeps only what it needs to run your account. Your app and its data stay on your server. These stay there:

  • Your code, builds and releases.
  • Your variables and secrets. ox shows them to you but keeps no copy.
  • Your databases, files and uploads.
  • Backups of your services. A copy goes to your own S3 bucket only if you add one.
  • Your app's logs. ox reads them only when you ask to see them.

Logs you open, rows you browse in Explore, and backups you download pass through ox to your screen. ox does not save them. With tested deploys on, ox also sends a pull request's text and changes, and the text of its preview's pages, to DigitalOcean's AI inference, and keeps the checks it writes and each run's results. The questions below list what ox keeps and for how long, and the privacy policy has every detail.

What stays your job

  • Your app's own code and dependencies. The sandbox limits what a bad dependency can reach, but it can still read that project's own data.
  • Who holds an SSH key. Anyone with a key for the operator account is root on the server.
  • The Add server command. Treat it like a password until the server shows up. It expires on its own.
  • Backups somewhere else. Keep copies in your own S3 bucket, as Backups and restore explains. Also copy what no backup holds, which What ox backs up lists.

If something here looks wrong, or you found a hole, write to the address on the privacy page. If a deploy fails after a hardening change, start with Troubleshooting, and see the CLI reference for the rest of ox.

Questions people ask

What does ox install on my server?

Here are the main pieces; sudo ox doctor --inventory on the server lists everything ox could own there.

  • The ox agent, /usr/local/bin/ox, run by ox-agent.service, with its settings in /etc/ox.
  • ox's own build of Caddy, /usr/local/bin/ox-caddy, for HTTPS. Its config is one JSON file, /etc/caddy/ox.json, not a Caddyfile, and it reads the address each app listens on from a one-line file in /etc/ox/upstreams.
  • Each app as its own Linux user, with systemd units in /etc/systemd/system whose names start with ox-<app>. Its cron jobs are systemd timers.
  • Your language versions through mise. The databases your apps use come as apt packages, such as PostgreSQL, Redis and MariaDB for MySQL, or as pinned builds through mise, such as ClickHouse and Qdrant.
  • Your code in /srv/ox. Backups and run logs in /var/lib/ox, which only root can open.
  • Your variables and secrets in env files in /var/lib/ox, which systemd hands to the app when it starts. ox keeps no copy.
  • An ox_user account with sudo for you to log in as, SSH with keys only, the ufw firewall, fail2ban, and Ubuntu's automatic security updates.
  • Network rules in /etc/ox/isolation.nft that let only Caddy and the app's own project reach its ports, loaded at boot by ox-isolation.service.
  • A 2 GB swap file, when the server has less than 8 GB of memory and no swap.

Of ox's own pieces, only the agent and Caddy run all the time; mise, git and pg_dump run only while a job needs them.

Ubuntu installs security updates every day, and ox checks daily that they ran. A kernel fix needs a restart: the server's page says when one is needed, and you choose when to restart, because ox never restarts a server on its own. What ox sets on your server lists every setting.

How hard is it to leave?

Your apps are plain systemd services behind Caddy, with their variables in env files, so they keep running without ox. If you only cancel ox Pro, every app stays up, and ox keeps deploying, backing up and watching your free project.

When you delete your account, the ox agent removes itself from each server, or, for a server that is offline or runs an old agent, the page gives you the one command to run there. Your apps, their data and their config stay as they are, and the agent leaves a note at /etc/ox/LEFT.txt that says where each app's code, variables, backups and passwords are. If ox itself shuts down, sudo ox leave on the server does the same with no help from ox's servers.

After that, the units, /etc/caddy/ox.json and the timers are plain files you can edit by hand. While ox manages the server, it rewrites them on each deploy, so change them through ox.toml.

Deploys, alerts and the daily backups stop with ox. The free test address stops too, because its name lives in ox's DNS: give each app your own domain before you leave. The backups run from ox's side, not from a cron on your server, so set up your own; the ones ox already made stay on the server, and PostgreSQL's are standard pg_dump files. Nothing updates ox's build of Caddy after you leave, while Ubuntu's security updates keep installing. The leave takes the free test addresses out of Caddy's config, and both the note in LEFT.txt and the leaving guide's section on keeping Caddy updated give the one line that switches Caddy to the packaged build. Then add Caddy's apt source, so apt upgrade updates Caddy: both point to it, and the section gives the steps from Caddy's install page. How to leave lists what to copy.

What does ox store, and what stays on my server?

ox keeps only what it needs to run your account. Your app and its data stay on your server.

ox keeps:

  • Your GitHub login, picture and email.
  • The servers and projects you add, and a short record of each run.
  • For a failed run, its last 40 lines of output, with secrets hidden.
  • Usage numbers for the charts: one-minute points for 24 hours, then 15-minute points for 30 days.
  • A log of who did what in your account, with the IP address it came from, kept for 12 months.
  • Your settings, like which alerts you want.
  • Your plan and its Dodo Payments subscription, never your card number.
  • Tokens and keys, encrypted or stored only as a hash.
  • Each browser you are signed in on, with its IP address, a short browser name, and when it signed in and was last active.
  • Sign-in codes for ox login, with the IP address that asked.
  • ox's own server logs, which can name your account and hold IP addresses, kept up to 5 weeks.
  • A copy of its own database every hour. The newest 24 copies stay on ox's own server, and the newest 168 copies (7 days) in a storage bucket ox owns.

Stays on your server:

  • Your code, builds and releases.
  • Your variables and secrets. ox shows them to you but keeps no copy.
  • Your databases, files and uploads.
  • Backups of your services. A copy goes to your own S3 bucket only if you add one.
  • Your app's logs. ox reads them only when you ask to see them.

The privacy policy has every detail, and Where your data lives says it in short.

Does my app's data ever leave my server?

Only to do what you ask. Logs you open, rows you browse in Explore, and backups you download pass through ox to your screen. Your variables pass through ox when you view or save them, and when ox copies them to a new staging copy or preview. A page that only lists them asks your server for the names, never the values, and your server keeps its own record of each time it hands them over. ox does not save them.

Your server also sends ox its health, its usage numbers and the output of each run while it runs. Your code goes from GitHub straight to your server, with a short-lived, read-only token for that one repo (a repo with private submodules also gets one for just those repos). Alert emails go out through Resend. You choose which: a failed run or backup, a deploy held by its tests, a crashing process, a site or server that stops answering, high memory or CPU, a nearly full disk, a server that drifts from its security setup, or a finished deploy. Each says what happened, and for a failed run the start of its error.

ox itself adds no analytics, ads or tracking scripts. Cloudflare, which sits in front of the site, adds a small security check script, as the privacy policy explains. ox never reads your app's data for its own use.