# Mint a token and connect remotely

> Put shard serve and an HTTPS proxy in front of a daemon, mint an API token, and drive the sandboxes of that host from another machine.

Source: https://useshards.com/docs/essentials/remote-access/

You'll mint a token on a shard server, connect to it from another machine, and run a sandbox there as if it ran locally.

You need shell access to a server you set up with `shard setup`: see [Quickstart: server](https://useshards.com/docs/quickstart/server/). Steps 2 and 3 below add the HTTP API and HTTPS, if the server has neither yet.

**CLI**

```sh
# On the server: mint a token for this client.
# Prints JSON, and its token field is the client's API key.
shard tokens mint --name laptop --duration 720h \
  --scopes sandbox:read,sandbox:write,sandbox:delete,exec \
  --signing-key-file /etc/shard/serve.secret

# On the client: set the key, then check and save the connection.
export SHARD_API_KEY=<the token field>
shard setup --remote https://shard.example.com --save -y

# Every command now goes to the server. The exec prints hello.
shard create --name demo --memory 512MiB alpine:3.20
shard exec demo echo hello
shard remove --force demo

# On the server: revoke the token. Prints revoked 1 token(s) named laptop.
shard tokens revoke --name laptop --signing-key-file /etc/shard/serve.secret
```

**TypeScript**

```ts
import { Shard } from "useshards";

// Mint the token on the server first, as in the CLI tab.
// Reads the token from SHARD_API_KEY.
const shard = new Shard({ remote: "https://shard.example.com" });
try {
  // Create a sandbox on the server.
  const sandbox = await shard.create({
    name: "demo",
    image: "alpine:3.20",
    memoryMiB: 512,
  });

  // Prints hello.
  console.log((await sandbox.exec(["echo", "hello"])).stdout);

  // Remove it.
  await sandbox.remove({ force: true });
} finally {
  shard.close();
}
```

**Python**

```python
from useshards import Shard

# Mint the token on the server first, as in the CLI tab.
# Reads the token from SHARD_API_KEY.
with Shard(remote="https://shard.example.com") as shard:
    # Create a sandbox on the server.
    sandbox = shard.create("alpine:3.20", name="demo", memory_mib=512)

    # Prints hello.
    print(sandbox.exec(["echo", "hello"]).stdout)

    # Remove it.
    sandbox.remove(force=True)
```

On a Mac server, leave out `--signing-key-file`: the token commands and `shard serve` both use the default key.

`shard setup --remote` checks that it can reach the server, that the server accepts the key, and which commands the server supports. With `--save`, it saves the connection, so later commands need no variables.

The server needs three things before a client can connect: a daemon, `shard serve`, and an HTTPS proxy or a tunnel in front of it. The steps below set them up on a Linux host with systemd.

## 1. Set up the daemon

Set up the host to run sandboxes, as in [Quickstart: server](https://useshards.com/docs/quickstart/server/):

```sh
shard setup --local --provider gvisor --start-at-boot=true -y
```

The daemon serves its API on a unix socket on the host, never on TCP. This step does not expose it to the network.

## 2. Set up the HTTP API

`shard serve` is the HTTP API, and the only way in from the network. It is a separate process that does not run as root. It checks the key of each request, then passes the request to the daemon's socket. Setup runs it as a background service:

```sh
shard setup --local --provider gvisor --http-api=true -y
```

On a host where setup already ran, this changes only the HTTP API. On a new host, it sets up the daemon too, so step 1 is optional. Without `-y`, setup asks **Set up the HTTP API?** after the daemon startup question.

Setup then:

1. Checks the address, the port, the permissions and the service manager, and shows the configuration. Nothing changes before this.
2. Creates the `shard` account, the token signing key in `/etc/shard/serve.secret`, and an API key.
3. Starts the service. It starts at boot after the daemon, stops with it, and starts again with it. API requests fail while the daemon is stopped.
4. Sends an authenticated request to prove the API works.

- **The address:** `127.0.0.1:7850` by default, which accepts connections only from the host. Pass `--listen` with a VPN address, such as `--listen 100.64.0.5:7850`, to permit direct access through that private network.
- **The API key:** in `/etc/shard/api-key`, which only root can read. Setup shows the file, never the key. A repeated setup keeps the key, and `--replace-api-key` replaces it, which cuts off every client that uses the old key.
- **On a Mac:** the key is `/var/lib/shard/auth/api-key`, and the service is the LaunchDaemon `/Library/LaunchDaemons/shard.serve.plist`.

Read the key on the server:

```sh
cat /etc/shard/api-key
```

## 3. Put HTTPS in front

`shard serve` has no TLS, and setup does not configure HTTPS. For one client, an SSH tunnel is enough. Run this on the client, then use `http://127.0.0.1:7850` as the server URL:

```sh
ssh -N -L 7850:127.0.0.1:7850 <user>@<server>
```

For more clients, put a proxy or a tunnel in front of it that gives clients an `https` URL on port 443. The proxy must:

- **Pass WebSocket upgrades through, and not buffer responses.** Otherwise `shard exec`, `shard shell` and `shard logs -f` stall.
- **Not log the `Authorization` header.** That header carries the token.

**Caddy**

```text
shard.example.com {
	reverse_proxy 127.0.0.1:7850 {
		flush_interval -1
	}
}
```

Caddy gets a certificate from a public CA for a public name. On a host with no public name, add `tls internal` inside the site block, and give clients the root certificate of Caddy's own CA in `SHARD_CA_FILE`.

**Cloudflare Tunnel**

```yaml
tunnel: <tunnel id>
credentials-file: /etc/cloudflared/<tunnel id>.json
ingress:
  - hostname: shard.example.com
    service: http://127.0.0.1:7850
  - service: http_status:404
```

A tunnel needs no inbound port open on the host.

**Tailscale Serve**

```sh
tailscale serve --bg --https=443 http://127.0.0.1:7850
```

Clients on the same tailnet use `https://<host>.<tailnet>.ts.net`.

`shard serve` sees every client as one source: the proxy. It holds at most 32 connections from one source that have not yet shown a valid token, so all clients share this limit. Rate-limit floods at the proxy.

## 4. Mint a token

Mint one token for each client, on the server:

```sh
shard tokens mint --name laptop --duration 720h \
  --scopes sandbox:read,sandbox:write,sandbox:delete,exec \
  --signing-key-file /etc/shard/serve.secret
```

```text
{"token":"<jwt>","expires_at":"2026-11-04T18:00:00Z","scopes":["sandbox:read","sandbox:write","sandbox:delete","exec"]}
```

Setup's own key, `shard-setup`, can do everything and never expires. Mint a narrower token for each client instead.

The `token` field is the client's API key. Give `shard tokens mint`, `list` and `revoke` the same `--signing-key-file`, so the token lands in the ledger that `shard serve` reads.

Without `--scopes`, a token can do everything, secrets and policies included. Without `--duration`, it never expires. [Authentication](https://useshards.com/docs/reference/auth/#scopes) lists the scopes and what each one allows.

## 5. Connect a client

Set the key in `SHARD_API_KEY`, never as a command argument. Then save the connection:

```sh
shard setup --remote https://shard.example.com --save -y
```

Setup writes the server URL and the key to `~/.config/shard/config.json` by default, and only your user can read the file.

- **To connect without saving anything,** set `SHARD_REMOTE` and `SHARD_API_KEY` instead. The SDKs read the same two variables, or take `remote` and `apiKey` (`remote=` and `api_key=` in Python). They never read the saved connection.
- **If a private CA signed the server's certificate,** set `SHARD_CA_FILE` to that CA's certificate. shard always verifies the certificate, and has no option to skip it.
- **An `http` URL works too,** but nothing encrypts the key or the requests. Use it only on localhost or through an encrypted VPN. Each command then prints a warning to stderr.

[Environment and config](https://useshards.com/docs/reference/config/) describes `SHARD_REMOTE`, `SHARD_API_KEY` and `SHARD_CA_FILE`.

## Revoke a token

List the tokens, then revoke one by its ID, or every token of a name:

```sh
# Prints each token's ID, name, issue time, expiry, scopes and status.
shard tokens list --signing-key-file /etc/shard/serve.secret

# Revoke every token named laptop.
shard tokens revoke --name laptop --signing-key-file /etc/shard/serve.secret
```

`shard serve` refuses a revoked token on the next request, and needs no restart. It also closes any open stream that uses that token, such as `shard exec`, `shard shell` or `shard logs -f`, within a second. The command and its sandbox keep running.

## Commands that run on the server only

These commands refuse a remote server: `shard pull`, `shard image list`, `shard image remove`, `shard image prune`, `shard daemon`, `shard daemon status`, `shard info`, `shard serve`, and `shard tokens mint`, `list` and `revoke`. Run them on the server.

On a machine with a saved connection, add `--remote ""` to send one command to this machine's daemon:

```sh
shard --remote "" list
```

For how a request travels from a client to the daemon, see [Architecture](https://useshards.com/docs/concepts/architecture/#remote-access). For the socket's permissions, see [Security model](https://useshards.com/docs/security/model/#who-can-reach-the-api).
