docs(app-developer-guide): describe the security rules the parser actually enforces

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/<id>` — 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) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-07 20:46:17 -04:00
co-authored by Claude Opus 5
parent ff3b3c860e
commit ba052736be
+44 -12
View File
@@ -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/<secret_file>`, 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:<name>` or `ns:<path>`
- 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/<app-id>`
- 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