docs(apps): document app capabilities and rootless CDI device nodes
- commit
- 714888a36578486f39054213a3d71d909c903b27
- author
- brice · · 2026-10-18 19:36 UTC
- committer
- brice · · 2026-10-18 20:33 UTC
- parent
- de8f68e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +99 -0
M docs/config/apps.md +3 -0
| @@ -32,6 +32,7 @@ image = "gordon.mydomain.com/blog:1.4.2" | |||
| 32 | 32 | command = ["node", "server.js"] # optional override | |
| 33 | 33 | stop_grace = "30s" # optional, default 30s | |
| 34 | 34 | devices = ["transcode-gpu"] # 0..n logical names from [app_devices.<name>] | |
| 35 | + | capabilities = ["vm-net"] # 0..n logical names from [app_capabilities.<name>] | |
| 35 | 36 | ||
| 36 | 37 | [services.web.readiness] # optional explicit readiness | |
| 37 | 38 | type = "http" | |
| @@ -86,6 +87,7 @@ services = ["web"] | |||
| 86 | 87 | - Service name: the `[services.<name>]` table key — `[a-z0-9_.-]`, max 63, quoted when it contains a dot. TOML keys are unique, so one table exists per name; names that share a runtime identifier after normalization (`web.api` and `web-api`) are rejected. | |
| 87 | 88 | - Bind name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service. | |
| 88 | 89 | - Device name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service. | |
| 90 | + | - Capability policy name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service. It names an `[app_capabilities.<name>]` policy, not a Linux capability: `capabilities = ["NET_ADMIN"]` is rejected. | |
| 89 | 91 | - Removing an app ends its incarnation: the name is freed, volumes and secrets are archived as retained under the old internal UUID, desired/active/intent state is cleared, and the next apply allocates a new UUID. A new app reusing the name never adopts the old secrets or volumes. | |
| 90 | 92 | ||
| 91 | 93 | ## Services | |
| @@ -135,6 +137,7 @@ A deploy recreates a service whose image digest, spec, environment, or secret va | |||
| 135 | 137 | - Named volumes are Gordon-owned app data: created, labeled, retained, backed up, and pruned by Gordon. Administrative binds are operator-owned host locations: Gordon mounts them and never creates, deletes, owns, backs up, or prunes them. | |
| 136 | 138 | - `[[services.<name>.bind]]` requires an absolute, normalized container `path` and an optional `readonly`. The policy's `read_only` and the bind's `readonly` force read-only together: either side wins and a manifest can never weaken its policy. Reserved destinations (`/`, `/proc`, `/sys`, `/dev`, `/boot` and their children) and paths colliding with a declared volume or another bind are rejected at apply time. | |
| 137 | 139 | - `devices` lists logical device names granted by `[app_devices.<name>]` policy the operator declares in `gordon.toml`; a device whose name has no matching policy, or whose policy does not allow the app+service pair, is rejected at apply time. Gordon resolves each name to explicit CDI IDs at activation time and encodes them as one native CDI `DeviceRequest`. Revisions persist the logical names, never the host resolution. A device add/remove shows as `service/<name>/devices` in diffs; reorder-only input is a no-op. Changing a mapping never recreates a running container: the next deploy serves the new resolution. Revoking a grant fails subsequent deploys closed while the running service is untouched. Device-bearing creates require Podman 5.4+ or Docker 28.3+ with native CDI configured; older or unrecognized engines return a structured `runtime-unsupported` error and never run without devices. Gordon installs no drivers, manages no quotas, and injects no `NVIDIA_*` environment: images carry their own runtime expectations. | |
| 140 | + | - `capabilities` lists logical capability-policy names granted by `[app_capabilities.<name>]` policy the operator declares in `gordon.toml`; a name with no matching policy, or whose policy does not allow the app+service pair, is rejected at apply time. Gordon resolves each name to Linux capabilities at activation time and adds them on top of its default capability set; `cap_drop = ALL`, `no-new-privileges`, and non-privileged mode always remain. Only a documented safe set of capabilities can be granted (never `ALL` or `SYS_ADMIN`; see [Security Hardening](security-hardening.md#administrative-app-capabilities)). Revisions persist the logical names, never the resolution. An add/remove shows as `service/<name>/capabilities` in diffs; reorder-only input is a no-op. Revoking a grant fails subsequent deploys, restarts, and recoveries closed while the running service is untouched. | |
| 138 | 141 | - A service with any bind runs as a single writer, like a volume-backed service: replacements never run two generations at once. `gordon apps status` and `apps show` mark such services `single-writer` (`restart_unsafe` in JSON); the mark is informational. See [apps status](../cli/apps.md#gordon-apps-status). | |
| 139 | 142 | - A volume declared `readonly = true` is mounted read-only in the container; the service cannot modify protected data. | |
| 140 | 143 | - `[[services.<name>.database]]` declares databases explicitly (no image inference). Only PostgreSQL is supported, and each database declares its own backup `schedule` (`hourly`, `daily`, `weekly`, or `monthly`). | |
| @@ -32,6 +32,7 @@ image = "gordon.mydomain.com/blog:1.4.2" | |||||
| 32 | command = ["node", "server.js"] # optional override | 32 | command = ["node", "server.js"] # optional override | ||
| 33 | stop_grace = "30s" # optional, default 30s | 33 | stop_grace = "30s" # optional, default 30s | ||
| 34 | devices = ["transcode-gpu"] # 0..n logical names from [app_devices.<name>] | 34 | devices = ["transcode-gpu"] # 0..n logical names from [app_devices.<name>] | ||
| 35 | + | capabilities = ["vm-net"] # 0..n logical names from [app_capabilities.<name>] | |||
| 35 | 36 | ||||
| 36 | [services.web.readiness] # optional explicit readiness | 37 | [services.web.readiness] # optional explicit readiness | ||
| 37 | type = "http" | 38 | type = "http" | ||
| @@ -86,6 +87,7 @@ services = ["web"] | |||||
| 86 | - Service name: the `[services.<name>]` table key — `[a-z0-9_.-]`, max 63, quoted when it contains a dot. TOML keys are unique, so one table exists per name; names that share a runtime identifier after normalization (`web.api` and `web-api`) are rejected. | 87 | - Service name: the `[services.<name>]` table key — `[a-z0-9_.-]`, max 63, quoted when it contains a dot. TOML keys are unique, so one table exists per name; names that share a runtime identifier after normalization (`web.api` and `web-api`) are rejected. | ||
| 87 | - Bind name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service. | 88 | - Bind name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service. | ||
| 88 | - Device name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service. | 89 | - Device name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service. | ||
| 90 | + | - Capability policy name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service. It names an `[app_capabilities.<name>]` policy, not a Linux capability: `capabilities = ["NET_ADMIN"]` is rejected. | |||
| 89 | - Removing an app ends its incarnation: the name is freed, volumes and secrets are archived as retained under the old internal UUID, desired/active/intent state is cleared, and the next apply allocates a new UUID. A new app reusing the name never adopts the old secrets or volumes. | 91 | - Removing an app ends its incarnation: the name is freed, volumes and secrets are archived as retained under the old internal UUID, desired/active/intent state is cleared, and the next apply allocates a new UUID. A new app reusing the name never adopts the old secrets or volumes. | ||
| 90 | 92 | ||||
| 91 | ## Services | 93 | ## Services | ||
| @@ -135,6 +137,7 @@ A deploy recreates a service whose image digest, spec, environment, or secret va | |||||
| 135 | - Named volumes are Gordon-owned app data: created, labeled, retained, backed up, and pruned by Gordon. Administrative binds are operator-owned host locations: Gordon mounts them and never creates, deletes, owns, backs up, or prunes them. | 137 | - Named volumes are Gordon-owned app data: created, labeled, retained, backed up, and pruned by Gordon. Administrative binds are operator-owned host locations: Gordon mounts them and never creates, deletes, owns, backs up, or prunes them. | ||
| 136 | - `[[services.<name>.bind]]` requires an absolute, normalized container `path` and an optional `readonly`. The policy's `read_only` and the bind's `readonly` force read-only together: either side wins and a manifest can never weaken its policy. Reserved destinations (`/`, `/proc`, `/sys`, `/dev`, `/boot` and their children) and paths colliding with a declared volume or another bind are rejected at apply time. | 138 | - `[[services.<name>.bind]]` requires an absolute, normalized container `path` and an optional `readonly`. The policy's `read_only` and the bind's `readonly` force read-only together: either side wins and a manifest can never weaken its policy. Reserved destinations (`/`, `/proc`, `/sys`, `/dev`, `/boot` and their children) and paths colliding with a declared volume or another bind are rejected at apply time. | ||
| 137 | - `devices` lists logical device names granted by `[app_devices.<name>]` policy the operator declares in `gordon.toml`; a device whose name has no matching policy, or whose policy does not allow the app+service pair, is rejected at apply time. Gordon resolves each name to explicit CDI IDs at activation time and encodes them as one native CDI `DeviceRequest`. Revisions persist the logical names, never the host resolution. A device add/remove shows as `service/<name>/devices` in diffs; reorder-only input is a no-op. Changing a mapping never recreates a running container: the next deploy serves the new resolution. Revoking a grant fails subsequent deploys closed while the running service is untouched. Device-bearing creates require Podman 5.4+ or Docker 28.3+ with native CDI configured; older or unrecognized engines return a structured `runtime-unsupported` error and never run without devices. Gordon installs no drivers, manages no quotas, and injects no `NVIDIA_*` environment: images carry their own runtime expectations. | 139 | - `devices` lists logical device names granted by `[app_devices.<name>]` policy the operator declares in `gordon.toml`; a device whose name has no matching policy, or whose policy does not allow the app+service pair, is rejected at apply time. Gordon resolves each name to explicit CDI IDs at activation time and encodes them as one native CDI `DeviceRequest`. Revisions persist the logical names, never the host resolution. A device add/remove shows as `service/<name>/devices` in diffs; reorder-only input is a no-op. Changing a mapping never recreates a running container: the next deploy serves the new resolution. Revoking a grant fails subsequent deploys closed while the running service is untouched. Device-bearing creates require Podman 5.4+ or Docker 28.3+ with native CDI configured; older or unrecognized engines return a structured `runtime-unsupported` error and never run without devices. Gordon installs no drivers, manages no quotas, and injects no `NVIDIA_*` environment: images carry their own runtime expectations. | ||
| 140 | + | - `capabilities` lists logical capability-policy names granted by `[app_capabilities.<name>]` policy the operator declares in `gordon.toml`; a name with no matching policy, or whose policy does not allow the app+service pair, is rejected at apply time. Gordon resolves each name to Linux capabilities at activation time and adds them on top of its default capability set; `cap_drop = ALL`, `no-new-privileges`, and non-privileged mode always remain. Only a documented safe set of capabilities can be granted (never `ALL` or `SYS_ADMIN`; see [Security Hardening](security-hardening.md#administrative-app-capabilities)). Revisions persist the logical names, never the resolution. An add/remove shows as `service/<name>/capabilities` in diffs; reorder-only input is a no-op. Revoking a grant fails subsequent deploys, restarts, and recoveries closed while the running service is untouched. | |||
| 138 | - A service with any bind runs as a single writer, like a volume-backed service: replacements never run two generations at once. `gordon apps status` and `apps show` mark such services `single-writer` (`restart_unsafe` in JSON); the mark is informational. See [apps status](../cli/apps.md#gordon-apps-status). | 141 | - A service with any bind runs as a single writer, like a volume-backed service: replacements never run two generations at once. `gordon apps status` and `apps show` mark such services `single-writer` (`restart_unsafe` in JSON); the mark is informational. See [apps status](../cli/apps.md#gordon-apps-status). | ||
| 139 | - A volume declared `readonly = true` is mounted read-only in the container; the service cannot modify protected data. | 142 | - A volume declared `readonly = true` is mounted read-only in the container; the service cannot modify protected data. | ||
| 140 | - `[[services.<name>.database]]` declares databases explicitly (no image inference). Only PostgreSQL is supported, and each database declares its own backup `schedule` (`hourly`, `daily`, `weekly`, or `monthly`). | 143 | - `[[services.<name>.database]]` declares databases explicitly (no image inference). Only PostgreSQL is supported, and each database declares its own backup `schedule` (`hourly`, `daily`, `weekly`, or `monthly`). | ||
M docs/config/reference.md +20 -0
| @@ -172,6 +172,23 @@ preserve = true # Keep volumes when containers are | |||
| 172 | 172 | # allowed_apps = ["video"] # Required; exact, non-empty | |
| 173 | 173 | # allowed_services = ["transcoder"] # Required; exact, non-empty | |
| 174 | 174 | ||
| 175 | + | # ============================================================================= | |
| 176 | + | # ADMINISTRATIVE APP CAPABILITIES | |
| 177 | + | # ============================================================================= | |
| 178 | + | # A named capability grant is the only way an app manifest may add Linux | |
| 179 | + | # capabilities. The manifest declares capabilities = ["<name>"] (a policy | |
| 180 | + | # name, not a capability). Granted capabilities are added on top of Gordon's | |
| 181 | + | # defaults; cap_drop = ALL, no-new-privileges, and non-privileged mode always | |
| 182 | + | # remain. Allowed names (no CAP_ prefix): AUDIT_WRITE, CHOWN, DAC_OVERRIDE, | |
| 183 | + | # FOWNER, FSETID, IPC_LOCK, KILL, MKNOD, NET_ADMIN, NET_BIND_SERVICE, | |
| 184 | + | # NET_RAW, SETFCAP, SETGID, SETPCAP, SETUID, SYS_CHROOT, SYS_NICE, | |
| 185 | + | # SYS_RESOURCE. ALL, SYS_ADMIN, SYS_PTRACE, SYS_MODULE and the rest are | |
| 186 | + | # rejected. allowed_apps and allowed_services are exact and non-empty. | |
| 187 | + | # [app_capabilities.vm-net] | |
| 188 | + | # add = ["NET_ADMIN"] # Required; allowed set only | |
| 189 | + | # allowed_apps = ["myapp"] # Required; exact, non-empty | |
| 190 | + | # allowed_services = ["worker"] # Required; exact, non-empty | |
| 191 | + | ||
| 175 | 192 | # REMOVED in v3: [routes], [attachments], [network_groups], | |
| 176 | 193 | # app-like [[services]], [service_routes], [auto_route], [previews]. | |
| 177 | 194 | # Declare apps in standalone files (see ./apps.md). The standalone | |
| @@ -324,6 +341,9 @@ keep_last = 3 # Keep N newest tags per repository | |||
| 324 | 341 | | `app_devices.<name>.cdi` | none | Required explicit, non-empty CDI device IDs granted under this logical name (no `=all` aggregate form) | | |
| 325 | 342 | | `app_devices.<name>.allowed_apps` | none | Required exact, non-empty app allowlist for this grant (no wildcard or empty-means-all form) | | |
| 326 | 343 | | `app_devices.<name>.allowed_services` | none | Required exact, non-empty service allowlist for this grant | | |
| 344 | + | | `app_capabilities.<name>.add` | none | Required explicit, non-empty Linux capability names (no `CAP_` prefix) added on top of Gordon's defaults. Only the documented safe set is accepted; `ALL` and `SYS_ADMIN` are rejected | | |
| 345 | + | | `app_capabilities.<name>.allowed_apps` | none | Required exact, non-empty app allowlist for this grant | | |
| 346 | + | | `app_capabilities.<name>.allowed_services` | none | Required exact, non-empty service allowlist for this grant | | |
| 327 | 347 | | `services[].name` | none | Standalone service name used by `service:<service>:<port-name>` traffic refs | | |
| 328 | 348 | | `services[].image` | none | Container image for enabled standalone services | | |
| 329 | 349 | | `services[].enabled` | `false` | Whether Gordon creates, starts, and reconciles the service container | | |
| @@ -172,6 +172,23 @@ preserve = true # Keep volumes when containers are | |||||
| 172 | # allowed_apps = ["video"] # Required; exact, non-empty | 172 | # allowed_apps = ["video"] # Required; exact, non-empty | ||
| 173 | # allowed_services = ["transcoder"] # Required; exact, non-empty | 173 | # allowed_services = ["transcoder"] # Required; exact, non-empty | ||
| 174 | 174 | ||||
| 175 | + | # ============================================================================= | |||
| 176 | + | # ADMINISTRATIVE APP CAPABILITIES | |||
| 177 | + | # ============================================================================= | |||
| 178 | + | # A named capability grant is the only way an app manifest may add Linux | |||
| 179 | + | # capabilities. The manifest declares capabilities = ["<name>"] (a policy | |||
| 180 | + | # name, not a capability). Granted capabilities are added on top of Gordon's | |||
| 181 | + | # defaults; cap_drop = ALL, no-new-privileges, and non-privileged mode always | |||
| 182 | + | # remain. Allowed names (no CAP_ prefix): AUDIT_WRITE, CHOWN, DAC_OVERRIDE, | |||
| 183 | + | # FOWNER, FSETID, IPC_LOCK, KILL, MKNOD, NET_ADMIN, NET_BIND_SERVICE, | |||
| 184 | + | # NET_RAW, SETFCAP, SETGID, SETPCAP, SETUID, SYS_CHROOT, SYS_NICE, | |||
| 185 | + | # SYS_RESOURCE. ALL, SYS_ADMIN, SYS_PTRACE, SYS_MODULE and the rest are | |||
| 186 | + | # rejected. allowed_apps and allowed_services are exact and non-empty. | |||
| 187 | + | # [app_capabilities.vm-net] | |||
| 188 | + | # add = ["NET_ADMIN"] # Required; allowed set only | |||
| 189 | + | # allowed_apps = ["myapp"] # Required; exact, non-empty | |||
| 190 | + | # allowed_services = ["worker"] # Required; exact, non-empty | |||
| 191 | + | ||||
| 175 | # REMOVED in v3: [routes], [attachments], [network_groups], | 192 | # REMOVED in v3: [routes], [attachments], [network_groups], | ||
| 176 | # app-like [[services]], [service_routes], [auto_route], [previews]. | 193 | # app-like [[services]], [service_routes], [auto_route], [previews]. | ||
| 177 | # Declare apps in standalone files (see ./apps.md). The standalone | 194 | # Declare apps in standalone files (see ./apps.md). The standalone | ||
| @@ -324,6 +341,9 @@ keep_last = 3 # Keep N newest tags per repository | |||||
| 324 | | `app_devices.<name>.cdi` | none | Required explicit, non-empty CDI device IDs granted under this logical name (no `=all` aggregate form) | | 341 | | `app_devices.<name>.cdi` | none | Required explicit, non-empty CDI device IDs granted under this logical name (no `=all` aggregate form) | | ||
| 325 | | `app_devices.<name>.allowed_apps` | none | Required exact, non-empty app allowlist for this grant (no wildcard or empty-means-all form) | | 342 | | `app_devices.<name>.allowed_apps` | none | Required exact, non-empty app allowlist for this grant (no wildcard or empty-means-all form) | | ||
| 326 | | `app_devices.<name>.allowed_services` | none | Required exact, non-empty service allowlist for this grant | | 343 | | `app_devices.<name>.allowed_services` | none | Required exact, non-empty service allowlist for this grant | | ||
| 344 | + | | `app_capabilities.<name>.add` | none | Required explicit, non-empty Linux capability names (no `CAP_` prefix) added on top of Gordon's defaults. Only the documented safe set is accepted; `ALL` and `SYS_ADMIN` are rejected | | |||
| 345 | + | | `app_capabilities.<name>.allowed_apps` | none | Required exact, non-empty app allowlist for this grant | | |||
| 346 | + | | `app_capabilities.<name>.allowed_services` | none | Required exact, non-empty service allowlist for this grant | | |||
| 327 | | `services[].name` | none | Standalone service name used by `service:<service>:<port-name>` traffic refs | | 347 | | `services[].name` | none | Standalone service name used by `service:<service>:<port-name>` traffic refs | | ||
| 328 | | `services[].image` | none | Container image for enabled standalone services | | 348 | | `services[].image` | none | Container image for enabled standalone services | | ||
| 329 | | `services[].enabled` | `false` | Whether Gordon creates, starts, and reconciles the service container | | 349 | | `services[].enabled` | `false` | Whether Gordon creates, starts, and reconciles the service container | | ||
M docs/config/security-hardening.md +59 -0
| @@ -83,6 +83,65 @@ The manifest references only the logical name: `devices = ["transcode-gpu"]`. | |||
| 83 | 83 | - Device-bearing creates require Podman 5.4+ or Docker 28.3+ with native CDI configured. Older or unrecognized engines fail with a structured `runtime-unsupported` error and never run without devices. | |
| 84 | 84 | - Gordon installs no drivers, manages no quotas, and injects no device environment: images carry their own runtime expectations. | |
| 85 | 85 | ||
| 86 | + | ### Plain device nodes under rootless Podman | |
| 87 | + | ||
| 88 | + | CDI is not GPU-specific. Any device node can be exposed through a user-level CDI spec, so `/dev/kvm` and `/dev/net/tun` reach a rootless Podman service without raw `/dev` paths in manifests and without any privilege: | |
| 89 | + | ||
| 90 | + | ```yaml | |
| 91 | + | # ~/.config/cdi/vm.yaml (rootless user, mode 0644) | |
| 92 | + | cdiVersion: "0.6.0" | |
| 93 | + | kind: example.com/vm | |
| 94 | + | devices: | |
| 95 | + | - name: kvm | |
| 96 | + | containerEdits: | |
| 97 | + | deviceNodes: | |
| 98 | + | - path: /dev/kvm | |
| 99 | + | - name: net-tun | |
| 100 | + | containerEdits: | |
| 101 | + | deviceNodes: | |
| 102 | + | - path: /dev/net/tun | |
| 103 | + | ``` | |
| 104 | + | ||
| 105 | + | ```toml | |
| 106 | + | # ~/.config/containers/containers.conf (the rootless user that runs the Podman API service) | |
| 107 | + | [engine] | |
| 108 | + | # Setting cdi_spec_dirs replaces the defaults: keep /etc/cdi and /var/run/cdi if you use them. | |
| 109 | + | cdi_spec_dirs = ["/etc/cdi", "/var/run/cdi", "/home/<user>/.config/cdi"] | |
| 110 | + | ``` | |
| 111 | + | ||
| 112 | + | ```toml | |
| 113 | + | # gordon.toml | |
| 114 | + | [app_devices.my-vm-device] | |
| 115 | + | cdi = ["example.com/vm=kvm", "example.com/vm=net-tun"] | |
| 116 | + | allowed_apps = ["myapp"] | |
| 117 | + | allowed_services = ["worker"] | |
| 118 | + | ``` | |
| 119 | + | ||
| 120 | + | - No Gordon change is needed (verified against the Podman 5.7 sources; confirm with a live create on your host). Gordon sends the IDs as a native `cdi` `DeviceRequest`; the Podman compat API forwards them to the same CDI injection as `podman run --device <id>`, which reads `cdi_spec_dirs` from the containers.conf of the user running the API service. Restart that service (`systemctl --user restart podman.socket podman.service`) after editing `containers.conf`; edits to the spec files themselves are picked up on the next container create. | |
| 121 | + | - Under rootless Podman the OCI runtime (crun or runc) cannot `mknod` in a user namespace and falls back to bind-mounting the host device node into the container. The node must therefore be accessible to the rootless user on the host: `/dev/kvm` and `/dev/net/tun` are usually `0666` or group-owned (`kvm`). Supplementary groups do not carry into the user namespace, so prefer a `0666`/ACL-based permission over group membership. | |
| 122 | + | - CDI device names Gordon accepts match `[A-Za-z0-9][A-Za-z0-9_.-]*` (so `kvm` and `net-tun` are valid; names containing `:` are not). | |
| 123 | + | - A missing spec dir entry, an unreadable spec, or an unknown device name surfaces as a redacted create failure (`runtime error redacted`); check the Podman service journal for the CDI error. | |
| 124 | + | ||
| 125 | + | ## Administrative app capabilities | |
| 126 | + | ||
| 127 | + | App manifests cannot add Linux capabilities. A grant exists only when the operator declares a named policy in `gordon.toml`: | |
| 128 | + | ||
| 129 | + | ```toml | |
| 130 | + | [app_capabilities.vm-net] | |
| 131 | + | add = ["NET_ADMIN"] | |
| 132 | + | allowed_apps = ["myapp"] # exact, non-empty | |
| 133 | + | allowed_services = ["worker"] # exact, non-empty | |
| 134 | + | ``` | |
| 135 | + | ||
| 136 | + | The manifest references only the logical name: `capabilities = ["vm-net"]`. | |
| 137 | + | ||
| 138 | + | - `add` holds explicit Linux capability names, written without the `CAP_` prefix. Only this allowed set is accepted: `AUDIT_WRITE`, `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `FSETID`, `IPC_LOCK`, `KILL`, `MKNOD`, `NET_ADMIN`, `NET_BIND_SERVICE`, `NET_RAW`, `SETFCAP`, `SETGID`, `SETPCAP`, `SETUID`, `SYS_CHROOT`, `SYS_NICE`, `SYS_RESOURCE`. Everything else is rejected when the config loads, including `ALL`, `SYS_ADMIN`, `SYS_PTRACE`, `SYS_MODULE`, `SYS_RAWIO`, `SYS_BOOT`, `SYS_TIME`, `DAC_READ_SEARCH`, `BPF`, `PERFMON`, `MAC_ADMIN`, `MAC_OVERRIDE`, `AUDIT_CONTROL`, and `SYSLOG`. These can reach the host or defeat container confinement; a workload that needs one is not an unprivileged service. | |
| 139 | + | - `allowed_apps` and `allowed_services` are exact, non-empty allowlists. There is no wildcard, prefix, or empty-means-all form. Unknown keys inside `[app_capabilities.<name>]` fail closed. | |
| 140 | + | - Granted capabilities are added on top of Gordon's default set (`CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID`, `SETGID`, `NET_BIND_SERVICE`). Gordon's `cap_drop = ALL` and `no-new-privileges` stay in force, and containers are never privileged. The runtime guard re-checks the resolved list before every create. | |
| 141 | + | - Capabilities apply inside the container's own user and network namespaces. Under rootless Podman, `NET_ADMIN` lets the service create TAP devices and nftables rules in its own network namespace, not on the host. Because `no-new-privileges` stays on, a process only holds the added capabilities when it runs as container root (uid 0 in the user namespace, which is an unprivileged host user under rootless Podman); a non-root container user cannot gain them through file capabilities. | |
| 142 | + | - Gordon resolves logical names at activation time. Revisions persist the logical names, never the resolution. The grant is re-resolved before every preflight, container create, restart, and recovery. Revoking a grant blocks future deploys and restarts while the running service is untouched. | |
| 143 | + | - Adding or removing a policy name shows as `service/<name>/capabilities` in diffs and recreates the container on the next deploy; reorder-only input is a no-op. Changing what a policy grants does not recreate a running container: the next deploy serves the new resolution. | |
| 144 | + | ||
| 86 | 145 | ## External image registries | |
| 87 | 146 | ||
| 88 | 147 | Docker Hub, `ghcr.io`, `quay.io`, and Gordon's configured registry are always allowed. Add every other registry hostname and non-default port explicitly: | |
| @@ -83,6 +83,65 @@ The manifest references only the logical name: `devices = ["transcode-gpu"]`. | |||||
| 83 | - Device-bearing creates require Podman 5.4+ or Docker 28.3+ with native CDI configured. Older or unrecognized engines fail with a structured `runtime-unsupported` error and never run without devices. | 83 | - Device-bearing creates require Podman 5.4+ or Docker 28.3+ with native CDI configured. Older or unrecognized engines fail with a structured `runtime-unsupported` error and never run without devices. | ||
| 84 | - Gordon installs no drivers, manages no quotas, and injects no device environment: images carry their own runtime expectations. | 84 | - Gordon installs no drivers, manages no quotas, and injects no device environment: images carry their own runtime expectations. | ||
| 85 | 85 | ||||
| 86 | + | ### Plain device nodes under rootless Podman | |||
| 87 | + | ||||
| 88 | + | CDI is not GPU-specific. Any device node can be exposed through a user-level CDI spec, so `/dev/kvm` and `/dev/net/tun` reach a rootless Podman service without raw `/dev` paths in manifests and without any privilege: | |||
| 89 | + | ||||
| 90 | + | ```yaml | |||
| 91 | + | # ~/.config/cdi/vm.yaml (rootless user, mode 0644) | |||
| 92 | + | cdiVersion: "0.6.0" | |||
| 93 | + | kind: example.com/vm | |||
| 94 | + | devices: | |||
| 95 | + | - name: kvm | |||
| 96 | + | containerEdits: | |||
| 97 | + | deviceNodes: | |||
| 98 | + | - path: /dev/kvm | |||
| 99 | + | - name: net-tun | |||
| 100 | + | containerEdits: | |||
| 101 | + | deviceNodes: | |||
| 102 | + | - path: /dev/net/tun | |||
| 103 | + | ``` | |||
| 104 | + | ||||
| 105 | + | ```toml | |||
| 106 | + | # ~/.config/containers/containers.conf (the rootless user that runs the Podman API service) | |||
| 107 | + | [engine] | |||
| 108 | + | # Setting cdi_spec_dirs replaces the defaults: keep /etc/cdi and /var/run/cdi if you use them. | |||
| 109 | + | cdi_spec_dirs = ["/etc/cdi", "/var/run/cdi", "/home/<user>/.config/cdi"] | |||
| 110 | + | ``` | |||
| 111 | + | ||||
| 112 | + | ```toml | |||
| 113 | + | # gordon.toml | |||
| 114 | + | [app_devices.my-vm-device] | |||
| 115 | + | cdi = ["example.com/vm=kvm", "example.com/vm=net-tun"] | |||
| 116 | + | allowed_apps = ["myapp"] | |||
| 117 | + | allowed_services = ["worker"] | |||
| 118 | + | ``` | |||
| 119 | + | ||||
| 120 | + | - No Gordon change is needed (verified against the Podman 5.7 sources; confirm with a live create on your host). Gordon sends the IDs as a native `cdi` `DeviceRequest`; the Podman compat API forwards them to the same CDI injection as `podman run --device <id>`, which reads `cdi_spec_dirs` from the containers.conf of the user running the API service. Restart that service (`systemctl --user restart podman.socket podman.service`) after editing `containers.conf`; edits to the spec files themselves are picked up on the next container create. | |||
| 121 | + | - Under rootless Podman the OCI runtime (crun or runc) cannot `mknod` in a user namespace and falls back to bind-mounting the host device node into the container. The node must therefore be accessible to the rootless user on the host: `/dev/kvm` and `/dev/net/tun` are usually `0666` or group-owned (`kvm`). Supplementary groups do not carry into the user namespace, so prefer a `0666`/ACL-based permission over group membership. | |||
| 122 | + | - CDI device names Gordon accepts match `[A-Za-z0-9][A-Za-z0-9_.-]*` (so `kvm` and `net-tun` are valid; names containing `:` are not). | |||
| 123 | + | - A missing spec dir entry, an unreadable spec, or an unknown device name surfaces as a redacted create failure (`runtime error redacted`); check the Podman service journal for the CDI error. | |||
| 124 | + | ||||
| 125 | + | ## Administrative app capabilities | |||
| 126 | + | ||||
| 127 | + | App manifests cannot add Linux capabilities. A grant exists only when the operator declares a named policy in `gordon.toml`: | |||
| 128 | + | ||||
| 129 | + | ```toml | |||
| 130 | + | [app_capabilities.vm-net] | |||
| 131 | + | add = ["NET_ADMIN"] | |||
| 132 | + | allowed_apps = ["myapp"] # exact, non-empty | |||
| 133 | + | allowed_services = ["worker"] # exact, non-empty | |||
| 134 | + | ``` | |||
| 135 | + | ||||
| 136 | + | The manifest references only the logical name: `capabilities = ["vm-net"]`. | |||
| 137 | + | ||||
| 138 | + | - `add` holds explicit Linux capability names, written without the `CAP_` prefix. Only this allowed set is accepted: `AUDIT_WRITE`, `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `FSETID`, `IPC_LOCK`, `KILL`, `MKNOD`, `NET_ADMIN`, `NET_BIND_SERVICE`, `NET_RAW`, `SETFCAP`, `SETGID`, `SETPCAP`, `SETUID`, `SYS_CHROOT`, `SYS_NICE`, `SYS_RESOURCE`. Everything else is rejected when the config loads, including `ALL`, `SYS_ADMIN`, `SYS_PTRACE`, `SYS_MODULE`, `SYS_RAWIO`, `SYS_BOOT`, `SYS_TIME`, `DAC_READ_SEARCH`, `BPF`, `PERFMON`, `MAC_ADMIN`, `MAC_OVERRIDE`, `AUDIT_CONTROL`, and `SYSLOG`. These can reach the host or defeat container confinement; a workload that needs one is not an unprivileged service. | |||
| 139 | + | - `allowed_apps` and `allowed_services` are exact, non-empty allowlists. There is no wildcard, prefix, or empty-means-all form. Unknown keys inside `[app_capabilities.<name>]` fail closed. | |||
| 140 | + | - Granted capabilities are added on top of Gordon's default set (`CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETUID`, `SETGID`, `NET_BIND_SERVICE`). Gordon's `cap_drop = ALL` and `no-new-privileges` stay in force, and containers are never privileged. The runtime guard re-checks the resolved list before every create. | |||
| 141 | + | - Capabilities apply inside the container's own user and network namespaces. Under rootless Podman, `NET_ADMIN` lets the service create TAP devices and nftables rules in its own network namespace, not on the host. Because `no-new-privileges` stays on, a process only holds the added capabilities when it runs as container root (uid 0 in the user namespace, which is an unprivileged host user under rootless Podman); a non-root container user cannot gain them through file capabilities. | |||
| 142 | + | - Gordon resolves logical names at activation time. Revisions persist the logical names, never the resolution. The grant is re-resolved before every preflight, container create, restart, and recovery. Revoking a grant blocks future deploys and restarts while the running service is untouched. | |||
| 143 | + | - Adding or removing a policy name shows as `service/<name>/capabilities` in diffs and recreates the container on the next deploy; reorder-only input is a no-op. Changing what a policy grants does not recreate a running container: the next deploy serves the new resolution. | |||
| 144 | + | ||||
| 86 | ## External image registries | 145 | ## External image registries | ||
| 87 | 146 | ||||
| 88 | Docker Hub, `ghcr.io`, `quay.io`, and Gordon's configured registry are always allowed. Add every other registry hostname and non-default port explicitly: | 147 | Docker Hub, `ghcr.io`, `quay.io`, and Gordon's configured registry are always allowed. Add every other registry hostname and non-default port explicitly: | ||
M gordon.toml.example +17 -0
| @@ -142,6 +142,23 @@ syslog_identifier = "gordon-access" | |||
| 142 | 142 | # allowed_apps = ["video"] # Required; exact, non-empty | |
| 143 | 143 | # allowed_services = ["transcoder"] # Required; exact, non-empty | |
| 144 | 144 | ||
| 145 | + | # ============================================================================= | |
| 146 | + | # ADMINISTRATIVE APP CAPABILITIES | |
| 147 | + | # ============================================================================= | |
| 148 | + | # A named capability grant is the only way an app manifest may add Linux | |
| 149 | + | # capabilities. The manifest declares capabilities = ["<name>"] (a policy | |
| 150 | + | # name, not a capability). Granted capabilities are added on top of Gordon's | |
| 151 | + | # defaults; cap_drop = ALL, no-new-privileges, and non-privileged mode always | |
| 152 | + | # remain. Allowed names (no CAP_ prefix): AUDIT_WRITE, CHOWN, DAC_OVERRIDE, | |
| 153 | + | # FOWNER, FSETID, IPC_LOCK, KILL, MKNOD, NET_ADMIN, NET_BIND_SERVICE, | |
| 154 | + | # NET_RAW, SETFCAP, SETGID, SETPCAP, SETUID, SYS_CHROOT, SYS_NICE, | |
| 155 | + | # SYS_RESOURCE. ALL, SYS_ADMIN, SYS_PTRACE, SYS_MODULE and the rest are | |
| 156 | + | # rejected. allowed_apps and allowed_services are exact and non-empty. | |
| 157 | + | # [app_capabilities.vm-net] | |
| 158 | + | # add = ["NET_ADMIN"] # Required; allowed set only | |
| 159 | + | # allowed_apps = ["myapp"] # Required; exact, non-empty | |
| 160 | + | # allowed_services = ["worker"] # Required; exact, non-empty | |
| 161 | + | ||
| 145 | 162 | [telemetry] | |
| 146 | 163 | enabled = false | |
| 147 | 164 | # endpoint = "http://localhost:4318" | |
| @@ -142,6 +142,23 @@ syslog_identifier = "gordon-access" | |||||
| 142 | # allowed_apps = ["video"] # Required; exact, non-empty | 142 | # allowed_apps = ["video"] # Required; exact, non-empty | ||
| 143 | # allowed_services = ["transcoder"] # Required; exact, non-empty | 143 | # allowed_services = ["transcoder"] # Required; exact, non-empty | ||
| 144 | 144 | ||||
| 145 | + | # ============================================================================= | |||
| 146 | + | # ADMINISTRATIVE APP CAPABILITIES | |||
| 147 | + | # ============================================================================= | |||
| 148 | + | # A named capability grant is the only way an app manifest may add Linux | |||
| 149 | + | # capabilities. The manifest declares capabilities = ["<name>"] (a policy | |||
| 150 | + | # name, not a capability). Granted capabilities are added on top of Gordon's | |||
| 151 | + | # defaults; cap_drop = ALL, no-new-privileges, and non-privileged mode always | |||
| 152 | + | # remain. Allowed names (no CAP_ prefix): AUDIT_WRITE, CHOWN, DAC_OVERRIDE, | |||
| 153 | + | # FOWNER, FSETID, IPC_LOCK, KILL, MKNOD, NET_ADMIN, NET_BIND_SERVICE, | |||
| 154 | + | # NET_RAW, SETFCAP, SETGID, SETPCAP, SETUID, SYS_CHROOT, SYS_NICE, | |||
| 155 | + | # SYS_RESOURCE. ALL, SYS_ADMIN, SYS_PTRACE, SYS_MODULE and the rest are | |||
| 156 | + | # rejected. allowed_apps and allowed_services are exact and non-empty. | |||
| 157 | + | # [app_capabilities.vm-net] | |||
| 158 | + | # add = ["NET_ADMIN"] # Required; allowed set only | |||
| 159 | + | # allowed_apps = ["myapp"] # Required; exact, non-empty | |||
| 160 | + | # allowed_services = ["worker"] # Required; exact, non-empty | |||
| 161 | + | ||||
| 145 | [telemetry] | 162 | [telemetry] | ||
| 146 | enabled = false | 163 | enabled = false | ||
| 147 | # endpoint = "http://localhost:4318" | 164 | # endpoint = "http://localhost:4318" | ||