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:
co-authored by
Claude Opus 5
parent
ff3b3c860e
commit
ba052736be
+44
-12
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user