From 8351e2cc56a32b3140e3c66f0ff061768f54aa5e Mon Sep 17 00:00:00 2001 From: Worph Date: Sun, 4 Oct 2026 09:33:47 +0000 Subject: [PATCH] IPCR Forge: bridge 1.0.0 and DAC_READ_SEARCH so IPCR can import from Gitea Found on a fresh install (demostaging1): the Forge pinned bridge 0.1.0, which predates IPCR_AUTH_FILE, so IPCR's read-only Gitea token was never written. And ipcr-gateway (root, cap_drop ALL) could not read the token anyway: it is 0600, owned by $PUID, in a 0700 folder. Bump both bridge refs to 1.0.0 and give the gateway back DAC_READ_SEARCH (read-only), documented in rationale.md. --- Apps/IPCR-Forge/docker-compose.yml | 9 +++++++-- Apps/IPCR-Forge/rationale.md | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Apps/IPCR-Forge/docker-compose.yml b/Apps/IPCR-Forge/docker-compose.yml index 1b764c8..50152ca 100644 --- a/Apps/IPCR-Forge/docker-compose.yml +++ b/Apps/IPCR-Forge/docker-compose.yml @@ -331,7 +331,7 @@ services: # Verbatim from Apps/Radicle-Gitea-Bridge; only its data folder moves to $AppID/bridge/. radicle-gitea-bridge: - image: ghcr.io/worph/radicle-gitea-bridge:0.1.0 + image: ghcr.io/worph/radicle-gitea-bridge:1.0.0 container_name: radicle-gitea-bridge user: $PUID:$PGID # Reads two HTTP APIs and runs git. Needs no capability at all. @@ -386,6 +386,11 @@ services: user: "0:0" # Root needs no capabilities to write root-owned files it owns; drop them all. cap_drop: [ALL] + # Forge only: one back, read-only. The import token below is written by bridge-credentials as + # $PUID, mode 0600, in a 0700 folder; a root with no capabilities is bound by those modes like + # anyone else (verified: "permission denied"). DAC_READ_SEARCH bypasses read and search checks + # only, never write, and only on what this container mounts. See rationale.md. + cap_add: [DAC_READ_SEARCH] security_opt: ["no-new-privileges:true"] restart: unless-stopped command: ["gateway"] @@ -757,7 +762,7 @@ x-compose-app: chmod 600 /runner/token when: absent:/DATA/AppData/$AppID/gitea/runner/.runner - name: bridge-credentials - image: ghcr.io/worph/radicle-gitea-bridge:0.1.0 + image: ghcr.io/worph/radicle-gitea-bridge:1.0.0 user: $PUID:$PGID phase: post_up network: $APP_NET diff --git a/Apps/IPCR-Forge/rationale.md b/Apps/IPCR-Forge/rationale.md index 17c6cb9..eba68af 100644 --- a/Apps/IPCR-Forge/rationale.md +++ b/Apps/IPCR-Forge/rationale.md @@ -25,6 +25,8 @@ bundle adds. 3. **A public landing page.** `ipcr-forge-` is not behind the SSO gate. 4. **It replaces four apps** and cannot be installed beside them: same container names, domains and host ports. +5. **One added capability.** `ipcr-gateway` gets `cap_add: [DAC_READ_SEARCH]` on top of the + standalone listing's `cap_drop: [ALL]`. ## Why it is necessary @@ -55,8 +57,21 @@ Renaming every container, domain and port to coexist would break the defaults th and IPCR rely on (`radicle-api:8080`, `gitea:3000`) and the documented addresses users type, for no use case: running two copies of the same forge on one server. +**5. `DAC_READ_SEARCH`.** The gateway runs as root (for the CA it drops into the host's Docker +trust store) with every capability dropped, and a root with no capabilities is bound by file +modes like any other user. The import token is written by the bridge's install step as `$PUID`, +mode `0600`, in a `0700` folder, so without the capability the gateway cannot open it (verified +on a fresh install: `open /data/secrets/registry-auth: permission denied`, then `401` from +Gitea). `DAC_READ_SEARCH` restores exactly the missing half: reading files and traversing +directories regardless of their modes. + ## Security mitigations in place +- **The added capability is read-only and narrow.** `DAC_READ_SEARCH` bypasses read and + directory-search checks only. It grants no write, ownership or privilege change, and reaches + only what the gateway mounts: its own TLS folder and state, the token (mounted read-only) and + the Docker trust store it already writes to. + - **The network link is narrow.** `gitea-internal` holds only `gitea`, `gitea-runner` and now `ipcr-gateway`. The gateway's only listener is the read-only registry API. Kubo's unauthenticated RPC API stays on `ipcr-internal`, which nothing from Gitea joins. Jobs run