diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 5ba423f..c828724 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -21,6 +21,12 @@ jobs: platform: linux arch: x64 bun-target: bun-linux-x64 + # Linux arm64: cross-compile from ubuntu-latest via Bun's --target flag. + # No Swift helper on Linux, so cross-compilation is straightforward. + - os: ubuntu-latest + platform: linux + arch: arm64 + bun-target: bun-linux-arm64 runs-on: ${{ matrix.os }} permissions: contents: write @@ -150,6 +156,11 @@ jobs: end on_linux do + on_arm do + url "https://github.com/ameshdev/amesh/releases/download/v${VERSION}/amesh-${VERSION}-linux-arm64.tar.gz" + sha256 "${{ steps.hashes.outputs.linux_arm64 }}" + end + on_intel do url "https://github.com/ameshdev/amesh/releases/download/v${VERSION}/amesh-${VERSION}-linux-x64.tar.gz" sha256 "${{ steps.hashes.outputs.linux_x64 }}" @@ -158,6 +169,7 @@ jobs: def install bin.install "amesh" + bin.install "amesh-agent" if File.exist?("amesh-agent") bin.install "amesh-se-helper" if File.exist?("amesh-se-helper") end diff --git a/docs/architecture-decisions.md b/docs/architecture-decisions.md index 9234f0f..f01387e 100644 --- a/docs/architecture-decisions.md +++ b/docs/architecture-decisions.md @@ -167,7 +167,13 @@ The controller CLI displays this code; the target CLI prompts the operator to en ## ADR-011: Remote shell in the CLI with explicit shell permission -**Decision:** The remote shell feature is part of `@authmesh/cli` — one package, one binary. `amesh shell` connects to a remote target. `amesh agent start` runs the daemon. Shell access requires explicit `amesh grant --shell` after pairing. +> **Status: Partially superseded (2026-04-05).** The single-package design was reversed: the agent daemon now ships in a separate `@authmesh/agent` package exposing an `amesh-agent` binary, while `@authmesh/cli` (`amesh`) keeps the controller-side commands (`init`, `list`, `invite`, `shell`, etc.) without the daemon. +> +> **Why the split:** the daemon uses `Bun.spawn({ terminal })` for PTY support, a Bun-only API. Bundling it with `@authmesh/cli` forced the entire controller install to depend on Bun even for users who only wanted `amesh init` + `amesh.fetch()`. Splitting lets `@authmesh/cli` ship a Node-compatible CLI and `@authmesh/agent` ship a Bun-dependent (or prebuilt-binary-only) daemon. The per-architecture prebuilt binaries are produced by the same release pipeline, so end users `brew install ameshdev/tap/amesh` to get both. +> +> **The security argument below is unchanged:** `amesh grant --shell` is still the real boundary, not the package boundary. The split is purely a runtime-dependency concern. + +**Original decision (superseded):** The remote shell feature is part of `@authmesh/cli` — one package, one binary. `amesh shell` connects to a remote target. `amesh agent start` runs the daemon. Shell access requires explicit `amesh grant --shell` after pairing. **Why:** @@ -175,7 +181,7 @@ The controller CLI displays this code; the target CLI prompts the operator to en 2. **Explicit consent:** Pairing for API authentication (`amesh invite`) does not grant shell access. A `permissions.shell` flag in the allow list defaults to `false`. The target admin must explicitly run `amesh grant --shell`. This is the security boundary, not the package boundary. -3. **The daemon is opt-in by invocation:** `amesh agent start` must be explicitly run. It doesn't auto-start, doesn't install as a service, and refuses to run as root without `--allow-root`. +3. **The daemon is opt-in by invocation:** `amesh-agent agent start` must be explicitly run. It doesn't auto-start, doesn't install as a service, and refuses to run as root without `--allow-root`. **Security design choices:** @@ -187,8 +193,8 @@ The controller CLI displays this code; the target CLI prompts the operator to en - **Root guard** — agent refuses `root` without `--allow-root` - **Per-controller session limits** — prevents DoS by authorized-but-misbehaving peers -**Rejected alternatives:** -- Separate `@authmesh/agent` package — adds install confusion without meaningful security benefit; the permission gate (`amesh grant --shell`) is the real security boundary, not the package boundary +**Rejected alternatives (at the time of the original decision):** +- ~~Separate `@authmesh/agent` package — adds install confusion without meaningful security benefit; the permission gate (`amesh grant --shell`) is the real security boundary, not the package boundary~~ *This was later reversed — see the Status note above. The runtime-dependency concern (Bun for PTY) outweighed the install-confusion concern once prebuilt binaries were shipped via the release pipeline.* - Auto-granting shell on pairing — violates principle of least privilege - Reusing pairing handshake's random-nonce encryption — birthday-bound risk over long sessions - Session resumption — complexity and nonce-reuse risk outweigh the latency benefit diff --git a/docs/remote-shell-spec.md b/docs/remote-shell-spec.md index 156a0f2..d25e32f 100644 --- a/docs/remote-shell-spec.md +++ b/docs/remote-shell-spec.md @@ -40,7 +40,7 @@ amesh already solves these problems for HTTP APIs. The remote shell extends the ``` Controller (laptop) Relay Target (server) ───────────────── ───── ─────────────── -amesh shell am_7f2e amesh agent (daemon) +amesh shell am_7f2e amesh-agent agent (daemon) │ │ │ │──── { type: 'shell', otc } ─────►│ │ │ │◄── { type: 'listen' } ────│ (agent is always connected) @@ -85,7 +85,7 @@ No new pairing ceremony is needed. If `amesh list` on the target shows the contr ## 5. Components -### 5.1 `amesh agent` (target-side daemon) +### 5.1 `amesh-agent agent` (target-side daemon) A long-running process on the target machine that: @@ -97,12 +97,14 @@ A long-running process on the target machine that: 6. Streams encrypted I/O between the PTY and the relay tunnel ```bash -amesh agent start # start daemon (foreground) -amesh agent start --daemon # start as background process -amesh agent stop # stop the daemon -amesh agent status # show running state + connected controllers +amesh-agent agent start # start daemon (foreground) +amesh-agent agent start --daemon # start as background process +amesh-agent agent stop # stop the daemon +amesh-agent agent status # show running state + connected controllers ``` +> **Note:** The daemon ships in a separate npm package (`@authmesh/agent`), installed on the target. The controller-side `amesh` command from `@authmesh/cli` does not include `agent start`. See [ADR: remote shell packaging](./architecture-decisions.md) for the rationale. + **Daemon lifecycle:** - Reconnects to relay on disconnect (exponential backoff: 1s, 2s, 4s, ..., max 30s) - Heartbeat every 30 seconds to keep WebSocket alive @@ -243,8 +245,8 @@ With a 12-byte incrementing nonce and the high-bit split, each side can send 2^9 ### Starting the agent (target) ``` -$ amesh agent start - amesh agent listening on relay.authmesh.dev +$ amesh-agent agent start + amesh-agent listening on relay.authmesh.dev Device: am_7f2e8a1b (prod-api) Authorized controllers: 2 @@ -280,7 +282,7 @@ $ echo $? ``` $ amesh shell prod-api Error: agent not connected for prod-api (am_7f2e8a1b). - Start the agent on the target: amesh agent start + Start the agent on the target: amesh-agent agent start ``` --- @@ -300,7 +302,7 @@ $ amesh shell prod-api - Tests: handshake succeeds for paired devices, fails for unknown devices ### Phase 3 — Target agent daemon -- `amesh agent start` command (oclif) +- `amesh-agent agent start` command (oclif, shipped via `@authmesh/agent`) - Persistent relay connection with reconnect - PTY spawning via `Bun.spawn({ terminal: ... })` - Encrypted I/O streaming (frame protocol) diff --git a/docs/why-amesh.md b/docs/why-amesh.md index 4d2f564..63b60bc 100644 --- a/docs/why-amesh.md +++ b/docs/why-amesh.md @@ -16,7 +16,7 @@ This model has a fundamental flaw: **the secret IS the identity.** Anyone who ha ### 1. Secrets leak constantly -GitHub's secret scanning detected over **1 million leaked secrets in public repos in 2024 alone.** This includes API keys, database credentials, and cloud provider tokens. These aren't just mistakes by juniors — Uber, Samsung, Toyota, and Twitch have all had major secret leaks. +GitHub's secret scanning detected over **1 million leaked secrets in public repos in 2024 alone.** This includes API keys, database credentials, and cloud provider tokens. These aren't hypothetical — real keys committed to real public repositories by real engineers at companies of every size. Every year, major breaches trace back to leaked credentials. These aren't junior mistakes; they're what happens when the design makes leaks easy. Common leak vectors: - `.env` committed to git (`.gitignore` missed or not set up) @@ -55,16 +55,18 @@ A Bearer token doesn't tell you *who* is calling. If three servers and a develop **amesh gives every machine a unique, verifiable identity.** When a request arrives, the server knows exactly which device sent it (`req.authMesh.deviceId`), its friendly name, and when it was verified. If one machine is compromised, you revoke it by device ID — the others are unaffected. -### 4. Secrets managers add complexity, not security +### 4. Secrets managers don't change the shape of the problem -Services like AWS Secrets Manager, HashiCorp Vault, and Doppler improve *management* of secrets but don't solve the fundamental problem: the secret still exists as a copyable string that must be fetched, held in memory, and sent over the wire. +Secrets managers improve the *management* of secrets, but they don't change the fundamental shape of the problem: the secret still exists as a copyable string that must be fetched, held in memory, and sent over the wire. A secrets manager: -- Adds a dependency (if Vault is down, your service can't authenticate) +- Adds a runtime dependency — if the manager is unreachable, your service can't authenticate - Still delivers the secret as a string to your application -- Requires its own authentication (how does your server authenticate to Secrets Manager? With... another secret) +- Requires its own authentication (how does your server authenticate to the secrets manager? With *another secret*, which has the same problem) - Adds latency on every cold start -- Costs money at scale +- Costs real money at scale + +Secrets managers are a genuine improvement over loose `.env` files and they solve a real problem. But the secret is still a string, and a string is still copyable. **amesh removes the secret entirely.** There is no string to manage, fetch, cache, or protect. The signing happens on the device. The only thing that crosses the wire is a cryptographic signature that is useless to an attacker — it's bound to a specific request, timestamp, and nonce. @@ -92,7 +94,7 @@ This matters for: | **Revocation** | Breaks everything using that key | Revokes one device. Others unaffected | | **Audit trail** | "Someone with this key called the API" | "Device am_8f3a (prod-api-east) called the API at 10:05:32" | | **Replay protection** | None (same token works forever) | Every request has a unique nonce + 30-second timestamp window | -| **What to protect** | .env files, CI variables, Vault access, Slack threads | Access to the device (same as SSH keys, passkeys) | +| **What to protect** | .env files, CI variables, secrets manager access, Slack threads | Access to the device (same as SSH keys, passkeys) | | **What you store in git** | Nothing (and hope you never accidentally do) | Everything. There are no secrets in the codebase | --- diff --git a/landpage/src/lib/blog.ts b/landpage/src/lib/blog.ts new file mode 100644 index 0000000..c309690 --- /dev/null +++ b/landpage/src/lib/blog.ts @@ -0,0 +1,48 @@ +export interface BlogPost { + slug: string; + title: string; + description: string; + date: string; + author: string; + readingTime: string; + tags: string[]; +} + +// Posts listed newest first. Individual post content lives in +// src/routes/blog/[slug]/+page.svelte — this file is for metadata only. +export const posts: BlogPost[] = [ + { + slug: 'introducing-amesh-0-3', + title: 'Introducing amesh 0.3', + description: + 'Auto-generated passphrases, verbose backend detection, a docs sidebar, and the security hardening that shipped across the 0.3.x line.', + date: '2026-04-04', + author: 'The amesh team', + readingTime: '5 min read', + tags: ['release', 'changelog'], + }, + { + slug: 'why-we-built-amesh', + title: 'Why we built amesh', + description: + "Static API keys are a broken model. Over 1 million were leaked on GitHub in 2024. Here's why we think device-bound identity is the only honest fix.", + date: '2026-04-01', + author: 'The amesh team', + readingTime: '7 min read', + tags: ['essay', 'security'], + }, +]; + +export function getPost(slug: string): BlogPost | undefined { + return posts.find((p) => p.slug === slug); +} + +export function getPostNav(slug: string): { prev?: BlogPost; next?: BlogPost } { + const idx = posts.findIndex((p) => p.slug === slug); + if (idx === -1) return {}; + return { + // posts[] is newest-first, so "next (newer)" is idx-1 and "prev (older)" is idx+1 + prev: idx < posts.length - 1 ? posts[idx + 1] : undefined, + next: idx > 0 ? posts[idx - 1] : undefined, + }; +} diff --git a/landpage/src/lib/components/DocsSidebar.svelte b/landpage/src/lib/components/DocsSidebar.svelte index c6d73b4..a96ec7e 100644 --- a/landpage/src/lib/components/DocsSidebar.svelte +++ b/landpage/src/lib/components/DocsSidebar.svelte @@ -1,6 +1,6 @@