Skip to main content

Self-host on a cloud VM

A gateway on your own cloud VM is always on, reachable from anywhere, and costs a few dollars a month. Nothing in this guide needs a tunnel or a paid plan: you bring the machine and the domain, and your agents reach the gateway directly, with Permaura out of their path. If you enroll it to your account, the console and the Permaura app still reach the gateway through permaura.com, which relays their requests in memory and without logging them.

This is the do-it-yourself route. If you would rather not run a box, Pro turns the whole of this page into one click: a managed permaura.app tunnel for a gateway on your own machine, or a fully hosted gateway that Permaura runs for you. See plans.

What you will end up with

The gateway listening on 127.0.0.1 on the VM, a reverse proxy terminating TLS on a subdomain you own, port 7376 closed to the world, and the gateway restarting on its own after a reboot.

Before you start​

  • A Linux VM. Anything current works; the walkthrough was checked on Ubuntu 24.04 on a DigitalOcean droplet.
  • A subdomain you control, pointed at the VM's IP (gw.example.com in the examples). Use a subdomain rather than a path prefix: the gateway builds absolute URLs from its public URL, so a path prefix needs rewriting to work.
  • Root or sudo on the box.

1. Install the gateway​

SSH to the VM and, as root, run:

curl -fsSL https://permaura.com/install | sh

That does rather more than drop a binary. Run as root on a machine with systemd, which is the usual VPS case, it:

  • Installs permaura to /opt/permaura/bin, owned by root, after verifying the download's sha256 against a manifest that permaura.com checked against the pinned release key.
  • Gives the gateway its own permaura account and runs it as that account. Its store, vault key and operator token live in /var/lib/permaura, which only that account can read. What Permaura protects explains what that buys, and --no-service-user runs it as root in ~/.permaura instead.
  • Registers a systemd service so the gateway starts at boot, logging to the journal. Where systemd is absent (some containers and minimal images) it says so plainly rather than pretending.
  • Starts it on 127.0.0.1:7376 and waits until it actually answers.
  • Writes its settings to /etc/permaura/gateway.env, owned by root.
  • Ends by saying where the operator token is and how to read it, for adding the gateway to the console by hand, rather than printing the token.

Check it:

curl -s http://127.0.0.1:7376/health
sudo systemctl status permaura
sudo journalctl -u permaura -n 50

Run as an ordinary user instead, the installer puts everything in ~/.permaura (binary, settings, store, log) and runs the gateway as you, with a systemd user unit where one is available.

Your keys really are yours

On a headless install the gateway generates a per-install vault key on first run, and seals your credentials under it. Where the machine has a TPM the installer seals that key to it, in /etc/permaura/vault-kek.cred; otherwise it is an owner-read-only vault-kek file in the gateway's data directory. There is no shared default key and no key held by Permaura. A gateway that cannot resolve a real key refuses to start rather than falling back to something weaker.

2. Put TLS in front of it​

The gateway speaks plain HTTP and cannot terminate TLS. There is no inbound TLS in the binary at all, so https://your-host:7376 is not a thing that can work. Exposing port 7376 directly puts the operator token, and every agent call, on the wire in clear text.

The supported shape is a reverse proxy on your subdomain that terminates TLS and forwards to loopback. The gateway keeps its default 127.0.0.1 bind and is never exposed directly.

Caddy gets a certificate from Let's Encrypt and renews it with no further configuration. The entire config is:

/etc/caddy/Caddyfile
gw.example.com {
reverse_proxy 127.0.0.1:7376
}
sudo systemctl reload caddy

3. Tell the gateway its public URL​

A gateway only answers to hosts it recognizes. Out of the box that is 127.0.0.1, localhost and [::1] on its own port, and nothing else. This is a DNS-rebinding guard and it is worth having, but it means a request arriving as gw.example.com comes back 421 Misdirected Request until you say otherwise.

Edit /etc/permaura/gateway.env (sudoedit /etc/permaura/gateway.env), or ~/.permaura/gateway.env if the gateway runs as you:

/etc/permaura/gateway.env
PERMAURA_PUBLIC_URL=https://gw.example.com
PERMAURA_OWNER_SUB=<your Permaura user id>
sudo systemctl restart permaura
Never set PERMAURA_PUBLIC_URL on its own

PERMAURA_PUBLIC_URL does two things: it admits your hostname, and it switches the gateway into an OAuth resource server so remote agents can sign in through Permaura. Without PERMAURA_OWNER_SUB alongside it, the resource server has nothing to bind tokens to and fails closed, rejecting every remote token. The symptom is a gateway that is plainly reachable and refuses every agent. Set both, or neither.

Settings → Account does not show your user id. Enroll the gateway first (see below), then on Settings → Gateway turn on remote access with custom domain and your hostname: the console then lists the lines to set, PERMAURA_OWNER_SUB included.

Reachable, but no remote agent sign-in?

If you want the host admitted without turning on OAuth (a proxy that does its own authentication, say), use the narrower knob instead and leave PERMAURA_PUBLIC_URL unset:

PERMAURA_ALLOWED_HOSTS=gw.example.com

This admits the Host and nothing else. Do not reach for it as the default for a remote-agent setup: it leaves you with a reachable gateway that no agent can authenticate to. Note also that with no resource server configured, unauthenticated callers can still read the gateway's version and enumerate its tool list. They cannot invoke anything, which is refused without a valid session, but there is no reason to publish that surface.

Settings in gateway.env are read by both the installer and the systemd unit, so they survive a restart, a reboot and an --update, and it is the file to edit for everything on this page.

Since gateway 1.2.9 the binary also reads config.toml in its data directory (/var/lib/permaura, or ~/.permaura) directly, which it did not when this guide was written. The two overlap, and where they do the environment wins: gateway.env is loaded by the unit that starts the gateway, and the file only fills in settings the environment has not already set. So editing gateway.env still does what you expect, and a value you put in config.toml will look ignored if the same key is in gateway.env too.

config.toml is strict on purpose

An unrecognized key is a startup error, not a warning. That is deliberate (a typo like public_utl would otherwise leave the gateway on loopback with no resource server and nothing said about why), but it does mean a key from a newer gateway will stop an older one from starting. Check permaura --version against the setting you are adding.

4. Close the port​

A stock cloud VM often has no firewall running at all. On a fresh DigitalOcean droplet ufw is inactive and the iptables policy is ACCEPT, so anything you bind is immediately world-reachable.

Since the gateway is on loopback and the proxy is the only thing that talks to it, allow only SSH and HTTPS:

sudo ufw allow OpenSSH
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status

Port 7376 should never appear. If your provider has its own cloud firewall, set the same rules there.

5. Verify​

From your laptop, not the VM:

curl -s https://gw.example.com/health
curl -s -o /dev/null -w '%{http_code}\n' https://gw.example.com/v1/agents

You want {"status":"ok",...} from the first and 401 from the second. A 401 is the right answer: it means the gateway is reachable and is refusing unauthenticated callers.

curl -s -o /dev/null -w '%{http_code}\n' http://<vm-ip>:7376/health

That should fail to connect. If it answers, port 7376 is still open.

Managing it​

You have a shell on the box, and for a self-hosted gateway that is the primary way to work with it. The permaura CLI on the VM reads the operator token from the gateway's data directory and talks to the gateway over loopback. A gateway running as its own account is driven as that account, since nothing else can read the token:

alias permaura='sudo -u permaura /opt/permaura/bin/permaura'
permaura status
permaura agent add my-agent
permaura connection list
permaura audit --limit 20

If the gateway runs as you, permaura is already on your PATH and reads ~/.permaura/operator.token; skip the alias.

Some things can only be done on the box, by design. Three primitives mint or move authority with no credential, so they are refused to any caller that is not a loopback client on the gateway's own host:

  • opening a session (POST /v1/sessions)
  • graceful shutdown
  • pairing an approver device, and lowering the approval floor

A reachable gateway that accepted those would let anyone who can open a socket mint operator access, so the restriction is the point rather than a gap. Everything else is ordinary authenticated management and works from anywhere.

From gateway 1.3.44 four more are refused off the box until a device is paired: a vault export, an import that would replace or attach to a credential already there, a new connection that would take over a stored credential, and an edit that changes where an SSH connection's credential is sent or how it is presented. Your CLI on another machine counts as off the box, so run those on the server itself or pair a device first.

If you also enroll the gateway to a Permaura account, the console becomes a convenience layer over the same gateway: permaura.com relays console requests to it and signs each one with a short-lived credential minted for the gateway's owner (naming you as the person acting when that is someone else), which the gateway verifies itself. If one cannot be minted, the console shows an error rather than sending a stored operator token, and permaura.com keeps no copy of the operator token for a gateway with a public URL. No gateway token reaches your browser. Enroll it with a pairing code from Settings → Gateway, run as root:

curl -fsSL https://permaura.com/install | PERMAURA_PAIR_CODE=PGW-XXXXXXXXXX sh

The code goes in the environment rather than after --pair, where every account on the box could read it in the process list while the install runs. From a non-root shell, export PERMAURA_PAIR_CODE=PGW-XXXXXXXXXX first and pipe to sudo --preserve-env=PERMAURA_PAIR_CODE sh.

The console is optional. A gateway you never enroll still works, and still enforces everything.

Reboots​

The systemd unit the installer registers brings the gateway back after a reboot. Confirm it rather than assuming:

sudo systemctl is-enabled permaura
sudo reboot
# then, once it is back
curl -s http://127.0.0.1:7376/health

If the installer told you it could not register a service (no systemd), the gateway runs under nohup and will not come back. Install it on a machine with systemd, or supervise it yourself.

Updating​

A gateway running as its own permaura account, the default for a root install, updates by re-running the installer:

sudo sh -c "curl -fsSL https://permaura.com/install | sh -s -- --update"

That verifies the new binary against the signed release manifest, swaps it in, rewrites the systemd unit, and restarts the service on the same address and settings. Replacing the file is not enough on its own: the running process holds the old binary and keeps serving it until something restarts it, which is why the update does the restart rather than telling you to. permaura upgrade does not work here, because the binary belongs to root and the gateway's account cannot replace it. An install made before the binary and settings moved out of /var/lib/permaura is moved into this layout by the same command, keeping its vault key.

A gateway that runs as you updates itself with permaura upgrade, which does the same verification and restart. Use the installer form there too after any change to how the service itself is installed, since it rewrites the unit as well as the binary.

Neither will replace the gateway with an older release than the one installed. If going back is really what you want, add --allow-downgrade to the installer.

If you would rather not run a box​

Everything above is the price of owning the whole path. Permaura's paid plans exist to remove it:

  • A managed tunnel. Flip one switch and your gateway, wherever it runs, gets a stable permaura.app URL over a secure tunnel. No VM, no proxy, no certificate, no firewall, no DNS. See Remote access.
  • A hosted gateway. Permaura runs the gateway itself, so there is no machine in your life at all. See execution modes.

Both are one click from the console. This guide is here because self-hosting should be a real option rather than a lock-in point, not because it is the easy one.

A note on binding without a proxy​

You can bind a reachable interface directly:

curl -fsSL https://permaura.com/install | sh -s -- --host 0.0.0.0

The installer allows the machine's detected address in PERMAURA_ALLOWED_HOSTS for you, and warns you about what you just did.

Do not do this on the public internet. The gateway cannot terminate TLS, so the operator token and every agent call cross the network in clear text, readable and modifiable by anything on the path. It is a reasonable shape only on a network you already trust end to end, such as a private VPC, a Tailscale tailnet or a WireGuard network, where something else is providing the encryption. For anything reachable from the internet, use the reverse proxy above.