<!-- https://zunderlabs.com/docs/deploy/ssh · Markdown version of the page -->

# One SSH command

Install Guard on a Linux server with one SSH command that checks the installer's signature first, then sets it up with your rules. The key is typed on the server, never here.

:::note[Planned]
No installer is published yet. The file names and URLs are the plan for Guard 1.0.
:::

One command from your computer: it connects to your server, installs Guard after checking its signature, and walks you through the setup there. Your key is typed into the server, with hidden input, and never into a command line, a URL or this page.

With the default rules (the page fills in your own, and asks where Guard runs):

```sh
ssh -t you@your-server "curl -fsSL https://zunderlabs.com/i | sh -s -- --rules zr1_eyJ2IjoxLCJtYXhMZXZlcmFnZSI6NSwibWF4TG9zc0F0U3RvcFBjdCI6Miwic3RvcFBvbGljeSI6ImF0dGFjaCIsImRlZmF1bHRTdG9wRGlzdGFuY2VQY3QiOjIsIm1pbkxpcURpc3RhbmNlUGN0IjoxMCwibWF4UG9zaXRpb25QY3QiOjIwMCwibWF4T3BlblJpc2tQY3QiOjYsImRhaWx5TG9zc1N0b3BQY3QiOjYsImRyYXdkb3duSGFsdFBjdCI6MjUsIm1hcmtldHMiOlsiKiJdfQ"
```

The API wallet key is typed on the server into Guard's hidden prompt; it is never part of a command.

## What the command does

1. **The loader** (`zunderlabs.com/i`, 37 lines of `sh`) downloads `install.sh`, `SHA256SUMS` and the Sigstore bundle of that release. It verifies the bundle (signed by Guard's release workflow on GitHub, for exactly that tag) and checks `install.sh` against the signed checksums. A mismatch stops it before anything runs. Without `cosign` on the server it fetches a pinned `cosign` and checks its SHA-256 first.
2. **The installer** checks again: the signature, then the release archive for the server's architecture (x86-64 or ARM64) against the signed checksums, and only then extracts it.
3. It creates a system user `zunder-guard` with no login shell and a state directory readable only by that user, and installs the binary.
4. **The setup** is Guard's own (`zunder-guard init --interactive`): it shows your rules in plain words and asks to keep or edit them, then the account address and the mode. Paper is the default. Mainnet must be typed in full, followed by the account again.
5. **For testnet or mainnet** it asks for the API wallet key with hidden input. The key is read, never echoed, never put on a command line or into the environment, checked with `zunder-guard key check`, and stored with `systemd-creds`: encrypted, and decrypted only for the service when it starts. On systemd older than 250 it falls back to a file readable only by Guard's user, and says so.
6. It installs the hardened systemd unit, bound to `127.0.0.1:8547`, starts it, and prints the client key for your bot (once).

It opens no port. Running the same command again reconfigures Guard; it asks before replacing its configuration, and the journal is kept.

## Prefer to verify by hand

Skip the loader and run the same checks yourself, on the server (needs `cosign`):

```sh
V=v1.0.0; R=https://github.com/zunderlabs/zunder-guard/releases/download/$V
curl -fsSLO "$R/install.sh" -O "$R/SHA256SUMS" -O "$R/SHA256SUMS.sigstore.json"
cosign verify-blob --bundle SHA256SUMS.sigstore.json \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity "https://github.com/zunderlabs/zunder-guard/.github/workflows/release.yml@refs/tags/$V" \
  SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS && sh install.sh --rules zr1_…
```

`Verified OK` means the checksums were signed by Guard's release workflow on GitHub, for that tag. Then read `install.sh`; it is short on purpose. More: [Verify a release](https://zunderlabs.com/docs/deploy/verify).

## Without a terminal

Cloud-init, CI or `ssh` without `-t` give the installer no terminal, so it refuses, unless you pass `--non-interactive` with every value: `--rules`, `--network`, and for testnet or mainnet `--account` and `--key-file` (a file the installer reads, never the key itself on the command line).
