# WolfPanel Agent — Release Guide

This document covers building, signing, and publishing a WolfPanel Agent
release. For rollback, see [ROLLBACK_GUIDE.md](ROLLBACK_GUIDE.md); for the
downloads-server layout and post-deploy commands, see
[DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md).

## 1. Generating the production signing key (one-time)

The release pipeline signs every tarball with an RSA-2048 key. Generate it
**once**, on a secure machine, and never commit it to any repository:

```bash
openssl genrsa -out release_private_key.pem 2048
openssl rsa -in release_private_key.pem -pubout -out release_public_key.pem
```

`release_public_key.pem`'s modulus/exponent are baked into `install.sh`,
`scripts/verify-release.sh`, and `src/update.py` (`PUBLIC_KEY_N` /
`PUBLIC_KEY_E`) — that's expected, public keys are meant to be public. If you
ever rotate the key, all three of those must be updated together, and it is a
breaking change for every agent already installed with the old public key
baked into its installer (they will refuse to trust packages signed by a new
key with a different public modulus). Rotate deliberately, not casually.

**The private key must never**:
- be committed to this or any other repository (`.gitignore` blocks
  `*.pem`, `*.key`, `private_key.pem`, `release_private_key*`, `*_private_key*`
  as a backstop, but the real control is "it never touches a git working
  tree in the first place")
- be logged, printed, or embedded in source (`scripts/sign_release.py` never
  prints its contents, and there is no mock/fallback signing path that could
  accidentally use a look-alike key)
- be the same key used for local/test signing (see §5 below)

## 2. Storing the production key

Pick one:

- **File on the release/build host**, root-only permissions:
  ```bash
  install -m 0600 -o root -g root release_private_key.pem /secure/path/release_private_key.pem
  ```
- **Secret manager** (Vault, SSM Parameter Store, etc.) injected as an
  environment variable at release time.

## 3. Running a release

Exactly one of these two environment variables must be set before running
any release script; there is no other supported way to sign a production
release, and no fallback if neither is set:

```bash
export RELEASE_PRIVATE_KEY_FILE="/secure/path/release_private_key.pem"
# or
export RELEASE_PRIVATE_KEY="$(cat release_private_key.pem)"
```

If neither is set, `scripts/build-release.sh` / `scripts/sign_release.py`
fail immediately with:

```
Release aborted: no production signing key configured.
```

Then run the full pipeline for a channel (`stable`, `beta`, or `dev`):

```bash
bash scripts/release.sh stable
```

This runs, in order:

1. `scripts/build-release.sh --channel stable` — builds a deterministic
   `wolfpanel-agent-<version>.tar.gz` from `VERSION` + the packaged files,
   computes its SHA-256 and byte size, signs it, and writes
   `dist/latest.json`.
2. `scripts/publish-release.sh stable` — copies the tarball, its checksum,
   and `latest.json` into `${PUBLIC_ROOT}/stable/` (from
   `deploy/release.conf`), refreshes the `latest.tar.gz` alias and
   `checksums.txt`, and publishes `install.sh`/`uninstall.sh`.
3. `scripts/verify-release.sh stable` — re-downloads nothing; it validates
   the **published** files in place: required files exist, `latest.json`'s
   fields are well-formed and match the requested channel, the tarball's
   size and SHA-256 match the manifest, and the signature verifies against
   the embedded production public key.

The individual steps can also be run on their own:

```bash
bash scripts/build-release.sh --channel stable
bash scripts/publish-release.sh stable
bash scripts/verify-release.sh stable
```

`build-release.sh` refuses to rebuild an already-built version unless
`--force`/`-f` is passed (one version maps to exactly one artifact hash).
Bump `VERSION` before building the next release.

## 4. The manifest (`latest.json`)

```json
{
  "manifest_version": 1,
  "channel": "stable",
  "version": "0.1.2",
  "file": "wolfpanel-agent-0.1.2.tar.gz",
  "sha256": "…",
  "signature": "…",
  "size_bytes": 123456,
  "published_at": "2026-07-05T00:00:00Z"
}
```

Rules enforced by both the installer and the agent's own update-check:

- There is **no `url` field**. The installer/agent always derive the
  download location themselves as `${RELEASE_BASE}/${CHANNEL}/${file}` —
  the manifest cannot redirect a client to an arbitrary host.
- `manifest_version` must be `1`; any other value (or a missing/corrupt one)
  is rejected outright.
- `channel` must equal the channel the client asked for. A `stable`
  installer that receives a manifest whose `channel` says `dev` (server
  misconfiguration, stale cache, or a MITM channel-mix attempt) refuses to
  install.
- `size_bytes` must equal the tarball's actual byte size; this is checked
  before spending time on SHA-256/signature verification, so a truncated
  download fails fast with a clear message.
- `published_at` is ISO-8601 UTC (`build-release.sh` generates it with
  `date -u +"%Y-%m-%dT%H:%M:%SZ"`).

## 5. Test/mock signing is a separate keypair

`src/update.py` also defines a **TEST-ONLY** RSA keypair
(`TEST_PUBLIC_KEY_N` / `TEST_PUBLIC_KEY_E` / `TEST_PRIVATE_KEY_D`), used only
when `WOLFPANEL_API_MOCK=1` (local dev runs and `pytest`). It:

- is completely unrelated to the production public key described in §1 —
  neither key can verify a signature made with the other,
- is never used by `scripts/sign_release.py` / `scripts/build-release.sh`
  (those only ever use the real production key, or fail),
- exists purely so `wolfpanel-agent update` can be exercised end-to-end
  locally without a network round-trip.

Do not reuse the test key for anything that reaches
`downloads.wolfpanel.net`, and do not reuse the production key for local
testing.

## 6. Verifying a deployment independently

```bash
bash scripts/verify-release.sh stable   # or beta / dev
```

See [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) for the exact commands to run
against the live downloads host after a deploy.
