detached 714888a

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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"
3232 command = ["node", "server.js"] # optional override
3333 stop_grace = "30s" # optional, default 30s
3434 devices = ["transcode-gpu"] # 0..n logical names from [app_devices.<name>]
35+capabilities = ["vm-net"] # 0..n logical names from [app_capabilities.<name>]
3536
3637 [services.web.readiness] # optional explicit readiness
3738 type = "http"
@@ -86,6 +87,7 @@ services = ["web"]
8687 - 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.
8788 - Bind name: `[a-z0-9_.-]`, max 63, must not contain `--`, unique within its service.
8889 - 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.
8991 - 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.
9092
9193 ## Services
@@ -135,6 +137,7 @@ A deploy recreates a service whose image digest, spec, environment, or secret va
135137 - 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.
136138 - `[[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.
137139 - `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.
138141 - 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).
139142 - A volume declared `readonly = true` is mounted read-only in the container; the service cannot modify protected data.
140143 - `[[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 override32 command = ["node", "server.js"] # optional override
33 stop_grace = "30s" # optional, default 30s33 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 readiness37 [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 ## Services93 ## 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
172172 # allowed_apps = ["video"] # Required; exact, non-empty
173173 # allowed_services = ["transcoder"] # Required; exact, non-empty
174174
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+
175192 # REMOVED in v3: [routes], [attachments], [network_groups],
176193 # app-like [[services]], [service_routes], [auto_route], [previews].
177194 # Declare apps in standalone files (see ./apps.md). The standalone
@@ -324,6 +341,9 @@ keep_last = 3 # Keep N newest tags per repository
324341 | `app_devices.<name>.cdi` | none | Required explicit, non-empty CDI device IDs granted under this logical name (no `=all` aggregate form) |
325342 | `app_devices.<name>.allowed_apps` | none | Required exact, non-empty app allowlist for this grant (no wildcard or empty-means-all form) |
326343 | `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 |
327347 | `services[].name` | none | Standalone service name used by `service:<service>:<port-name>` traffic refs |
328348 | `services[].image` | none | Container image for enabled standalone services |
329349 | `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-empty172 # allowed_apps = ["video"] # Required; exact, non-empty
173 # allowed_services = ["transcoder"] # Required; exact, non-empty173 # 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 standalone194 # 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"]`.
8383 - 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.
8484 - Gordon installs no drivers, manages no quotas, and injects no device environment: images carry their own runtime expectations.
8585
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+
86145 ## External image registries
87146
88147 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 registries145 ## 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"
142142 # allowed_apps = ["video"] # Required; exact, non-empty
143143 # allowed_services = ["transcoder"] # Required; exact, non-empty
144144
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+
145162 [telemetry]
146163 enabled = false
147164 # endpoint = "http://localhost:4318"
@@ -142,6 +142,23 @@ syslog_identifier = "gordon-access"
142 # allowed_apps = ["video"] # Required; exact, non-empty142 # allowed_apps = ["video"] # Required; exact, non-empty
143 # allowed_services = ["transcoder"] # Required; exact, non-empty143 # 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 = false163 enabled = false
147 # endpoint = "http://localhost:4318"164 # endpoint = "http://localhost:4318"

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close