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