Rename the bridge to the forge service; this repository is the add-on
- bridge/ → forge/, module and binary ipcr-forge, image ghcr.io/yundera/ipcr-forge
(was ipcr-forge-bridge); releases are plain vX.Y.Z tags (workflow image.yml)
- data folders $AppID/bridge/{state,secrets} → $AppID/forge/..., in-container /forge
- docs link to the engine (Yundera/ipcr) instead of relative paths; README
This commit is contained in:
@@ -10,15 +10,15 @@ name: ipcr-forge
|
||||
# → ipcr-kubo / ipcr-registry
|
||||
# docker pull ipcr.localhost:4767/ipns/<forge name>/<owner>/<repo>:1.2.3 (anyone)
|
||||
#
|
||||
# gitea ──system webhook──▶ ipcr-forge (bridge) ──rad init / git push rad──▶ radicle-node
|
||||
# gitea ──system webhook──▶ ipcr-forge ──rad init / git push rad──▶ radicle-node
|
||||
# every PUBLIC repository, one way: Radicle is a distribution mirror signed by this node.
|
||||
#
|
||||
# The tile opens Gitea. ipcr-forge-<domain> is the forge's own page, served by the bridge: how to
|
||||
# The tile opens Gitea. ipcr-forge-<domain> is the forge's own page, served by the forge service: how to
|
||||
# publish, the published images with the `image:` line to use, and the Radicle mirrors.
|
||||
#
|
||||
# Gitea, Radicle and IPCR services are copied from their standalone listings (Apps/Gitea,
|
||||
# Apps/Radicle, Apps/IPCR), so each keeps its own comments and rationale.md; the differences are
|
||||
# marked "Forge:". Data folders move, one subfolder per part: $AppID/{gitea,radicle,bridge,ci,ipcr}/.
|
||||
# marked "Forge:". Data folders move, one subfolder per part: $AppID/{gitea,radicle,forge,ci,ipcr}/.
|
||||
#
|
||||
# Replaces the Gitea, Radicle and IPCR apps; it cannot sit beside them. It uses the same container
|
||||
# names, domains (gitea-, radicle-, radicle-api-) and host ports (8776, 4768, 127.0.0.1:4767).
|
||||
@@ -30,19 +30,19 @@ name: ipcr-forge
|
||||
|
||||
services:
|
||||
# ═══ Bridge — the forge's page, and Gitea → Radicle ════════════════════════════
|
||||
# ipcr-forge-bridge (bridge/ in the IPCR repository). Receives Gitea's system webhook on :8081
|
||||
# ipcr-forge (forge/ in the ipcr-forge repository). Receives Gitea's system webhook on :8081
|
||||
# (internal: no Caddy route) and mirrors every public repository to the Radicle node below,
|
||||
# with the stock `rad` CLI against the node's own home. Serves the forge's page on :8080.
|
||||
# Its install step (forge-setup, below) mints its Gitea token and registers the webhook.
|
||||
ipcr-forge:
|
||||
image: ghcr.io/yundera/ipcr-forge-bridge:0.4.0
|
||||
image: ghcr.io/yundera/ipcr-forge:0.4.0
|
||||
container_name: ipcr-forge
|
||||
user: $PUID:$PGID
|
||||
read_only: true
|
||||
cap_drop: [ALL]
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
restart: unless-stopped
|
||||
command: ["ipcr-forge-bridge", "serve"]
|
||||
command: ["ipcr-forge", "serve"]
|
||||
cpu_shares: 30
|
||||
deploy:
|
||||
resources:
|
||||
@@ -53,7 +53,7 @@ services:
|
||||
GITEA_URL: http://gitea:3000
|
||||
GITEA_OWNER: gitea_admin
|
||||
SECRETS_DIR: /secrets
|
||||
STATE_DIR: /bridge
|
||||
STATE_DIR: /forge
|
||||
RAD_HOME: /radicle-home
|
||||
IPCR_PUBLISHED: /srv/ipcr-state/published.json
|
||||
RECONCILE: 10m
|
||||
@@ -77,9 +77,9 @@ services:
|
||||
# radicle-api has.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/radicle/home/:/radicle-home/
|
||||
# repos.json (Gitea repository → Radicle ID) and one bare mirror per repository.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/state:/bridge
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/forge/state:/forge
|
||||
# The Gitea token and the webhook secret the install step writes. Read-only here.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/secrets:/secrets:ro
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/forge/secrets:/secrets:ro
|
||||
# published.json → /images.json, and the admin API's token. Read-only.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ipcr/state:/srv/ipcr-state:ro
|
||||
# Kubo's keystore, read-only: the publisher key, for encrypted backups (Kubo exports keys
|
||||
@@ -89,7 +89,7 @@ services:
|
||||
- gitea
|
||||
- radicle-node
|
||||
healthcheck:
|
||||
test: ["CMD", "ipcr-forge-bridge", "health"]
|
||||
test: ["CMD", "ipcr-forge", "health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
@@ -160,7 +160,7 @@ services:
|
||||
# Forge: off. Images go to the staging registry that IPCR follows; a second registry would
|
||||
# be a second place an image could be published from.
|
||||
GITEA__packages__ENABLED: "false"
|
||||
# Forge: webhooks may reach the bridge (ipcr-forge, on this server's network) and, as by
|
||||
# Forge: webhooks may reach the forge service (ipcr-forge, on this server's network) and, as by
|
||||
# default, any public address. Not `private`: that would let any user's webhook reach every
|
||||
# container on the shared network.
|
||||
GITEA__webhook__ALLOWED_HOST_LIST: external,ipcr-forge
|
||||
@@ -277,11 +277,11 @@ services:
|
||||
# The staging registry's front door: `localhost:5000` to the daemon and to every job (act runs
|
||||
# jobs on the host network, which is this namespace), forge-docker:5000 to ipcr-gateway. Plain
|
||||
# HTTP, which Docker allows to localhost only. It checks a login on every request: a job logs in
|
||||
# as its repository (<owner>/<repo>, the IPCR_PUSH_TOKEN secret the bridge sets on it) and may
|
||||
# as its repository (<owner>/<repo>, the IPCR_PUSH_TOKEN secret the forge service sets on it) and may
|
||||
# push that name only; IPCR logs in as the importer (state/staging-auth, which it generates).
|
||||
# Same image as the bridge (`ipcr-forge-bridge gate`, bridge/gate.go). See rationale.md.
|
||||
# Same image as the forge service (`ipcr-forge gate`, forge/gate.go). See rationale.md.
|
||||
forge-gate:
|
||||
image: ghcr.io/yundera/ipcr-forge-bridge:0.4.0
|
||||
image: ghcr.io/yundera/ipcr-forge:0.4.0
|
||||
container_name: forge-gate
|
||||
user: $PUID:$PGID
|
||||
read_only: true
|
||||
@@ -289,7 +289,7 @@ services:
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
restart: unless-stopped
|
||||
network_mode: service:forge-docker
|
||||
command: ["ipcr-forge-bridge", "gate"]
|
||||
command: ["ipcr-forge", "gate"]
|
||||
cpu_shares: 20
|
||||
deploy:
|
||||
resources:
|
||||
@@ -301,8 +301,8 @@ services:
|
||||
GATE_UPSTREAM: unix:///run/staging/registry.sock
|
||||
GATE_CREDENTIALS: /gate/credentials.json
|
||||
volumes:
|
||||
# Hashes only, written by the bridge.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/bridge/state/gate:/gate:ro
|
||||
# Hashes only, written by the forge service.
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/forge/state/gate:/gate:ro
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/registry-socket:/run/staging
|
||||
depends_on:
|
||||
- forge-docker
|
||||
@@ -342,7 +342,7 @@ services:
|
||||
- ${DATA_ROOT:-/DATA}/AppData/$AppID/ci/registry-socket:/run/staging
|
||||
|
||||
# ═══ Radicle — the public mirror ═══════════════════════════════════════════════
|
||||
# Verbatim from Apps/Radicle; only its data folder moves to $AppID/radicle/. The bridge
|
||||
# Verbatim from Apps/Radicle; only its data folder moves to $AppID/radicle/. The forge service
|
||||
# creates and pushes repositories in this node's storage; the node replicates them.
|
||||
|
||||
# The web UI: the Radicle Explorer bundle served as static files by Caddy.
|
||||
@@ -499,7 +499,7 @@ services:
|
||||
# Forge: watch the CI's staging registry, over ci-internal. Internal, so no credentials.
|
||||
IMPORT_WATCH: ${IPCR_IMPORT_WATCH:-http://forge-docker:5000}
|
||||
IMPORT_AUTH: ${IPCR_IMPORT_AUTH:-}
|
||||
# Forge: IPCR's login at the staging registry's gate, generated on first start; the bridge
|
||||
# Forge: IPCR's login at the staging registry's gate, generated on first start; the forge service
|
||||
# reads it (read-only ipcr/state) and gives it the importer's rights at the gate.
|
||||
IMPORT_AUTH_FILE: /data/state/staging-auth
|
||||
IMPORT_AUTH_GENERATE: "true"
|
||||
@@ -508,8 +508,8 @@ services:
|
||||
# /ipns/<forge key>/<repo>:<tag>. One name for the whole forge, which a DNSLink domain or an
|
||||
# ENS name can point at: ipcr.localhost:4767/ipns/example.eth/<repo>:<tag>.
|
||||
IMPORT_PUBLISHER: ${IPCR_IMPORT_PUBLISHER:-forge}
|
||||
# Forge: publish only the repositories the bridge allows (public Gitea repositories, minus
|
||||
# those switched off in the admin pages). Until the bridge sends its list, nothing is.
|
||||
# Forge: publish only the repositories the forge service allows (public Gitea repositories, minus
|
||||
# those switched off in the admin pages). Until the forge service sends its list, nothing is.
|
||||
IMPORT_ALLOW_REQUIRED: "true"
|
||||
# Forge: delete each tag from the staging registry once published (its blobs go at the
|
||||
# registry's next start, see forge-registry).
|
||||
@@ -518,7 +518,7 @@ services:
|
||||
# its name; resolvers cache them 5 minutes, so a new tag shows up quickly.
|
||||
IPNS_LIFETIME: ${IPCR_IPNS_LIFETIME:-168h}
|
||||
IPNS_TTL: 5m
|
||||
# Forge: the admin API, for the bridge, on ipcr-admin. Bearer token in state/admin-token.
|
||||
# Forge: the admin API, for the forge service, on ipcr-admin. Bearer token in state/admin-token.
|
||||
ADMIN_LISTEN: ":4769"
|
||||
IMPORT_INTERVAL: 60s
|
||||
STATE_DIR: /data/state
|
||||
@@ -547,7 +547,7 @@ services:
|
||||
- ipcr-internal
|
||||
# Forge only: reaches the staging registry for the import poller above.
|
||||
- ci-internal
|
||||
# Forge only: the admin API, reached by the bridge.
|
||||
# Forge only: the admin API, reached by the forge service.
|
||||
- ipcr-admin
|
||||
cpu_shares: 50
|
||||
deploy:
|
||||
@@ -841,15 +841,15 @@ x-compose-app:
|
||||
schema_version: 2
|
||||
folders:
|
||||
# ── Bridge ──
|
||||
- path: /DATA/AppData/$AppID/bridge/state
|
||||
- path: /DATA/AppData/$AppID/forge/state
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
- path: /DATA/AppData/$AppID/bridge/secrets
|
||||
- path: /DATA/AppData/$AppID/forge/secrets
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
# The gate's credentials (hashes), written by the bridge, read by the gate.
|
||||
- path: /DATA/AppData/$AppID/bridge/state/gate
|
||||
# The gate's credentials (hashes), written by the forge service, read by the gate.
|
||||
- path: /DATA/AppData/$AppID/forge/state/gate
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
# ── Gitea ──
|
||||
@@ -897,7 +897,7 @@ x-compose-app:
|
||||
user: $PUID
|
||||
group: $PGID
|
||||
mode: "0700"
|
||||
# Created ahead of Kubo's first start: the bridge mounts it, and Docker would otherwise create
|
||||
# Created ahead of Kubo's first start: the forge service mounts it, and Docker would otherwise create
|
||||
# it as root, where Kubo cannot write its keys.
|
||||
- path: /DATA/AppData/$AppID/ipcr/kubo/keystore
|
||||
user: $PUID
|
||||
@@ -927,7 +927,7 @@ x-compose-app:
|
||||
mode: "0755"
|
||||
init:
|
||||
# In order: Radicle's identity before its node starts, then Gitea's admin, then the runner
|
||||
# token and the bridge's setup, which both need that admin to exist.
|
||||
# token and the forge service's setup, which both need that admin to exist.
|
||||
- name: radicle-identity
|
||||
image: dirk1980/radicle-seed-node@sha256:78ccee526ad4b4f4d7c79c2323b032a7209858a48e1dc9c077272ff1660050e2
|
||||
user: $PUID:$PGID
|
||||
@@ -969,21 +969,21 @@ x-compose-app:
|
||||
mv /runner/token.new /runner/token
|
||||
chmod 600 /runner/token
|
||||
when: absent:/DATA/AppData/$AppID/gitea/runner/.runner
|
||||
# The bridge's Gitea tokens (public-only read; and write:repository for push secrets), the
|
||||
# The forge service's Gitea tokens (public-only read; and write:repository for push secrets), the
|
||||
# webhook secret, the system webhook, the admin pages' OAuth2 app, the root organisation and
|
||||
# the example repository (in that organisation). Idempotent, so on every start: it repairs the webhook.
|
||||
# The admin password is used here only, never stored.
|
||||
- name: forge-setup
|
||||
image: ghcr.io/yundera/ipcr-forge-bridge:0.4.0
|
||||
image: ghcr.io/yundera/ipcr-forge:0.4.0
|
||||
user: $PUID:$PGID
|
||||
phase: post_up
|
||||
network: $APP_NET
|
||||
volumes:
|
||||
- /DATA/AppData/$AppID/bridge/secrets:/secrets
|
||||
- /DATA/AppData/$AppID/forge/secrets:/secrets
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- GITEA_PASSWORD="$APP_DEFAULT_PASSWORD" GITEA_URL=http://gitea:3000 IPCR_ROOT_ORG="$IPCR_ROOT_ORG" PUBLIC_HOSTS="ipcr-forge-$APP_DOMAIN,ipcr-forge-$APP_PUBLIC_IP_DASH.nip.io,ipcr-forge-$APP_PUBLIC_IP_DASH.sslip.io" ipcr-forge-bridge setup
|
||||
- GITEA_PASSWORD="$APP_DEFAULT_PASSWORD" GITEA_URL=http://gitea:3000 IPCR_ROOT_ORG="$IPCR_ROOT_ORG" PUBLIC_HOSTS="ipcr-forge-$APP_DOMAIN,ipcr-forge-$APP_PUBLIC_IP_DASH.nip.io,ipcr-forge-$APP_PUBLIC_IP_DASH.sslip.io" ipcr-forge setup
|
||||
when: always
|
||||
webui-host: gitea-${domain}
|
||||
webui-path: /
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# IPCR Forge — Rationale
|
||||
|
||||
IPCR Forge bundles three store apps (Gitea, Radicle, IPCR), a CI runner with its own Docker daemon,
|
||||
and the bridge (`ipcr-forge-bridge`), which mirrors public Gitea repositories to Radicle and
|
||||
and the forge service (`ipcr-forge`), which mirrors public Gitea repositories to Radicle and
|
||||
serves the forge's page. The Gitea, Radicle and IPCR services are copied from their listings,
|
||||
so each keeps the deviations its standalone listing already argues. This document lists them,
|
||||
then covers only what the bundle adds.
|
||||
@@ -23,11 +23,11 @@ with. See docs/forge.md.
|
||||
**Added by the bundle:**
|
||||
|
||||
1. **`forge-docker` runs `privileged`** (Docker-in-Docker), as Gitea's runner did.
|
||||
2. **The bridge (`ipcr-forge`) mounts the Radicle node's home read-write**, including the node's
|
||||
2. **The forge service (`ipcr-forge`) mounts the Radicle node's home read-write**, including the node's
|
||||
key. It publishes repositories signed by that key.
|
||||
3. **A system webhook and its allow-list.** Gitea may deliver webhooks to `ipcr-forge` on the
|
||||
server's network (`GITEA__webhook__ALLOWED_HOST_LIST: external,ipcr-forge`).
|
||||
4. **An install step that uses the admin password** to mint the bridge's token and to register
|
||||
4. **An install step that uses the admin password** to mint the forge service's token and to register
|
||||
that webhook.
|
||||
5. **A cross-app network link.** `ipcr-gateway` joins `ci-internal`, where the staging registry
|
||||
is.
|
||||
@@ -37,15 +37,15 @@ with. See docs/forge.md.
|
||||
domains and host ports.
|
||||
9. **Admin pages with their own login** (`/admin`): Gitea OAuth2, Gitea site admins only. The
|
||||
install step registers the OAuth2 app.
|
||||
10. **The bridge mounts Kubo's keystore read-only**, so it can export the publisher key, encrypted.
|
||||
10. **The forge service mounts Kubo's keystore read-only**, so it can export the publisher key, encrypted.
|
||||
11. **An admin API in `ipcr-gateway`** (`:4769`) on a new internal network, `ipcr-admin`. Its Bearer
|
||||
token is in `ipcr/state/admin-token`, mode 0644.
|
||||
12. **A staging gate** (`forge-gate`), running in the CI daemon's network namespace. The registry
|
||||
behind it has no network and listens on a unix socket.
|
||||
13. **A second Gitea token for the bridge,** with `write:repository` and `read:user`. It sets each
|
||||
13. **A second Gitea token for the forge service,** with `write:repository` and `read:user`. It sets each
|
||||
publishing repository's `IPCR_PUSH_TOKEN` Actions secret and checks name collisions.
|
||||
14. **A credential IPCR generates for itself** (`ipcr/state/staging-auth`, mode 0644), read by the
|
||||
bridge.
|
||||
forge service.
|
||||
15. **The root organisation** (`IPCR_ROOT_ORG`): setup creates it if missing, and its repositories
|
||||
are also published at the root of the forge's name.
|
||||
16. **A public, unauthenticated, read-only registry route** (`ipcr-<domain>` → `ipcr-public`), so this
|
||||
@@ -65,11 +65,11 @@ container that reaches the daemon over a unix socket.
|
||||
|
||||
**2. The node's home.** Publishing to Radicle means three things: creating a repository in the
|
||||
node's storage, signing its refs with the node's key, and announcing them through the node's
|
||||
control socket. Those live in that folder. This is the mount `radicle-api` already has. The bridge
|
||||
control socket. Those live in that folder. This is the mount `radicle-api` already has. The forge service
|
||||
uses the stock `rad` CLI (1.10.1, the node's version) rather than reimplementing any of it.
|
||||
|
||||
**3. The webhook.** Pushes reach Radicle within seconds instead of at the next 10-minute
|
||||
reconcile. Gitea's default allow-list (`external`) refuses private addresses, and the bridge sits
|
||||
reconcile. Gitea's default allow-list (`external`) refuses private addresses, and the forge service sits
|
||||
on one. The list adds exactly one host name. `private` would let any user's webhook reach every
|
||||
container on the shared network.
|
||||
|
||||
@@ -105,7 +105,7 @@ on the PCS gate, which is not on every host.
|
||||
|
||||
**10. The keystore mount.** Kubo exports keys from its CLI only (`key/export` is not available
|
||||
over its HTTP API). ipcrd runs as root without capabilities, so it cannot read a `$PUID` 0700
|
||||
folder; giving it `DAC_READ_SEARCH` was rejected once already. The bridge runs as `$PUID` and can.
|
||||
folder; giving it `DAC_READ_SEARCH` was rejected once already. The forge service runs as `$PUID` and can.
|
||||
|
||||
**11. The admin API and its token.** Unpublishing, moving tags, key restore and the allowlist
|
||||
change ipcrd's own state (the published tree, `published.json`), so they live in ipcrd. The token is
|
||||
@@ -127,7 +127,7 @@ repository it belongs to. Setting a secret takes `write:repository`. The token i
|
||||
`public-only`, so collision checks also see private users and organisations.
|
||||
|
||||
**14. IPCR's own credential.** Same handoff as the admin token: ipcrd (root without capabilities)
|
||||
cannot write into another uid's folder, so it writes into its own state folder, which the bridge
|
||||
cannot write into another uid's folder, so it writes into its own state folder, which the forge service
|
||||
already mounts read-only.
|
||||
|
||||
**15. The root organisation.** Short names (`metadec.eth/ipcr-hello`) need one owner whose
|
||||
@@ -141,17 +141,17 @@ purpose: the images are public on IPFS anyway.
|
||||
|
||||
## Security mitigations in place
|
||||
|
||||
**The bridge**
|
||||
**The forge service**
|
||||
- **It sees public repositories only.** Its Gitea token is scoped `public-only` and
|
||||
`read:repository`. Gitea hides every private or limited repository from it, and it can write
|
||||
nothing in Gitea. The code checks visibility a second time. Forks and pull mirrors are skipped.
|
||||
- **Webhooks are authenticated and carry no content.** Each delivery must pass the HMAC-SHA256
|
||||
check against a random secret written at install (`bridge/secrets`, 0700). A delivery only says
|
||||
*which* repository changed. The bridge re-reads that repository from Gitea, so a forged payload
|
||||
check against a random secret written at install (`forge/secrets`, 0700). A delivery only says
|
||||
*which* repository changed. The forge service re-reads that repository from Gitea, so a forged payload
|
||||
can at most cause a pointless sync. Port 8081 has no Caddy route.
|
||||
- **It runs unprivileged:** `$PUID`, every capability dropped, a read-only root filesystem,
|
||||
`no-new-privileges`, 256 MB. One worker does one sync at a time.
|
||||
- **The node's key stays in the bridge and the node.** No CI job mounts the Radicle home.
|
||||
- **The node's key stays in the forge service and the node.** No CI job mounts the Radicle home.
|
||||
|
||||
**The CI daemon**
|
||||
- **No network listener.** `dockerd` gets an explicit `--host=unix:///run/dind/docker.sock` and an
|
||||
@@ -202,8 +202,8 @@ purpose: the images are public on IPFS anyway.
|
||||
- **Can be switched off** in the admin pages (`config.json` `"public": false`).
|
||||
|
||||
**The page**
|
||||
- **The public page cannot change anything.** The bridge serves it read-only: the embedded page,
|
||||
IPCR's `published.json` (mounted read-only) and the bridge's repository list.
|
||||
- **The public page cannot change anything.** The forge service serves it read-only: the embedded page,
|
||||
IPCR's `published.json` (mounted read-only) and the forge service's repository list.
|
||||
|
||||
**The admin pages**
|
||||
- **Only Gitea site admins get in.** The check is `is_admin`, read from Gitea's own API through the
|
||||
@@ -215,12 +215,12 @@ purpose: the images are public on IPFS anyway.
|
||||
custom `X-IPCR-Admin` header is required. A cross-site form cannot send that header, and a
|
||||
cross-site script cannot without a CORS preflight, which is never answered.
|
||||
- **The admin API is only on `ipcr-admin`.** That network is `internal: true` and shared only by
|
||||
the bridge and `ipcr-gateway`; every call needs the Bearer token, compared in constant time. Kubo's
|
||||
the forge service and `ipcr-gateway`; every call needs the Bearer token, compared in constant time. Kubo's
|
||||
own API stays on `ipcr-internal`.
|
||||
- **The cleartext key never crosses the network.** The bridge encrypts it in memory (AES-256-GCM,
|
||||
- **The cleartext key never crosses the network.** The forge service encrypts it in memory (AES-256-GCM,
|
||||
PBKDF2 at 600k iterations, at least 12 characters of passphrase) and ipcrd decrypts it. A
|
||||
restore never deletes a key: the previous one is kept, renamed.
|
||||
- **The allowlist is closed by default.** Until the bridge sends its list, nothing is published
|
||||
- **The allowlist is closed by default.** Until the forge service sends its list, nothing is published
|
||||
(`IMPORT_ALLOW_REQUIRED`). Private repositories are never on it.
|
||||
|
||||
## Alternatives considered and rejected
|
||||
@@ -234,7 +234,7 @@ purpose: the images are public on IPFS anyway.
|
||||
| A per-repository webhook | Every repository would need setup; one system webhook covers all of them, including new ones |
|
||||
| Gitea's package registry as the staging registry (v1) | Needs tokens for the workflow and for IPCR, plus the `DAC_READ_SEARCH` capability for IPCR to read its token. It would also be a second place images get published from |
|
||||
| Host Docker socket for jobs | Any workflow would get root on the host |
|
||||
| A long-lived admin token for the bridge | The webhook is repaired by the install step on every start instead, so the running service never holds admin rights |
|
||||
| A long-lived admin token for the forge service | The webhook is repaired by the install step on every start instead, so the running service never holds admin rights |
|
||||
|
||||
## Known limitations
|
||||
|
||||
@@ -252,7 +252,7 @@ purpose: the images are public on IPFS anyway.
|
||||
repository changes hands.
|
||||
- **Existing workflows must add a login step,** or their pushes fail with 401.
|
||||
- **Private repositories cannot publish:** they get no push credential.
|
||||
- **The bridge can read the publisher key.** A compromise of the public-facing bridge leaks it. That
|
||||
- **The forge service can read the publisher key.** A compromise of the public-facing forge service leaks it. That
|
||||
is the trust it already has with the Radicle node key.
|
||||
- **The admin token is world-readable** inside the app's state folder on the host.
|
||||
- **Old keys accumulate.** `<key>-replaced-*` keys are kept and republished by Kubo until removed by
|
||||
@@ -268,7 +268,7 @@ Everything lives under `/DATA/AppData/ipcr-forge/`, one folder per part:
|
||||
| --- | --- |
|
||||
| `gitea/` | repositories, the database (SQLite), configuration, the runner's registration and config |
|
||||
| `radicle/` | node identity (key pair), seeded repositories, Caddy config |
|
||||
| `bridge/` | `state/`: the Gitea → Radicle mapping, one bare mirror per repository (disposable), the admin session key and the repository toggles (`admin.json`); `gate/credentials.json` (token hashes); `secrets/` (0700): its two Gitea tokens, the webhook secret, the OAuth2 client credentials |
|
||||
| `forge/` | `state/`: the Gitea → Radicle mapping, one bare mirror per repository (disposable), the admin session key and the repository toggles (`admin.json`); `gate/credentials.json` (token hashes); `secrets/` (0700): its two Gitea tokens, the webhook secret, the OAuth2 client credentials |
|
||||
| `ci/` | the daemon's socket, its image store (`docker/`), the staging registry (`registry/`, disposable) and its socket (`registry-socket/`) |
|
||||
| `ipcr/` | Kubo repo (node identity, IPNS keys, pinned images), TLS CA, watcher state, `config.json` (name, allowlist), the admin token |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user