From ba052736be57ee07e87b7d95e9a3389ad622950b Mon Sep 17 00:00:00 2001 From: archipelago Date: Fri, 7 Aug 2026 20:46:17 -0400 Subject: [PATCH] docs(app-developer-guide): describe the security rules the parser actually enforces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Narrative pass. The Security Requirements section described a blocklist where the code enforces an allow-list, and attributed enforcement to the wrong layer: - **"Forbidden: mounting system paths /, /etc, /var, /usr, /proc, /sys"** — the real rule (`manifest.rs:1290-1313`) is the inverse: `volumes[].source` must be absolute and under `/var/lib/archipelago/`, or a plain named volume, or one of two reviewed exceptions (`/run/user/1000/podman/podman.sock`, `/var/run/dbus`). Anything else is a parse error. The old wording also listed `/var` as forbidden while every app in the repo binds `/var/lib/archipelago/` — a developer reading it would not know where their own data goes. - **"enforced by the marketplace/catalog pipeline and the node"** — split by layer instead. The capability allow-list is parser-enforced (verified against the 9 entries at `manifest.rs:1089-1099`); `:latest` is NOT — only `validate-app-manifest.sh` checks it, and a `:latest` manifest still installs. readonly_root / no_new_privileges / network_policy=isolated are parser defaults, so omitting them is safe rather than dangerous. Also: - `derived_env` documented `HOST_IP`/`HOST_MDNS`/`DISK_GB` "such as"; the set is closed and includes a fourth, `{{BITCOIN_HOST}}`. Noted that unknown placeholders pass through verbatim rather than erroring, so a typo silently ships `{{FOO}}` into the container. - The networking example hardcoded `bitcoin-knots`; `{{BITCOIN_HOST}}` resolves to knots or core depending on what's installed. - Documented the `files[].content` placeholder set, which is a different set from derived_env and wasn't mentioned at all — notably `{{NETWORK_GATEWAY}}` (the nginx `resolver` fix for post-restart 502s) and `{{secret:NAME}}`. - The "check the UI" URL `/app/my-app/` is not a route; it's `/dashboard/apps/:id` (detail) or `/dashboard/app-session/:appId` (embed). Co-Authored-By: Claude Opus 5 (1M context) --- docs/app-developer-guide.md | 56 +++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/docs/app-developer-guide.md b/docs/app-developer-guide.md index 11378008..d0dce5b7 100644 --- a/docs/app-developer-guide.md +++ b/docs/app-developer-guide.md @@ -109,7 +109,7 @@ app: | `app.container.pull_policy` | Pull behavior, usually `if-not-present` | | `app.container.network` | Podman network setting such as `archy-net` or `pasta`; dangerous namespace-sharing modes are rejected | | `app.container.entrypoint` / `custom_args` | Entrypoint and command override | -| `app.container.derived_env` | Environment values rendered from allowed host facts such as `HOST_IP`, `HOST_MDNS`, and `DISK_GB` | +| `app.container.derived_env` | Environment values rendered from host facts. The complete placeholder set is `{{HOST_IP}}`, `{{HOST_MDNS}}`, `{{DISK_GB}}`, `{{BITCOIN_HOST}}` — anything else is left in the value verbatim rather than erroring, so a typo ships a literal `{{FOO}}` to your container | | `app.container.secret_env` | Environment values read from `/var/lib/archipelago/secrets/`, injected as podman secrets (never visible in `podman inspect` or unit files) | | `app.container.generated_secrets` | Secrets the orchestrator creates on first use (`hex16`/`hex32`/`base64`/`bcrypt`) — self-healing, 0600, no host provisioning | | `app.container.generated_certs` | Self-signed TLS certs materialised before create; CN/SANs rendered from host facts | @@ -206,15 +206,28 @@ metadata: ## Security Requirements -These are enforced by the marketplace/catalog pipeline and the node. Non-compliant apps are flagged. +Two different things enforce these, and it's worth knowing which is which: + +- **The Rust parser (`core/container/src/manifest.rs`)** is the hard gate. A + manifest that violates one of its rules fails to parse, so the app cannot be + installed at all. +- **`scripts/validate-app-manifest.sh`** is the submission preflight. It applies + the *policy* rules the parser doesn't encode, and grades them `fail`/`warn`. ### Mandatory -1. **No `:latest` tag** — Pin a specific version: `myapp:1.0.0` -2. **Read-only root filesystem** — `security.readonly_root: true` (use volumes for writable data) -3. **No privilege escalation** — `security.no_new_privileges: true` -4. **Minimal capabilities** — Drop all caps, only add required ones -5. **No host network unless explicitly approved** — keep `security.network_policy` isolated or bridge +1. **No `:latest` tag** — Pin a specific version: `myapp:1.0.0`. Checked by the + preflight script (a `fail` for new apps, a `warn` for existing manifests being + migrated), **not** by the parser — a `:latest` manifest still installs, so + pinning is on you. +2. **Read-only root filesystem** — `security.readonly_root: true` (use volumes + for writable data). This is the parser's default when you omit it. +3. **No privilege escalation** — `security.no_new_privileges: true`. Also the + parser's default when omitted. +4. **Minimal capabilities** — Drop all caps, only add required ones. The + allow-list below *is* parser-enforced: anything outside it is a parse error. +5. **No host network unless explicitly approved** — keep + `security.network_policy` isolated or bridge (`isolated` is the default). ### Allowed Capabilities @@ -234,9 +247,16 @@ The parser currently accepts this allow-list. Keep capability requests minimal; ### Forbidden - Namespace-sharing network modes such as `container:` or `ns:` -- Mounting system paths: `/`, `/etc`, `/var`, `/usr`, `/proc`, `/sys` -- `SYS_PTRACE`, privileged containers, Docker socket mounts, or rootful execution -- Hardcoded secrets in environment variables or images +- **Host bind mounts outside `/var/lib/archipelago/`.** This is an allow-list, + not a blocklist of "system paths": `volumes[].source` must be absolute and + start with `/var/lib/archipelago/`, or be a plain named volume (no slashes), + or be one of two reviewed exceptions (`/run/user/1000/podman/podman.sock`, + `/var/run/dbus`). `..` anywhere in the path is rejected. Everything else fails + to parse, so your app's data belongs under `/var/lib/archipelago/` +- Capabilities outside the allow-list above — including `SYS_PTRACE` +- Privileged containers or rootful execution +- Hardcoded secrets in environment variables or images — use `secret_env` or + `generated_secrets` ## Container Best Practices @@ -264,6 +284,15 @@ files: Use `overwrite: false` for first-run defaults that users or the app may later modify. Use `overwrite: true` only for generated files the platform must own. +`files[].content` supports its own placeholder set — a different one from +`derived_env`: + +| Placeholder | Renders to | +|---|---| +| `{{HOST_IP}}` / `{{HOST_MDNS}}` | Host facts (`hostname -I` / the node's `.local` name) | +| `{{NETWORK_GATEWAY}}` | The gateway of the app's Podman network, i.e. aardvark's DNS address. Use it as an nginx `resolver` so container names re-resolve per request instead of pinning a stale IP and 502-ing after a restart | +| `{{secret:NAME}}` | The trimmed contents of the `0600` secret `NAME` from the service-owned secrets dir. `NAME` must be a bare filename. Never logged | + ### Health Checks Define a health check endpoint in your container: @@ -292,11 +321,14 @@ container: network: archy-net derived_env: - key: BITCOIN_RPC_HOST - template: bitcoin-knots + template: "{{BITCOIN_HOST}}" # resolves to whichever Bitcoin app is installed - key: BITCOIN_RPC_PORT template: "8332" ``` +Prefer `{{BITCOIN_HOST}}` over hardcoding `bitcoin-knots` — a node may be running +Bitcoin Core instead, and the placeholder resolves to whichever is present. + The `archy-net` Podman network provides DNS resolution between containers. Use `derived_env` for host facts like `HOST_MDNS` instead of hardcoding node-specific URLs. ## Catalog Generation @@ -386,7 +418,7 @@ podman logs my-app curl -b cookies.txt -X POST http://archipelago.local/rpc/v1 \ -d '{"method":"container-list"}' ``` -3. Check the UI at `http://archipelago.local/app/my-app/` +3. Check the UI. The app's detail page is `http://archipelago.local/dashboard/apps/my-app`; the embedded launch surface is `http://archipelago.local/dashboard/app-session/my-app` ### Validate Manifest