gitshark

Clone repository

git clone https://gitshark.de/git/workaround/Gitshark.git
git clone git@gitshark.de:workaround/Gitshark.git

← Commits

πŸ“ (docs): Restructure guides by audience and add ForgeFed maintainer notes

a20ae85199019882b7624ad28541da2b4e665d4e Β· Miggi Β· 2026-07-08T19:20:26Z

Changes

9 files changed, +875 -622

MODIFY README.md +3 -2
diff --git a/README.md b/README.md
index 155b5ab..9fde82a 100644
--- a/README.md
+++ b/README.md
@@ -40,8 +40,9 @@
40 40 can interoperate. The first goal is **git-shark ↔ git-shark**: a remote instance follows a public
41 41 repository and receives a signed `Push` activity whenever commits land. Standard ActivityStreams +
42 42 ForgeFed vocabulary and HTTP Signatures (RSA) are used throughout, so other ForgeFed software *could*
43 -interoperate later (untested). Detailed guides: [for users](docs/federation-users.md) and
44 -[for deployment](docs/federation-deployment.md).
43 +interoperate later (untested). Detailed guides: [for users](docs/users/federation.md),
44 +[for deployment](docs/admins/federation.md), and
45 +[architecture notes for maintainers](docs/maintainers/forgefed.md).
45 46
46 47 When enabled, each public repository (and each user) is an actor under `/ap`:
47 48
MODIFY docs/README.md +18 -3
diff --git a/docs/README.md b/docs/README.md
index 8582bbb..0ffa259 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -6,12 +6,27 @@
6 6
7 7 ## Contents
8 8
9 -- **[Getting Started](getting-started.md)** β€” deploy git-shark with Docker Compose,
10 - from zero to a running instance behind TLS with OIDC login and SSH git access.
9 +### For users
10 +
11 +- **[Federation](users/federation.md)** β€” follow public repositories on other
12 + instances, the push feed, your federated identity.
13 +
14 +### For admins
15 +
16 +- **[Getting Started](admins/getting-started.md)** β€” deploy git-shark with Docker
17 + Compose, from zero to a running instance behind TLS with OIDC login and SSH git
18 + access.
19 +- **[Federation](admins/federation.md)** β€” enable and operate ForgeFed federation:
20 + configuration, allowlist, reverse-proxy requirements, delivery queue, monitoring.
21 +
22 +### For maintainers
23 +
24 +- **[ForgeFed architecture](maintainers/forgefed.md)** β€” how federation is
25 + implemented, the decisions behind it, what works and what is still missing.
11 26
12 27 ## Where else to look
13 28
14 29 - [`../README.md`](../README.md) β€” feature overview and architecture notes.
15 30 - Application configuration lives in `src/main/resources/application.properties`.
16 31 Every production knob is overridable via an environment variable (listed in
17 - [Getting Started β†’ Configuration reference](getting-started.md#configuration-reference)).
32 + [Getting Started β†’ Configuration reference](admins/getting-started.md#configuration-reference)).
ADD docs/admins/federation.md +189 -0
diff --git a/docs/admins/federation.md b/docs/admins/federation.md
new file mode 100644
index 0000000..1027557
--- /dev/null
+++ b/docs/admins/federation.md
@@ -0,0 +1,189 @@
1 +# Federation: deployment guide
2 +
3 +How to enable and operate ForgeFed/ActivityPub federation on a git-shark
4 +instance. For what federation offers end users, see the
5 +[user guide](../users/federation.md); for a general deployment walkthrough, see
6 +[Getting Started](getting-started.md). For architecture and implementation
7 +decisions, see the [maintainer notes](../maintainers/forgefed.md).
8 +
9 +Federation is **off by default** and fails closed: nothing is emitted or
10 +accepted until it is explicitly enabled *and* correctly configured.
11 +
12 +---
13 +
14 +## Configuration
15 +
16 +All settings come from environment variables:
17 +
18 +| Variable | Default | Meaning |
19 +|---|---|---|
20 +| `GITSHARK_FEDERATION_ENABLED` | `false` | Master switch |
21 +| `GITSHARK_FEDERATION_BASE_URL` | β€” | Public HTTPS origin of this instance, e.g. `https://shark.example.com`. Actor IDs derive from it |
22 +| `GITSHARK_FEDERATION_PEER_ALLOWLIST` | β€” (empty) | Comma-separated peer **hosts**, e.g. `shark.other.org,forge.example`. Empty denies every remote peer |
23 +| `GITSHARK_FEDERATION_MAX_ATTEMPTS` | `8` | Delivery attempts before an outbound activity is dead-lettered |
24 +| `GITSHARK_FEDERATION_DEV_ALLOW_INSECURE` | `false` | **Dev only.** Permits `http://` and loopback/private targets. Never in production |
25 +
26 +Minimal production setup:
27 +
28 +```bash
29 +GITSHARK_FEDERATION_ENABLED=true
30 +GITSHARK_FEDERATION_BASE_URL=https://shark.example.com
31 +GITSHARK_FEDERATION_PEER_ALLOWLIST=shark.other.org
32 +```
33 +
34 +> **`base-url` is permanent.** Actor IDs (`https://<base-url>/ap/...`) are
35 +> published to other servers and cannot change without breaking every existing
36 +> follow relationship. Set a real, stable, non-loopback HTTPS origin before
37 +> enabling. git-shark refuses to operate federation (`operational = false`) while
38 +> the base URL is unset, loopback, or otherwise unusable β€” the switch alone is
39 +> not enough.
40 +
41 +### The peer allowlist
42 +
43 +Federation is mutual and allowlist-bound in **both** directions:
44 +
45 +- **Inbound**: an activity is only accepted if its HTTP Signature's key belongs
46 + to an actor on an allowlisted host. Everything else is `401`.
47 +- **Outbound**: actor fetches, WebFinger lookups, and deliveries only go to
48 + allowlisted hosts.
49 +
50 +Matching is by exact, case-insensitive host name. Both instances must allowlist
51 +each other, or follows stay Pending forever.
52 +
53 +---
54 +
55 +## What gets exposed
56 +
57 +With federation operational, these endpoints are public (no login):
58 +
59 +| Endpoint | Purpose |
60 +|---|---|
61 +| `GET /.well-known/webfinger?resource=acct:…` | Actor discovery (`user@host`, `owner/name@host`; the host part may be bare or `host:port`) |
62 +| `GET /ap/repos/{owner}/{name}` | ForgeFed `Repository` actor (public repos only) |
63 +| `GET /ap/users/{username}` | ActivityPub `Person` actor |
64 +| `GET /ap/instance` | Instance `Application` actor |
65 +| `GET …/outbox`, `…/followers` | Activity and follower collections |
66 +| `POST …/inbox` | Signed inbound activities (repo, user, and instance inboxes) |
67 +
68 +Private repositories are never exposed. Inbound posts are verified (HTTP
69 +Signature, body digest, date skew), deduplicated by activity id, then dispatched.
70 +
71 +---
72 +
73 +## Reverse proxy requirements
74 +
75 +Federation signs and verifies the `Host` header and the raw request path.
76 +The proxy in front of git-shark must therefore:
77 +
78 +- **Preserve the original `Host` header** (`proxy_set_header Host $host;` in
79 + nginx; Caddy does this by default). A rewritten host breaks signature
80 + verification on every inbound activity.
81 +- Pass `/ap/*` and `/.well-known/webfinger` through **without** auth,
82 + path rewriting, or body buffering surprises.
83 +- Terminate TLS β€” peers will only talk `https` to you.
84 +
85 +The existing `quarkus.http.proxy.*` settings (already on in the default config)
86 +make git-shark trust `X-Forwarded-*` so actor URLs and signature reconstruction
87 +use the external origin.
88 +
89 +---
90 +
91 +## Keys and signing
92 +
93 +- Every local actor (each public repository, each user, plus the instance) gets
94 + an **RSA-2048 keypair**, generated on first use and stored in the
95 + `federation_keys` table. There is no key configuration and currently no
96 + rotation mechanism; protect your database accordingly.
97 +- Outbound activities are signed with draft-cavage HTTP Signatures
98 + (`SHA256withRSA`), key id `<actor-id>#main-key`.
99 +- Outbound follows are signed as the **user's `Person` actor**; `Accept` and
100 + `Push` fan-out are signed as the repository actor.
101 +
102 +---
103 +
104 +## Delivery queue and retries
105 +
106 +Outbound activities go through a persisted queue (`federation_delivery` table),
107 +drained every 10 seconds:
108 +
109 +- Failures retry with exponential backoff: 1m, 2m, 4m, … capped at 1h.
110 +- After `GITSHARK_FEDERATION_MAX_ATTEMPTS` (default 8) the row is dead-lettered
111 + as `FAILED` with the last error preserved β€” it will not retry again.
112 +- States: `PENDING` β†’ `DELIVERED` or `FAILED`.
113 +
114 +Monitoring queries worth having:
115 +
116 +```sql
117 +-- stuck or failing deliveries
118 +select target_inbox, attempts, state, last_error, next_attempt_at
119 +from federation_delivery
120 +where state <> 'DELIVERED'
121 +order by created_at desc;
122 +
123 +-- follow relationships still waiting for the remote's Accept
124 +select remote_actor_id, state, created_at from remote_follows where state = 'PENDING';
125 +```
126 +
127 +---
128 +
129 +## Data stored for federation
130 +
131 +| Table | Contents |
132 +|---|---|
133 +| `federation_keys` | Local actor keypairs (public + private PEM) |
134 +| `remote_actors` | Cache of fetched remote actors (inbox, public key; 6h TTL) |
135 +| `repository_followers` | Remote actors following local repositories |
136 +| `remote_follows` | Local users' follows of remote repositories (`PENDING`/`ACCEPTED`) |
137 +| `received_pushes` | `Push` activities received from followed repositories (feed) |
138 +| `federation_outbox` / `federation_inbox` | Published activities / inbound dedup log |
139 +| `federation_delivery` | Outbound delivery queue |
140 +
141 +---
142 +
143 +## SSRF protection
144 +
145 +All outbound federation traffic (actor fetches, WebFinger, deliveries) passes a
146 +URL guard: HTTPS only, host must be allowlisted, and the resolved address must
147 +not be loopback, link-local, private, or multicast. `dev-allow-insecure` relaxes
148 +the scheme and address checks **but never the allowlist**.
149 +
150 +---
151 +
152 +## Local two-host trial (dev)
153 +
154 +To try federation on one machine, run two dev instances that allowlist each
155 +other:
156 +
157 +```bash
158 +# terminal A
159 +GITSHARK_FEDERATION_ENABLED=true \
160 +GITSHARK_FEDERATION_BASE_URL=http://localhost:8080 \
161 +GITSHARK_FEDERATION_PEER_ALLOWLIST=localhost \
162 +GITSHARK_FEDERATION_DEV_ALLOW_INSECURE=true \
163 +./mvnw quarkus:dev
164 +
165 +# terminal B (second checkout or worktree)
166 +GITSHARK_FEDERATION_ENABLED=true \
167 +GITSHARK_FEDERATION_BASE_URL=http://localhost:8081 \
168 +GITSHARK_FEDERATION_PEER_ALLOWLIST=localhost \
169 +GITSHARK_FEDERATION_DEV_ALLOW_INSECURE=true \
170 +GITSHARK_SSH_PORT=2223 \
171 +./mvnw quarkus:dev -Dquarkus.http.port=8081 -Ddebug=false \
172 + -Dquarkus.datasource.devservices.shared=false
173 +```
174 +
175 +On B, log in and follow `alice/demo@localhost:8080` (dev seed data) from the
176 +Following page. The follow flips to Accepted within ~20s; pushing to the demo
177 +repo on A makes the push appear under "Recent pushes" on B.
178 +
179 +---
180 +
181 +## Troubleshooting
182 +
183 +| Symptom | Likely cause |
184 +|---|---|
185 +| Follow stays `PENDING` | Peer not allowlisted (either side), remote down, or delivery dead-lettered β€” check `federation_delivery.last_error` |
186 +| Inbound activities all `401` | Signer host not on your allowlist, or the reverse proxy rewrites the `Host` header |
187 +| `Could not resolve handle` | Remote host not allowlisted locally, WebFinger unreachable, or repo not public |
188 +| Actor documents `404` | Federation not operational: switch off, or `base-url` unset/loopback |
189 +| Deliveries fail with `Only https is allowed` | Peer published an `http://` inbox/actor URL and you are not in dev-insecure mode |
ADD docs/admins/getting-started.md +361 -0
diff --git a/docs/admins/getting-started.md b/docs/admins/getting-started.md
new file mode 100644
index 0000000..7ba95c7
--- /dev/null
+++ b/docs/admins/getting-started.md
@@ -0,0 +1,361 @@
1 +# Getting Started: deploying git-shark with Docker Compose
2 +
3 +This guide takes you from an empty host to a running git-shark instance:
4 +
5 +1. A PostgreSQL database (metadata store).
6 +2. The git-shark application container (web UI + smart-HTTP git + embedded SSH).
7 +3. A TLS-terminating reverse proxy in front (git-shark requires HTTPS).
8 +4. An external OIDC provider for login (kanidm, Keycloak, or any OpenID Connect IdP).
9 +
10 +By the end you can browse the web UI over HTTPS, log in with OIDC, and clone/push
11 +over both `https://` and `ssh://`.
12 +
13 +> **git-shark has no built-in password login.** Authentication to the web UI is
14 +> delegated entirely to an OIDC provider. You must have one reachable (or stand one
15 +> up) before the app is usable. Git transport authenticates separately β€” personal
16 +> access tokens over HTTP Basic, SSH public keys over SSH.
17 +
18 +---
19 +
20 +## Prerequisites
21 +
22 +- **Docker Engine 24+** and the **Docker Compose v2** plugin (`docker compose`, not the
23 + legacy `docker-compose`).
24 +- A **DNS name** pointing at the host (e.g. `gitshark.example.com`). OIDC redirect URIs
25 + and β€” if you enable federation β€” permanent actor IDs are derived from it.
26 +- An **OIDC provider** with an authorization-code client for git-shark. PKCE is required.
27 +- Ports **80/443** (reverse proxy) and **2222** (SSH git) reachable from your clients.
28 +
29 +---
30 +
31 +## Step 1 β€” Get the application image
32 +
33 +Use the prebuilt image published to GitHub Container Registry β€” nothing to build:
34 +
35 +```bash
36 +docker pull ghcr.io/workaround-org/git-shark:latest
37 +```
38 +
39 +Pin a specific release instead of `latest` for reproducible deploys (e.g.
40 +`ghcr.io/workaround-org/git-shark:1.0.0`).
41 +
42 +The image listens on **8080** (HTTP) and, once configured, **2222** (SSH). It runs as
43 +UID `185` and reads all production settings from environment variables.
44 +
45 +> **Building it yourself instead.** git-shark ships a JVM Dockerfile, so from the repo
46 +> root you can build a local image and point the Compose file's `image:` at it:
47 +>
48 +> ```bash
49 +> ./mvnw package # produces target/quarkus-app/
50 +> docker build -f src/main/docker/Dockerfile.jvm -t git-shark:local .
51 +> ```
52 +>
53 +> For a smaller, faster-starting image build the native variant (`-Dnative` with
54 +> `Dockerfile.native-micro`).
55 +
56 +---
57 +
58 +## Step 2 β€” Register the OIDC client
59 +
60 +git-shark uses the OIDC **authorization code flow** with **PKCE**. Create a confidential
61 +client at your IdP and note three things: the **issuer/discovery URL**, the **client ID**,
62 +and the **client secret**. Set the redirect URI to your public origin with a trailing
63 +slash: `https://gitshark.example.com/`.
64 +
65 +### kanidm example
66 +
67 +```bash
68 +kanidm system oauth2 create git-shark "Git Shark" https://gitshark.example.com
69 +kanidm system oauth2 add-redirect-url git-shark https://gitshark.example.com/
70 +kanidm group create gitshark_users
71 +kanidm group add-members gitshark_users <your-user>
72 +kanidm system oauth2 update-scope-map git-shark gitshark_users openid profile email
73 +kanidm system oauth2 show-basic-secret git-shark # -> client secret
74 +```
75 +
76 +The auth-server URL for kanidm is `https://<kanidm-host>/oauth2/openid/git-shark`.
77 +
78 +### Keycloak / other IdPs
79 +
80 +Create a confidential client with:
81 +- Standard flow (authorization code) enabled, PKCE `S256` required.
82 +- Redirect URI `https://gitshark.example.com/`.
83 +- Scopes `openid profile email`.
84 +
85 +The auth-server URL is the realm issuer, e.g.
86 +`https://keycloak.example.com/realms/<realm>`.
87 +
88 +---
89 +
90 +## Step 3 β€” Generate the encryption secrets
91 +
92 +Two secrets encrypt the PKCE state cookie and the post-login session cookie. Each must
93 +be **at least 32 characters** (Quarkus minimum). Generate them once and keep them stable
94 +β€” rotating them invalidates in-flight logins and existing sessions.
95 +
96 +```bash
97 +openssl rand -hex 16 # 32 hex chars β€” run twice, for the two secrets below
98 +```
99 +
100 +---
101 +
102 +## Step 4 β€” Write the `.env` file
103 +
104 +Compose reads these values. Put the file next to `docker-compose.yml`, keep it out of
105 +version control (it holds secrets).
106 +
107 +```dotenv
108 +# --- Public origin ---
109 +APP_DOMAIN=gitshark.example.com
110 +
111 +# --- PostgreSQL ---
112 +POSTGRES_DB=gitshark
113 +POSTGRES_USER=gitshark
114 +POSTGRES_PASSWORD=change-me-strong-db-password
115 +
116 +# --- OIDC (from Step 2) ---
117 +OIDC_AUTH_SERVER_URL=https://idm.example.com/oauth2/openid/git-shark
118 +OIDC_CLIENT_ID=git-shark
119 +OIDC_CLIENT_SECRET=the-basic-secret-from-your-idp
120 +
121 +# --- OIDC cookie encryption (from Step 3, >= 32 chars each) ---
122 +OIDC_STATE_SECRET=paste-first-openssl-rand-output
123 +OIDC_TOKEN_STATE_SECRET=paste-second-openssl-rand-output
124 +```
125 +
126 +---
127 +
128 +## Step 5 β€” The Compose file
129 +
130 +```yaml
131 +name: git-shark
132 +
133 +services:
134 + db:
135 + image: postgres:17
136 + restart: unless-stopped
137 + environment:
138 + POSTGRES_DB: ${POSTGRES_DB}
139 + POSTGRES_USER: ${POSTGRES_USER}
140 + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
141 + volumes:
142 + - db-data:/var/lib/postgresql/data
143 + healthcheck:
144 + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
145 + interval: 10s
146 + timeout: 5s
147 + retries: 5
148 +
149 + app:
150 + image: ghcr.io/workaround-org/git-shark:latest # or a pinned tag; see Step 1
151 + restart: unless-stopped
152 + depends_on:
153 + db:
154 + condition: service_healthy
155 + environment:
156 + # --- Datasource ---
157 + QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://db:5432/${POSTGRES_DB}
158 + QUARKUS_DATASOURCE_USERNAME: ${POSTGRES_USER}
159 + QUARKUS_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
160 + # --- OIDC ---
161 + QUARKUS_OIDC_AUTH_SERVER_URL: ${OIDC_AUTH_SERVER_URL}
162 + QUARKUS_OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
163 + QUARKUS_OIDC_CREDENTIALS_SECRET: ${OIDC_CLIENT_SECRET}
164 + QUARKUS_OIDC_AUTHENTICATION_STATE_SECRET: ${OIDC_STATE_SECRET}
165 + QUARKUS_OIDC_TOKEN_STATE_ENCRYPTION_SECRET: ${OIDC_TOKEN_STATE_SECRET}
166 + # --- Storage & SSH ---
167 + GITSHARK_STORAGE_ROOT: /data/repositories
168 + GITSHARK_SSH_HOST_KEY: /data/ssh/host-key
169 + GITSHARK_SSH_PORT: "2222"
170 + ports:
171 + - "2222:2222" # SSH git access, published directly
172 + volumes:
173 + - repos:/data/repositories # bare git repositories
174 + - ssh:/data/ssh # persistent SSH host key
175 + healthcheck:
176 + test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080 && echo ok >&3"]
177 + interval: 15s
178 + timeout: 5s
179 + retries: 10
180 + start_period: 40s
181 +
182 +volumes:
183 + db-data:
184 + repos:
185 + ssh:
186 +```
187 +
188 +Notes:
189 +
190 +- **Separate volumes for `/data/repositories` and `/data/ssh`** so Docker creates both
191 + mount points with the right ownership β€” no init container or `mkdir` needed. The SSH
192 + host key is generated on first boot and persists across restarts (so client
193 + `known_hosts` entries stay valid).
194 +- **HTTP port 8080 is not published** β€” it's reached through the reverse proxy on the
195 + Compose network (Step 6). Only SSH (2222) is exposed directly.
196 +- **Single app replica.** git-shark keeps git state on a `ReadWriteOnce`-style filesystem
197 + volume; do not scale `app` beyond one instance.
198 +- Flyway migrates the schema automatically at startup (`migrate-at-start=true`), so the
199 + database needs no manual initialization beyond an empty database + owner.
200 +
201 +---
202 +
203 +## Step 6 β€” TLS reverse proxy (required)
204 +
205 +git-shark always builds **HTTPS** OIDC redirect URIs and trusts `X-Forwarded-*` headers
206 +(`force-redirect-https-scheme=true`, `proxy-address-forwarding=true`). It is designed to
207 +run behind a TLS-terminating proxy β€” plain HTTP will break the login redirect.
208 +
209 +Add a Caddy service to the Compose file β€” it fetches and renews a Let's Encrypt
210 +certificate automatically:
211 +
212 +```yaml
213 + proxy:
214 + image: caddy:2
215 + restart: unless-stopped
216 + depends_on:
217 + - app
218 + ports:
219 + - "80:80"
220 + - "443:443"
221 + volumes:
222 + - ./Caddyfile:/etc/caddy/Caddyfile:ro
223 + - caddy-data:/data
224 + - caddy-config:/config
225 +```
226 +
227 +Add `caddy-data:` and `caddy-config:` to the top-level `volumes:` block, then create a
228 +`Caddyfile` next to the Compose file:
229 +
230 +```caddyfile
231 +gitshark.example.com {
232 + reverse_proxy app:8080
233 +}
234 +```
235 +
236 +Caddy forwards `X-Forwarded-Proto`/`-For`/`-Host` by default, which is exactly what
237 +git-shark's OIDC redirect construction needs. Using Traefik or nginx instead is fine β€”
238 +just terminate TLS and forward those headers.
239 +
240 +---
241 +
242 +## Step 7 β€” Bring it up
243 +
244 +```bash
245 +docker compose up -d
246 +docker compose logs -f app # watch for "Listening on: http://0.0.0.0:8080"
247 +```
248 +
249 +Then open `https://gitshark.example.com/`, click **Log in**, and complete the OIDC flow.
250 +On first login you're redirected to `/onboarding` to pick a URL-safe handle
251 +(`^[a-z0-9][a-z0-9-]{0,38}$`) β€” this handle, not the IdP username, appears in all repo,
252 +SSH, and federation URLs.
253 +
254 +---
255 +
256 +## Step 8 β€” Verify git access
257 +
258 +**HTTP** (anonymous read on public repos; push/private read use a personal access token
259 +as the HTTP Basic password β€” create one under *Access tokens* in the UI):
260 +
261 +```bash
262 +git clone https://gitshark.example.com/git/<owner>/<repo>.git
263 +```
264 +
265 +**SSH** (public-key only; add your key under *SSH keys* in the UI):
266 +
267 +```bash
268 +git clone ssh://git@gitshark.example.com:2222/<owner>/<repo>.git
269 +```
270 +
271 +> Want bare `git@gitshark.example.com` without the `:2222`? Publish the container's 2222
272 +> on host port 22 (`"22:2222"`) β€” but only if the host's own sshd isn't already using 22.
273 +
274 +---
275 +
276 +## Configuration reference
277 +
278 +Every value below is an environment variable on the `app` service. Defaults come from
279 +`src/main/resources/application.properties`.
280 +
281 +| Variable | Required | Default | Purpose |
282 +|---|---|---|---|
283 +| `QUARKUS_DATASOURCE_JDBC_URL` | βœ… | β€” | PostgreSQL JDBC URL |
284 +| `QUARKUS_DATASOURCE_USERNAME` | βœ… | β€” | DB user |
285 +| `QUARKUS_DATASOURCE_PASSWORD` | βœ… | β€” | DB password |
286 +| `QUARKUS_OIDC_AUTH_SERVER_URL` | βœ… | β€” | OIDC issuer / discovery URL |
287 +| `QUARKUS_OIDC_CLIENT_ID` | βœ… | β€” | OIDC client ID |
288 +| `QUARKUS_OIDC_CREDENTIALS_SECRET` | βœ… | β€” | OIDC client secret |
289 +| `QUARKUS_OIDC_AUTHENTICATION_STATE_SECRET` | βœ… | β€” | Encrypts PKCE state cookie (β‰₯ 32 chars) |
290 +| `QUARKUS_OIDC_TOKEN_STATE_ENCRYPTION_SECRET` | βœ… | β€” | Encrypts session/token cookie (β‰₯ 32 chars) |
291 +| `GITSHARK_STORAGE_ROOT` | β€” | `data/repositories` | On-disk bare-repo root |
292 +| `GITSHARK_SSH_HOST_KEY` | β€” | `data/ssh/host-key` | Persistent SSH host key path |
293 +| `GITSHARK_SSH_PORT` | β€” | `2222` | Embedded SSH server port |
294 +| `GITSHARK_FEDERATION_ENABLED` | β€” | `false` | Turn on ForgeFed/ActivityPub |
295 +| `GITSHARK_FEDERATION_BASE_URL` | β€” | β€” | Public HTTPS origin; permanent actor-ID base |
296 +| `GITSHARK_FEDERATION_PEER_ALLOWLIST` | β€” | β€” | Comma-separated peer hosts (empty denies all) |
297 +| `GITSHARK_FEDERATION_MAX_ATTEMPTS` | β€” | `8` | Outbound delivery retry cap |
298 +| `GITSHARK_FEDERATION_DEV_ALLOW_INSECURE` | β€” | `false` | Dev only: allow http/loopback peers |
299 +
300 +### Optional: federation (ForgeFed)
301 +
302 +Off by default. Enabling it publishes **permanent** actor IDs derived from
303 +`GITSHARK_FEDERATION_BASE_URL`, so set a real, stable, non-loopback HTTPS origin before
304 +turning it on β€” git-shark refuses to emit actor documents otherwise.
305 +
306 +```yaml
307 + GITSHARK_FEDERATION_ENABLED: "true"
308 + GITSHARK_FEDERATION_BASE_URL: https://gitshark.example.com
309 + GITSHARK_FEDERATION_PEER_ALLOWLIST: peer-a.example,peer-b.example
310 +```
311 +
312 +Inbound activities need a valid HTTP Signature from an allowlisted peer; outbound
313 +fetches are HTTPS-only, allowlist-bound, and SSRF-guarded. Never set
314 +`GITSHARK_FEDERATION_DEV_ALLOW_INSECURE=true` in production.
315 +
316 +---
317 +
318 +## Operations
319 +
320 +**Backups** β€” two things hold state:
321 +- The `db-data` volume (metadata: users, repo records, issues, MRs, comments).
322 +- The `repos` volume (the actual git objects).
323 +
324 +Back both up together and consistently. A logical DB dump:
325 +
326 +```bash
327 +docker compose exec db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > gitshark-db.sql
328 +```
329 +
330 +Snapshot the `repos` volume with your host's volume/snapshot tooling while the app is
331 +quiesced (or accept crash-consistent snapshots β€” bare repos tolerate them well).
332 +
333 +**Upgrades** β€” pull the new image and recreate the app:
334 +
335 +```bash
336 +docker compose pull app
337 +docker compose up -d app
338 +```
339 +
340 +Flyway applies any new migrations on startup. Because the app uses the `Recreate`
341 +pattern (one writer, filesystem state), a brief downtime during redeploy is expected.
342 +
343 +**Logs & health**:
344 +
345 +```bash
346 +docker compose ps
347 +docker compose logs -f app
348 +```
349 +
350 +---
351 +
352 +## Troubleshooting
353 +
354 +| Symptom | Likely cause |
355 +|---|---|
356 +| Login redirects to `http://…` or loops | Proxy not forwarding `X-Forwarded-Proto`, or you hit the app over plain HTTP. Front it with TLS (Step 6). |
357 +| Boot fails on OIDC discovery | `QUARKUS_OIDC_AUTH_SERVER_URL` wrong/unreachable, or IdP demands HTTPS the app can't reach. |
358 +| App exits complaining about secret length | `*_STATE_SECRET` shorter than 32 chars. Regenerate with `openssl rand -hex 16`. |
359 +| SSH host key changed after redeploy | The `ssh` volume wasn't persisted β€” confirm it's a named volume, not a throwaway mount. |
360 +| `git push` over HTTP rejected | Use a personal access token (from *Access tokens*) as the Basic-auth password, not your OIDC password. |
361 +| Schema validation error at start | DB not empty / migrated by a different tool. git-shark's Flyway owns the schema; start from an empty database. |
DELETE docs/federation-deployment.md +0 -177
diff --git a/docs/federation-deployment.md b/docs/federation-deployment.md
deleted file mode 100644
index e514fd6..0000000
--- a/docs/federation-deployment.md
+++ /dev/null
@@ -1,188 +0,0 @@
1 -# Federation: deployment guide
2 -
3 -How to enable and operate ForgeFed/ActivityPub federation on a git-shark
4 -instance. For what federation offers end users, see the
5 -[user guide](federation-users.md); for a general deployment walkthrough, see
6 -[Getting Started](getting-started.md).
7 -
8 -Federation is **off by default** and fails closed: nothing is emitted or
9 -accepted until it is explicitly enabled *and* correctly configured.
10 -
----
11 -
12 -## Configuration
13 -
14 -All settings come from environment variables:
15 -
16 -| Variable | Default | Meaning |
17 -|---|---|---|
18 -| `GITSHARK_FEDERATION_ENABLED` | `false` | Master switch |
19 -| `GITSHARK_FEDERATION_BASE_URL` | β€” | Public HTTPS origin of this instance, e.g. `https://shark.example.com`. Actor IDs derive from it |
20 -| `GITSHARK_FEDERATION_PEER_ALLOWLIST` | β€” (empty) | Comma-separated peer **hosts**, e.g. `shark.other.org,forge.example`. Empty denies every remote peer |
21 -| `GITSHARK_FEDERATION_MAX_ATTEMPTS` | `8` | Delivery attempts before an outbound activity is dead-lettered |
22 -| `GITSHARK_FEDERATION_DEV_ALLOW_INSECURE` | `false` | **Dev only.** Permits `http://` and loopback/private targets. Never in production |
23 -
24 -Minimal production setup:
25 -
26 -```bash
27 -GITSHARK_FEDERATION_ENABLED=true
28 -GITSHARK_FEDERATION_BASE_URL=https://shark.example.com
29 -GITSHARK_FEDERATION_PEER_ALLOWLIST=shark.other.org
30 -```
31 -
32 -> **`base-url` is permanent.** Actor IDs (`https://<base-url>/ap/...`) are
33 -> published to other servers and cannot change without breaking every existing
34 -> follow relationship. Set a real, stable, non-loopback HTTPS origin before
35 -> enabling. git-shark refuses to operate federation (`operational = false`) while
36 -> the base URL is unset, loopback, or otherwise unusable β€” the switch alone is
37 -> not enough.
38 -
39 -### The peer allowlist
40 -
41 -Federation is mutual and allowlist-bound in **both** directions:
42 -
43 -- **Inbound**: an activity is only accepted if its HTTP Signature's key belongs
44 - to an actor on an allowlisted host. Everything else is `401`.
45 -- **Outbound**: actor fetches, WebFinger lookups, and deliveries only go to
46 - allowlisted hosts.
47 -
48 -Matching is by exact, case-insensitive host name. Both instances must allowlist
49 -each other, or follows stay Pending forever.
50 -
----
51 -
52 -## What gets exposed
53 -
54 -With federation operational, these endpoints are public (no login):
55 -
56 -| Endpoint | Purpose |
57 -|---|---|
58 -| `GET /.well-known/webfinger?resource=acct:…` | Actor discovery (`user@host`, `owner/name@host`; the host part may be bare or `host:port`) |
59 -| `GET /ap/repos/{owner}/{name}` | ForgeFed `Repository` actor (public repos only) |
60 -| `GET /ap/users/{username}` | ActivityPub `Person` actor |
61 -| `GET /ap/instance` | Instance `Application` actor |
62 -| `GET …/outbox`, `…/followers` | Activity and follower collections |
63 -| `POST …/inbox` | Signed inbound activities (repo, user, and instance inboxes) |
64 -
65 -Private repositories are never exposed. Inbound posts are verified (HTTP
66 -Signature, body digest, date skew), deduplicated by activity id, then dispatched.
67 -
----
68 -
69 -## Reverse proxy requirements
70 -
71 -Federation signs and verifies the `Host` header and the raw request path.
72 -The proxy in front of git-shark must therefore:
73 -
74 -- **Preserve the original `Host` header** (`proxy_set_header Host $host;` in
75 - nginx; Caddy does this by default). A rewritten host breaks signature
76 - verification on every inbound activity.
77 -- Pass `/ap/*` and `/.well-known/webfinger` through **without** auth,
78 - path rewriting, or body buffering surprises.
79 -- Terminate TLS β€” peers will only talk `https` to you.
80 -
81 -The existing `quarkus.http.proxy.*` settings (already on in the default config)
82 -make git-shark trust `X-Forwarded-*` so actor URLs and signature reconstruction
83 -use the external origin.
84 -
----
85 -
86 -## Keys and signing
87 -
88 -- Every local actor (each public repository, each user, plus the instance) gets
89 - an **RSA-2048 keypair**, generated on first use and stored in the
90 - `federation_keys` table. There is no key configuration and currently no
91 - rotation mechanism; protect your database accordingly.
92 -- Outbound activities are signed with draft-cavage HTTP Signatures
93 - (`SHA256withRSA`), key id `<actor-id>#main-key`.
94 -- Outbound follows are signed as the **user's `Person` actor**; `Accept` and
95 - `Push` fan-out are signed as the repository actor.
96 -
----
97 -
98 -## Delivery queue and retries
99 -
100 -Outbound activities go through a persisted queue (`federation_delivery` table),
101 -drained every 10 seconds:
102 -
103 -- Failures retry with exponential backoff: 1m, 2m, 4m, … capped at 1h.
104 -- After `GITSHARK_FEDERATION_MAX_ATTEMPTS` (default 8) the row is dead-lettered
105 - as `FAILED` with the last error preserved β€” it will not retry again.
106 -- States: `PENDING` β†’ `DELIVERED` or `FAILED`.
107 -
108 -Monitoring queries worth having:
109 -
110 -```sql
--- stuck or failing deliveries
111 -select target_inbox, attempts, state, last_error, next_attempt_at
112 -from federation_delivery
113 -where state <> 'DELIVERED'
114 -order by created_at desc;
115 -
--- follow relationships still waiting for the remote's Accept
116 -select remote_actor_id, state, created_at from remote_follows where state = 'PENDING';
117 -```
118 -
----
119 -
120 -## Data stored for federation
121 -
122 -| Table | Contents |
123 -|---|---|
124 -| `federation_keys` | Local actor keypairs (public + private PEM) |
125 -| `remote_actors` | Cache of fetched remote actors (inbox, public key; 6h TTL) |
126 -| `repository_followers` | Remote actors following local repositories |
127 -| `remote_follows` | Local users' follows of remote repositories (`PENDING`/`ACCEPTED`) |
128 -| `received_pushes` | `Push` activities received from followed repositories (feed) |
129 -| `federation_outbox` / `federation_inbox` | Published activities / inbound dedup log |
130 -| `federation_delivery` | Outbound delivery queue |
131 -
----
132 -
133 -## SSRF protection
134 -
135 -All outbound federation traffic (actor fetches, WebFinger, deliveries) passes a
136 -URL guard: HTTPS only, host must be allowlisted, and the resolved address must
137 -not be loopback, link-local, private, or multicast. `dev-allow-insecure` relaxes
138 -the scheme and address checks **but never the allowlist**.
139 -
----
140 -
141 -## Local two-host trial (dev)
142 -
143 -To try federation on one machine, run two dev instances that allowlist each
144 -other:
145 -
146 -```bash
147 -# terminal A
148 -GITSHARK_FEDERATION_ENABLED=true \
149 -GITSHARK_FEDERATION_BASE_URL=http://localhost:8080 \
150 -GITSHARK_FEDERATION_PEER_ALLOWLIST=localhost \
151 -GITSHARK_FEDERATION_DEV_ALLOW_INSECURE=true \
152 -./mvnw quarkus:dev
153 -
154 -# terminal B (second checkout or worktree)
155 -GITSHARK_FEDERATION_ENABLED=true \
156 -GITSHARK_FEDERATION_BASE_URL=http://localhost:8081 \
157 -GITSHARK_FEDERATION_PEER_ALLOWLIST=localhost \
158 -GITSHARK_FEDERATION_DEV_ALLOW_INSECURE=true \
159 -GITSHARK_SSH_PORT=2223 \
160 -./mvnw quarkus:dev -Dquarkus.http.port=8081 -Ddebug=false \
161 - -Dquarkus.datasource.devservices.shared=false
162 -```
163 -
164 -On B, log in and follow `alice/demo@localhost:8080` (dev seed data) from the
165 -Following page. The follow flips to Accepted within ~20s; pushing to the demo
166 -repo on A makes the push appear under "Recent pushes" on B.
167 -
----
168 -
169 -## Troubleshooting
170 -
171 -| Symptom | Likely cause |
172 -|---|---|
173 -| Follow stays `PENDING` | Peer not allowlisted (either side), remote down, or delivery dead-lettered β€” check `federation_delivery.last_error` |
174 -| Inbound activities all `401` | Signer host not on your allowlist, or the reverse proxy rewrites the `Host` header |
175 -| `Could not resolve handle` | Remote host not allowlisted locally, WebFinger unreachable, or repo not public |
176 -| Actor documents `404` | Federation not operational: switch off, or `base-url` unset/loopback |
177 -| Deliveries fail with `Only https is allowed` | Peer published an `http://` inbox/actor URL and you are not in dev-insecure mode |
DELETE docs/federation-users.md +0 -91
diff --git a/docs/federation-users.md b/docs/federation-users.md
deleted file mode 100644
index 2581fc0..0000000
--- a/docs/federation-users.md
+++ /dev/null
@@ -1,96 +0,0 @@
1 -# Federation: user guide
2 -
3 -git-shark instances can talk to each other over [ForgeFed](https://forgefed.org)
4 -(ActivityPub). For you as a user that means: you can **follow a public repository
5 -that lives on another instance** and see its pushes on your own instance β€” no
6 -account on the remote server needed.
7 -
8 -> Federation is optional and off by default. If the features below are missing on
9 -> your instance, the operator has not enabled it (see the
10 -> [deployment guide](federation-deployment.md)).
11 -
----
12 -
13 -## What you can do
14 -
15 -- **Follow a remote public repository** and get its `Push` activity feed.
16 -- **Unfollow** it again.
17 -- **Be discovered**: your user and every public repository on your instance are
18 - visible to other ForgeFed servers.
19 -
20 -What is *not* federated (yet): issues, merge requests, comments, forks, and
21 -anything on private repositories. Private repositories are never exposed to
22 -federation at all.
23 -
----
24 -
25 -## Following a remote repository
26 -
27 -Open **Following** in the header navigation (you must be logged in), then enter
28 -either form of address:
29 -
30 -| Form | Example | When to use |
31 -|---|---|---|
32 -| Handle | `alice/demo@shark.example.com` | You know owner, name, and host |
33 -| Actor URL | `https://shark.example.com/ap/repos/alice/demo` | You have a direct link |
34 -
35 -The handle is resolved via WebFinger on the remote host; both resolve to the same
36 -repository actor.
37 -
38 -After submitting, the follow appears in your list as **Pending**: your instance
39 -has sent a signed `Follow` and is waiting for the remote's `Accept`. Both
40 -directions go through delivery queues that run every few seconds, so expect
41 -**Pending β†’ Accepted within roughly half a minute**. Refresh the page to see the
42 -state change.
43 -
44 -If it stays Pending for long, the remote is unreachable, not allowlisted by your
45 -instance (or vice versa β€” federation requires **both** operators to allowlist each
46 -other), or the repository doesn't exist / isn't public. Your operator can check
47 -the delivery queue for the exact error.
48 -
49 -### Common errors when following
50 -
51 -| Message | Meaning |
52 -|---|---|
53 -| `Could not resolve handle` | WebFinger lookup failed: typo, host down, or host not on your instance's peer allowlist |
54 -| `Could not resolve remote repository` | Actor URL didn't fetch: not a ForgeFed actor, private, or blocked by the allowlist |
55 -| `Choose a username before following` | Your account hasn't finished onboarding β€” pick a username first |
56 -
----
57 -
58 -## The "Recent pushes" feed
59 -
60 -Once a follow is in place, pushes to the followed repository arrive as signed
61 -`Push` activities and show up in the **Recent pushes** section of the Following
62 -page: repository, ref, a summary like `Pushed 2 commit(s) to refs/heads/main`,
63 -and when it was received. The feed shows the newest 50 entries across everything
64 -you follow.
65 -
66 -Only pushes from repositories somebody on your instance follows are stored;
67 -everything else is dropped on arrival.
68 -
----
69 -
70 -## Unfollowing
71 -
72 -Hit **Unfollow** next to the entry. Your instance sends an `Undo(Follow)` to the
73 -remote so it stops delivering, and the entry disappears from your list.
74 -
----
75 -
76 -## Your federated identity
77 -
78 -When federation is enabled, you exist to other servers as an ActivityPub `Person`:
79 -
80 -- Actor: `https://<your-host>/ap/users/<username>`
81 -- WebFinger: `acct:<username>@<your-host>`
82 -
83 -Each public repository is a ForgeFed `Repository` actor
84 -(`https://<your-host>/ap/repos/<owner>/<name>`, WebFinger
85 -`acct:<owner>/<name>@<your-host>`). Remote users can follow your public
86 -repositories the same way you follow theirs; their servers receive your pushes
87 -automatically. Followers of a repository are public at
88 -`…/ap/repos/<owner>/<name>/followers`.
89 -
90 -Signing keys for your actors are generated and managed by the server β€” there is
91 -nothing for you to configure.
DELETE docs/getting-started.md +0 -349
diff --git a/docs/getting-started.md b/docs/getting-started.md
deleted file mode 100644
index 7ba95c7..0000000
--- a/docs/getting-started.md
+++ /dev/null
@@ -1,361 +0,0 @@
1 -# Getting Started: deploying git-shark with Docker Compose
2 -
3 -This guide takes you from an empty host to a running git-shark instance:
4 -
5 -1. A PostgreSQL database (metadata store).
6 -2. The git-shark application container (web UI + smart-HTTP git + embedded SSH).
7 -3. A TLS-terminating reverse proxy in front (git-shark requires HTTPS).
8 -4. An external OIDC provider for login (kanidm, Keycloak, or any OpenID Connect IdP).
9 -
10 -By the end you can browse the web UI over HTTPS, log in with OIDC, and clone/push
11 -over both `https://` and `ssh://`.
12 -
13 -> **git-shark has no built-in password login.** Authentication to the web UI is
14 -> delegated entirely to an OIDC provider. You must have one reachable (or stand one
15 -> up) before the app is usable. Git transport authenticates separately β€” personal
16 -> access tokens over HTTP Basic, SSH public keys over SSH.
17 -
----
18 -
19 -## Prerequisites
20 -
21 -- **Docker Engine 24+** and the **Docker Compose v2** plugin (`docker compose`, not the
22 - legacy `docker-compose`).
23 -- A **DNS name** pointing at the host (e.g. `gitshark.example.com`). OIDC redirect URIs
24 - and β€” if you enable federation β€” permanent actor IDs are derived from it.
25 -- An **OIDC provider** with an authorization-code client for git-shark. PKCE is required.
26 -- Ports **80/443** (reverse proxy) and **2222** (SSH git) reachable from your clients.
27 -
----
28 -
29 -## Step 1 β€” Get the application image
30 -
31 -Use the prebuilt image published to GitHub Container Registry β€” nothing to build:
32 -
33 -```bash
34 -docker pull ghcr.io/workaround-org/git-shark:latest
35 -```
36 -
37 -Pin a specific release instead of `latest` for reproducible deploys (e.g.
38 -`ghcr.io/workaround-org/git-shark:1.0.0`).
39 -
40 -The image listens on **8080** (HTTP) and, once configured, **2222** (SSH). It runs as
41 -UID `185` and reads all production settings from environment variables.
42 -
43 -> **Building it yourself instead.** git-shark ships a JVM Dockerfile, so from the repo
44 -> root you can build a local image and point the Compose file's `image:` at it:
45 ->
46 -> ```bash
47 -> ./mvnw package # produces target/quarkus-app/
48 -> docker build -f src/main/docker/Dockerfile.jvm -t git-shark:local .
49 -> ```
50 ->
51 -> For a smaller, faster-starting image build the native variant (`-Dnative` with
52 -> `Dockerfile.native-micro`).
53 -
----
54 -
55 -## Step 2 β€” Register the OIDC client
56 -
57 -git-shark uses the OIDC **authorization code flow** with **PKCE**. Create a confidential
58 -client at your IdP and note three things: the **issuer/discovery URL**, the **client ID**,
59 -and the **client secret**. Set the redirect URI to your public origin with a trailing
60 -slash: `https://gitshark.example.com/`.
61 -
62 -### kanidm example
63 -
64 -```bash
65 -kanidm system oauth2 create git-shark "Git Shark" https://gitshark.example.com
66 -kanidm system oauth2 add-redirect-url git-shark https://gitshark.example.com/
67 -kanidm group create gitshark_users
68 -kanidm group add-members gitshark_users <your-user>
69 -kanidm system oauth2 update-scope-map git-shark gitshark_users openid profile email
70 -kanidm system oauth2 show-basic-secret git-shark # -> client secret
71 -```
72 -
73 -The auth-server URL for kanidm is `https://<kanidm-host>/oauth2/openid/git-shark`.
74 -
75 -### Keycloak / other IdPs
76 -
77 -Create a confidential client with:
78 -- Standard flow (authorization code) enabled, PKCE `S256` required.
79 -- Redirect URI `https://gitshark.example.com/`.
80 -- Scopes `openid profile email`.
81 -
82 -The auth-server URL is the realm issuer, e.g.
83 -`https://keycloak.example.com/realms/<realm>`.
84 -
----
85 -
86 -## Step 3 β€” Generate the encryption secrets
87 -
88 -Two secrets encrypt the PKCE state cookie and the post-login session cookie. Each must
89 -be **at least 32 characters** (Quarkus minimum). Generate them once and keep them stable
90 -β€” rotating them invalidates in-flight logins and existing sessions.
91 -
92 -```bash
93 -openssl rand -hex 16 # 32 hex chars β€” run twice, for the two secrets below
94 -```
95 -
----
96 -
97 -## Step 4 β€” Write the `.env` file
98 -
99 -Compose reads these values. Put the file next to `docker-compose.yml`, keep it out of
100 -version control (it holds secrets).
101 -
102 -```dotenv
103 -# --- Public origin ---
104 -APP_DOMAIN=gitshark.example.com
105 -
106 -# --- PostgreSQL ---
107 -POSTGRES_DB=gitshark
108 -POSTGRES_USER=gitshark
109 -POSTGRES_PASSWORD=change-me-strong-db-password
110 -
111 -# --- OIDC (from Step 2) ---
112 -OIDC_AUTH_SERVER_URL=https://idm.example.com/oauth2/openid/git-shark
113 -OIDC_CLIENT_ID=git-shark
114 -OIDC_CLIENT_SECRET=the-basic-secret-from-your-idp
115 -
116 -# --- OIDC cookie encryption (from Step 3, >= 32 chars each) ---
117 -OIDC_STATE_SECRET=paste-first-openssl-rand-output
118 -OIDC_TOKEN_STATE_SECRET=paste-second-openssl-rand-output
119 -```
120 -
----
121 -
122 -## Step 5 β€” The Compose file
123 -
124 -```yaml
125 -name: git-shark
126 -
127 -services:
128 - db:
129 - image: postgres:17
130 - restart: unless-stopped
131 - environment:
132 - POSTGRES_DB: ${POSTGRES_DB}
133 - POSTGRES_USER: ${POSTGRES_USER}
134 - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
135 - volumes:
136 - - db-data:/var/lib/postgresql/data
137 - healthcheck:
138 - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
139 - interval: 10s
140 - timeout: 5s
141 - retries: 5
142 -
143 - app:
144 - image: ghcr.io/workaround-org/git-shark:latest # or a pinned tag; see Step 1
145 - restart: unless-stopped
146 - depends_on:
147 - db:
148 - condition: service_healthy
149 - environment:
150 - # --- Datasource ---
151 - QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://db:5432/${POSTGRES_DB}
152 - QUARKUS_DATASOURCE_USERNAME: ${POSTGRES_USER}
153 - QUARKUS_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD}
154 - # --- OIDC ---
155 - QUARKUS_OIDC_AUTH_SERVER_URL: ${OIDC_AUTH_SERVER_URL}
156 - QUARKUS_OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
157 - QUARKUS_OIDC_CREDENTIALS_SECRET: ${OIDC_CLIENT_SECRET}
158 - QUARKUS_OIDC_AUTHENTICATION_STATE_SECRET: ${OIDC_STATE_SECRET}
159 - QUARKUS_OIDC_TOKEN_STATE_ENCRYPTION_SECRET: ${OIDC_TOKEN_STATE_SECRET}
160 - # --- Storage & SSH ---
161 - GITSHARK_STORAGE_ROOT: /data/repositories
162 - GITSHARK_SSH_HOST_KEY: /data/ssh/host-key
163 - GITSHARK_SSH_PORT: "2222"
164 - ports:
165 - - "2222:2222" # SSH git access, published directly
166 - volumes:
167 - - repos:/data/repositories # bare git repositories
168 - - ssh:/data/ssh # persistent SSH host key
169 - healthcheck:
170 - test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/8080 && echo ok >&3"]
171 - interval: 15s
172 - timeout: 5s
173 - retries: 10
174 - start_period: 40s
175 -
176 -volumes:
177 - db-data:
178 - repos:
179 - ssh:
180 -```
181 -
182 -Notes:
183 -
184 -- **Separate volumes for `/data/repositories` and `/data/ssh`** so Docker creates both
185 - mount points with the right ownership β€” no init container or `mkdir` needed. The SSH
186 - host key is generated on first boot and persists across restarts (so client
187 - `known_hosts` entries stay valid).
188 -- **HTTP port 8080 is not published** β€” it's reached through the reverse proxy on the
189 - Compose network (Step 6). Only SSH (2222) is exposed directly.
190 -- **Single app replica.** git-shark keeps git state on a `ReadWriteOnce`-style filesystem
191 - volume; do not scale `app` beyond one instance.
192 -- Flyway migrates the schema automatically at startup (`migrate-at-start=true`), so the
193 - database needs no manual initialization beyond an empty database + owner.
194 -
----
195 -
196 -## Step 6 β€” TLS reverse proxy (required)
197 -
198 -git-shark always builds **HTTPS** OIDC redirect URIs and trusts `X-Forwarded-*` headers
199 -(`force-redirect-https-scheme=true`, `proxy-address-forwarding=true`). It is designed to
200 -run behind a TLS-terminating proxy β€” plain HTTP will break the login redirect.
201 -
202 -Add a Caddy service to the Compose file β€” it fetches and renews a Let's Encrypt
203 -certificate automatically:
204 -
205 -```yaml
206 - proxy:
207 - image: caddy:2
208 - restart: unless-stopped
209 - depends_on:
210 - - app
211 - ports:
212 - - "80:80"
213 - - "443:443"
214 - volumes:
215 - - ./Caddyfile:/etc/caddy/Caddyfile:ro
216 - - caddy-data:/data
217 - - caddy-config:/config
218 -```
219 -
220 -Add `caddy-data:` and `caddy-config:` to the top-level `volumes:` block, then create a
221 -`Caddyfile` next to the Compose file:
222 -
223 -```caddyfile
224 -gitshark.example.com {
225 - reverse_proxy app:8080
226 -}
227 -```
228 -
229 -Caddy forwards `X-Forwarded-Proto`/`-For`/`-Host` by default, which is exactly what
230 -git-shark's OIDC redirect construction needs. Using Traefik or nginx instead is fine β€”
231 -just terminate TLS and forward those headers.
232 -
----
233 -
234 -## Step 7 β€” Bring it up
235 -
236 -```bash
237 -docker compose up -d
238 -docker compose logs -f app # watch for "Listening on: http://0.0.0.0:8080"
239 -```
240 -
241 -Then open `https://gitshark.example.com/`, click **Log in**, and complete the OIDC flow.
242 -On first login you're redirected to `/onboarding` to pick a URL-safe handle
243 -(`^[a-z0-9][a-z0-9-]{0,38}$`) β€” this handle, not the IdP username, appears in all repo,
244 -SSH, and federation URLs.
245 -
----
246 -
247 -## Step 8 β€” Verify git access
248 -
249 -**HTTP** (anonymous read on public repos; push/private read use a personal access token
250 -as the HTTP Basic password β€” create one under *Access tokens* in the UI):
251 -
252 -```bash
253 -git clone https://gitshark.example.com/git/<owner>/<repo>.git
254 -```
255 -
256 -**SSH** (public-key only; add your key under *SSH keys* in the UI):
257 -
258 -```bash
259 -git clone ssh://git@gitshark.example.com:2222/<owner>/<repo>.git
260 -```
261 -
262 -> Want bare `git@gitshark.example.com` without the `:2222`? Publish the container's 2222
263 -> on host port 22 (`"22:2222"`) β€” but only if the host's own sshd isn't already using 22.
264 -
----
265 -
266 -## Configuration reference
267 -
268 -Every value below is an environment variable on the `app` service. Defaults come from
269 -`src/main/resources/application.properties`.
270 -
271 -| Variable | Required | Default | Purpose |
272 -|---|---|---|---|
273 -| `QUARKUS_DATASOURCE_JDBC_URL` | βœ… | β€” | PostgreSQL JDBC URL |
274 -| `QUARKUS_DATASOURCE_USERNAME` | βœ… | β€” | DB user |
275 -| `QUARKUS_DATASOURCE_PASSWORD` | βœ… | β€” | DB password |
276 -| `QUARKUS_OIDC_AUTH_SERVER_URL` | βœ… | β€” | OIDC issuer / discovery URL |
277 -| `QUARKUS_OIDC_CLIENT_ID` | βœ… | β€” | OIDC client ID |
278 -| `QUARKUS_OIDC_CREDENTIALS_SECRET` | βœ… | β€” | OIDC client secret |
279 -| `QUARKUS_OIDC_AUTHENTICATION_STATE_SECRET` | βœ… | β€” | Encrypts PKCE state cookie (β‰₯ 32 chars) |
280 -| `QUARKUS_OIDC_TOKEN_STATE_ENCRYPTION_SECRET` | βœ… | β€” | Encrypts session/token cookie (β‰₯ 32 chars) |
281 -| `GITSHARK_STORAGE_ROOT` | β€” | `data/repositories` | On-disk bare-repo root |
282 -| `GITSHARK_SSH_HOST_KEY` | β€” | `data/ssh/host-key` | Persistent SSH host key path |
283 -| `GITSHARK_SSH_PORT` | β€” | `2222` | Embedded SSH server port |
284 -| `GITSHARK_FEDERATION_ENABLED` | β€” | `false` | Turn on ForgeFed/ActivityPub |
285 -| `GITSHARK_FEDERATION_BASE_URL` | β€” | β€” | Public HTTPS origin; permanent actor-ID base |
286 -| `GITSHARK_FEDERATION_PEER_ALLOWLIST` | β€” | β€” | Comma-separated peer hosts (empty denies all) |
287 -| `GITSHARK_FEDERATION_MAX_ATTEMPTS` | β€” | `8` | Outbound delivery retry cap |
288 -| `GITSHARK_FEDERATION_DEV_ALLOW_INSECURE` | β€” | `false` | Dev only: allow http/loopback peers |
289 -
290 -### Optional: federation (ForgeFed)
291 -
292 -Off by default. Enabling it publishes **permanent** actor IDs derived from
293 -`GITSHARK_FEDERATION_BASE_URL`, so set a real, stable, non-loopback HTTPS origin before
294 -turning it on β€” git-shark refuses to emit actor documents otherwise.
295 -
296 -```yaml
297 - GITSHARK_FEDERATION_ENABLED: "true"
298 - GITSHARK_FEDERATION_BASE_URL: https://gitshark.example.com
299 - GITSHARK_FEDERATION_PEER_ALLOWLIST: peer-a.example,peer-b.example
300 -```
301 -
302 -Inbound activities need a valid HTTP Signature from an allowlisted peer; outbound
303 -fetches are HTTPS-only, allowlist-bound, and SSRF-guarded. Never set
304 -`GITSHARK_FEDERATION_DEV_ALLOW_INSECURE=true` in production.
305 -
----
306 -
307 -## Operations
308 -
309 -**Backups** β€” two things hold state:
310 -- The `db-data` volume (metadata: users, repo records, issues, MRs, comments).
311 -- The `repos` volume (the actual git objects).
312 -
313 -Back both up together and consistently. A logical DB dump:
314 -
315 -```bash
316 -docker compose exec db pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" > gitshark-db.sql
317 -```
318 -
319 -Snapshot the `repos` volume with your host's volume/snapshot tooling while the app is
320 -quiesced (or accept crash-consistent snapshots β€” bare repos tolerate them well).
321 -
322 -**Upgrades** β€” pull the new image and recreate the app:
323 -
324 -```bash
325 -docker compose pull app
326 -docker compose up -d app
327 -```
328 -
329 -Flyway applies any new migrations on startup. Because the app uses the `Recreate`
330 -pattern (one writer, filesystem state), a brief downtime during redeploy is expected.
331 -
332 -**Logs & health**:
333 -
334 -```bash
335 -docker compose ps
336 -docker compose logs -f app
337 -```
338 -
----
339 -
340 -## Troubleshooting
341 -
342 -| Symptom | Likely cause |
343 -|---|---|
344 -| Login redirects to `http://…` or loops | Proxy not forwarding `X-Forwarded-Proto`, or you hit the app over plain HTTP. Front it with TLS (Step 6). |
345 -| Boot fails on OIDC discovery | `QUARKUS_OIDC_AUTH_SERVER_URL` wrong/unreachable, or IdP demands HTTPS the app can't reach. |
346 -| App exits complaining about secret length | `*_STATE_SECRET` shorter than 32 chars. Regenerate with `openssl rand -hex 16`. |
347 -| SSH host key changed after redeploy | The `ssh` volume wasn't persisted β€” confirm it's a named volume, not a throwaway mount. |
348 -| `git push` over HTTP rejected | Use a personal access token (from *Access tokens*) as the Basic-auth password, not your OIDC password. |
349 -| Schema validation error at start | DB not empty / migrated by a different tool. git-shark's Flyway owns the schema; start from an empty database. |
ADD docs/maintainers/forgefed.md +208 -0
diff --git a/docs/maintainers/forgefed.md b/docs/maintainers/forgefed.md
new file mode 100644
index 0000000..0ef34ed
--- /dev/null
+++ b/docs/maintainers/forgefed.md
@@ -0,0 +1,208 @@
1 +# ForgeFed in git-shark: architecture and implementation notes
2 +
3 +Maintainer-facing documentation for the federation subsystem: how it is built,
4 +why it is built that way, what works today, and what is still missing. For
5 +operating it see the [deployment guide](../admins/federation.md); for the user
6 +view see the [user guide](../users/federation.md).
7 +
8 +git-shark speaks [ForgeFed](https://forgefed.org), the forge-federation
9 +vocabulary on top of [ActivityPub](https://www.w3.org/TR/activitypub/). The
10 +implementation is hand-rolled on Jackson + JAX-RS β€” there is no maintained Java
11 +ForgeFed/ActivityPub library, and the subset we need (a handful of activity
12 +types, HTTP Signatures, WebFinger) is small enough that a dependency would cost
13 +more than it saves.
14 +
15 +---
16 +
17 +## Component map
18 +
19 +Everything lives in `src/main/java/de/workaround/federation/`:
20 +
21 +| Component | Role |
22 +|---|---|
23 +| `FederationConfig` | Central switch + validated config. `operational()` is the guard every code path checks |
24 +| `ActorUris` / `LocalActors` | Build and parse actor IDs from the base URL |
25 +| `ActorDocuments` | JSON builders for actor documents and activities (`Follow`, `Accept`, `Undo`, `Push`) |
26 +| `ActorKeyService` | Lazy per-actor RSA keypair creation, stored in `federation_keys` |
27 +| `HttpSignatures` | draft-cavage signing and verification (SHA256withRSA) |
28 +| `RemoteUrlGuard` | SSRF guard for every outbound URL |
29 +| `ActivityPubClient` | Signed fetches: remote actors (cached), public keys, WebFinger resolution |
30 +| `WebFingerResource` | `GET /.well-known/webfinger` β€” actor discovery |
31 +| `ActivityPubResource` | `GET /ap/**` β€” actor documents, outbox and followers collections |
32 +| `InboxResource` / `InboxService` | `POST /ap/**/inbox` β€” verify signature, dedup, dispatch |
33 +| `ActivityDispatcher` | Routes verified inbound activities by `type` to a handler |
34 +| `FollowHandler` / `UndoHandler` | Inbound: remote actor (un)follows a local repository |
35 +| `AcceptHandler` | Inbound: remote accepts a `Follow` we sent β€” flips our follow to `ACCEPTED` |
36 +| `PushHandler` | Inbound: stores `Push` from repositories local users follow |
37 +| `RemoteFollowService` | Outbound: follow/unfollow a remote repository, push feed query |
38 +| `FederationPushService` | Outbound: fans out `Push` to followers from the git post-receive hook |
39 +| `DeliveryService` | Persisted outbound queue with retry/backoff/dead-letter |
40 +
41 +Web UI: `web/FollowingResource` + Qute template (`/following` page). Persistence
42 +in `model/` (`FederationKey`, `RemoteActor`, `RepositoryFollower`,
43 +`RemoteFollow`, `ReceivedPush`, `InboxActivity`, `OutboxActivity`,
44 +`FederationDelivery`), schema in `db/migration/V2__federation.sql` and
45 +`V9__federation_following.sql`.
46 +
47 +## Actor model
48 +
49 +Three local actor types, all rooted at `gitshark.federation.base-url`:
50 +
51 +| Actor | Type | ID | WebFinger |
52 +|---|---|---|---|
53 +| Public repository | ForgeFed `Repository` | `/ap/repos/{owner}/{name}` | `acct:owner/name@host` |
54 +| User | `Person` | `/ap/users/{username}` | `acct:username@host` |
55 +| Instance | `Application` | `/ap/instance` | β€” |
56 +
57 +Each has its own inbox; repositories and users also expose `outbox` and
58 +(repositories) `followers` collections. Private repositories are invisible to
59 +every federation endpoint β€” visibility is checked at the resource layer, not
60 +filtered in templates.
61 +
62 +## Data flow
63 +
64 +**Outbound follow** (`RemoteFollowService.follow`): input is a handle
65 +(`owner/name@host`, resolved via WebFinger) or a direct actor URL β†’ fetch the
66 +remote actor β†’ persist a `RemoteFollow` row in state `PENDING` β†’ enqueue a
67 +signed `Follow`. The remote's `Accept` arrives at the user's inbox and
68 +`AcceptHandler` flips the row to `ACCEPTED` β€” but only when the accepting actor
69 +matches the one we followed. Unfollow enqueues `Undo(Follow)` and deletes the
70 +row.
71 +
72 +**Inbound follow** (`FollowHandler`): a remote actor follows one of our public
73 +repositories β†’ persist `RepositoryFollower` β†’ enqueue a signed `Accept` back.
74 +`UndoHandler` removes the follower again.
75 +
76 +**Push fan-out** (`FederationPushService`): both git transports (smart HTTP and
77 +SSH) call `onPush` from the JGit post-receive hook. For each updated branch on a
78 +PUBLIC repository it builds a ForgeFed `Push` (old/new ref ids plus up to 50
79 +commit ids), records it in the outbox, and enqueues one delivery per follower.
80 +Runs on a git worker thread, so it activates its own CDI request context and
81 +never lets an exception escape into the git path.
82 +
83 +**Inbound push** (`PushHandler`): stored into `received_pushes` only when at
84 +least one local user follows the sending actor; everything else is dropped.
85 +Deduplicated by activity id (application check + DB unique constraint). The
86 +`/following` page renders the newest 50 across a user's follows.
87 +
88 +**Inbound pipeline** (`InboxService.receive`) β€” fails closed, any failure is a
89 +`401` with no processing:
90 +
91 +1. Parse the `Signature` header; extract the signer host from its `keyId`.
92 +2. Reject unless that host is on the peer allowlist.
93 +3. Fetch the signer's public key (via the cached remote-actor fetch, itself
94 + SSRF-guarded and allowlist-bound).
95 +4. Verify the signature over method, raw path, headers, and body digest, with
96 + date-skew checking.
97 +5. Dedup by activity id in `federation_inbox`, then dispatch inside the same
98 + transaction. Unknown activity types are recorded and ignored.
99 +
100 +**Outbound delivery** (`DeliveryService`): activities are never sent inline.
101 +They are persisted to `federation_delivery` and drained by a scheduler every
102 +10 s; failures retry with exponential backoff (1 m doubling to a 1 h cap) and
103 +dead-letter as `FAILED` after `max-attempts` (default 8), preserving the last
104 +error for operators.
105 +
106 +## Implementation decisions
107 +
108 +Decisions that shaped the subsystem, with the reasoning β€” so future changes
109 +don't accidentally undo them:
110 +
111 +- **Fail-closed operational gate.** Actor IDs are absolute URLs derived from
112 + `base-url` and are *permanent* once another server has stored them. So
113 + federation refuses to emit anything until `enabled=true` AND a valid,
114 + non-loopback base URL is set (`FederationConfig.operational()`). The
115 + alternative β€” defaulting to whatever host the request came in on β€” would
116 + publish throwaway IDs that break every follow relationship on rename.
117 +- **Mutual peer allowlist, both directions.** Inbound activities must be signed
118 + by a key on an allowlisted host; outbound fetches/deliveries only go to
119 + allowlisted hosts. Empty list denies everything. This bounds the first
120 + git-shark↔git-shark rollout to explicitly trusted peers instead of open
121 + federation; open federation is a policy decision to make later, not a default.
122 +- **SSRF guard on every outbound URL** (`RemoteUrlGuard`): HTTPS only, host
123 + allowlisted, resolved address must not be loopback/link-local/private/
124 + multicast. Federation fetches URLs supplied by remote servers (inbox URLs,
125 + key ids, WebFinger targets), which is a textbook SSRF vector into the
126 + deployment network. `dev-allow-insecure` relaxes scheme and address classes
127 + for single-machine trials but **never** the allowlist.
128 +- **Persisted delivery queue instead of fire-and-forget.** Remote instances go
129 + down; a synchronous send from a request (or worse, a git hook) thread would
130 + lose activities and add latency to pushes. Queue + backoff + dead-letter makes
131 + delivery observable (`federation_delivery` is the operator's debugging
132 + surface) and keeps the git path fast.
133 +- **Per-actor RSA-2048 keys, generated lazily, stored in the DB**
134 + (`federation_keys`, keyId `<actor-id>#main-key`). Lazy generation means no
135 + key ceremony at repo/user creation; DB storage means no filesystem key
136 + management in a container deployment. Cost: the DB now holds private keys β€”
137 + documented in the deployment guide.
138 +- **Signing identity: `Follow` is signed by the user's `Person` actor**, since
139 + following is a user action; `Accept` and `Push` are signed by the repository
140 + actor that owns the relationship. (Decision recorded in issue #3.)
141 +- **draft-cavage HTTP Signatures**, not RFC 9421 β€” it is what the existing
142 + ForgeFed/Fediverse ecosystem (Forgejo, Mastodon et al.) actually verifies.
143 + The signature covers the `Host` header and raw path, which is why the reverse
144 + proxy must preserve `Host`.
145 +- **WebFinger interop: send bare-host, accept both.** The client builds the
146 + `acct:` resource with the bare hostname (the WebFinger standard form) while
147 + addressing the endpoint at `host[:port]`; the server accepts both bare-host
148 + and `host:port` acct forms. This keeps us compatible with implementations
149 + that compare the bare host strictly *and* with port-based multi-instance dev
150 + setups.
151 +- **Inbound processing is idempotent by activity id** at two layers: the
152 + `federation_inbox` dedup log short-circuits redelivery, and consumers with
153 + their own tables (e.g. `received_pushes`) additionally carry a unique
154 + constraint. Remote queues retry; every handler must tolerate replays.
155 +- **Store-and-ignore unknown activity types.** Dispatch is a `switch` on
156 + `type` with a debug-logged default. Unknown activities still land in the
157 + dedup log, so adding a handler later never double-processes history.
158 +- **Server-rendered UI only.** The `/following` page is a Qute template with
159 + plain form POSTs, like the rest of git-shark β€” no client-side JS, state
160 + changes visible on refresh.
161 +- **Remote actor cache** (`remote_actors`, 6 h TTL) so signature verification
162 + and fan-out don't re-fetch actor documents per activity.
163 +
164 +## What works today
165 +
166 +- Actor documents and WebFinger discovery for repositories, users, and the
167 + instance; outbox and followers collections.
168 +- Inbound `Follow`/`Undo(Follow)` on public repositories, answered with a
169 + signed `Accept` (remote users can follow local repos).
170 +- `Push` fan-out to remote followers from both git transports.
171 +- Outbound follow/unfollow of remote repositories by handle or actor URL,
172 + including `Accept` confirmation tracking (`PENDING` β†’ `ACCEPTED`) and the
173 + received-pushes feed β€” the `/following` UI covers all of it.
174 +- HTTP Signature signing/verification, per-actor keys, inbound dedup, peer
175 + allowlist, SSRF guard, delivery queue with retry and dead-letter.
176 +- Tested git-shark↔git-shark, including a scripted local two-host trial (see
177 + the deployment guide). Interop with Forgejo/Vervis is expected via WebFinger
178 + bare-host handling but **untested**.
179 +
180 +## What still needs to be implemented
181 +
182 +Protocol gaps:
183 +
184 +- **`Reject(Follow)` handling** β€” dispatcher ignores it, so a rejected outbound
185 + follow stays `PENDING` forever. Related: pending follows never expire and are
186 + not retried if the original `Follow` delivery dead-letters.
187 +- **Actor lifecycle activities** β€” no handling of `Delete` (remote account/repo
188 + removal), `Move`, or `Update` (key rotation of a peer actor; the 6 h cache
189 + masks it briefly, then verification fails until re-fetch).
190 +- **Shared inbox** β€” deliveries go per-follower inbox; N followers on one host
191 + mean N deliveries.
192 +- **ForgeFed beyond Push**: `Ticket` (federated issues), patches/merge-request
193 + offers, `Fork`, stars/watch semantics. Issues, MRs, and comments are
194 + local-only today.
195 +- **NodeInfo** endpoint and instance-level metadata for discovery.
196 +- **RFC 9421 signatures** as the ecosystem migrates (double-knocking).
197 +
198 +Operational gaps:
199 +
200 +- **Key rotation** for local actors β€” none; a leaked key means manual DB
201 + surgery.
202 +- **Rate limiting / abuse controls on the inbox** β€” the allowlist is currently
203 + the only throttle, fine for the closed rollout, insufficient for open
204 + federation.
205 +- **Follower/feed UI depth** β€” repository pages don't show remote followers;
206 + the push feed is a flat newest-50 with no pagination or per-repo filtering.
207 +- **Delivery observability** β€” dead-letters are only visible via SQL; no
208 + admin UI or metrics.
ADD docs/users/federation.md +96 -0
diff --git a/docs/users/federation.md b/docs/users/federation.md
new file mode 100644
index 0000000..52aa729
--- /dev/null
+++ b/docs/users/federation.md
@@ -0,0 +1,96 @@
1 +# Federation: user guide
2 +
3 +git-shark instances can talk to each other over [ForgeFed](https://forgefed.org)
4 +(ActivityPub). For you as a user that means: you can **follow a public repository
5 +that lives on another instance** and see its pushes on your own instance β€” no
6 +account on the remote server needed.
7 +
8 +> Federation is optional and off by default. If the features below are missing on
9 +> your instance, the operator has not enabled it (see the
10 +> [deployment guide](../admins/federation.md)).
11 +
12 +---
13 +
14 +## What you can do
15 +
16 +- **Follow a remote public repository** and get its `Push` activity feed.
17 +- **Unfollow** it again.
18 +- **Be discovered**: your user and every public repository on your instance are
19 + visible to other ForgeFed servers.
20 +
21 +What is *not* federated (yet): issues, merge requests, comments, forks, and
22 +anything on private repositories. Private repositories are never exposed to
23 +federation at all.
24 +
25 +---
26 +
27 +## Following a remote repository
28 +
29 +Open **Following** in the header navigation (you must be logged in), then enter
30 +either form of address:
31 +
32 +| Form | Example | When to use |
33 +|---|---|---|
34 +| Handle | `alice/demo@shark.example.com` | You know owner, name, and host |
35 +| Actor URL | `https://shark.example.com/ap/repos/alice/demo` | You have a direct link |
36 +
37 +The handle is resolved via WebFinger on the remote host; both resolve to the same
38 +repository actor.
39 +
40 +After submitting, the follow appears in your list as **Pending**: your instance
41 +has sent a signed `Follow` and is waiting for the remote's `Accept`. Both
42 +directions go through delivery queues that run every few seconds, so expect
43 +**Pending β†’ Accepted within roughly half a minute**. Refresh the page to see the
44 +state change.
45 +
46 +If it stays Pending for long, the remote is unreachable, not allowlisted by your
47 +instance (or vice versa β€” federation requires **both** operators to allowlist each
48 +other), or the repository doesn't exist / isn't public. Your operator can check
49 +the delivery queue for the exact error.
50 +
51 +### Common errors when following
52 +
53 +| Message | Meaning |
54 +|---|---|
55 +| `Could not resolve handle` | WebFinger lookup failed: typo, host down, or host not on your instance's peer allowlist |
56 +| `Could not resolve remote repository` | Actor URL didn't fetch: not a ForgeFed actor, private, or blocked by the allowlist |
57 +| `Choose a username before following` | Your account hasn't finished onboarding β€” pick a username first |
58 +
59 +---
60 +
61 +## The "Recent pushes" feed
62 +
63 +Once a follow is in place, pushes to the followed repository arrive as signed
64 +`Push` activities and show up in the **Recent pushes** section of the Following
65 +page: repository, ref, a summary like `Pushed 2 commit(s) to refs/heads/main`,
66 +and when it was received. The feed shows the newest 50 entries across everything
67 +you follow.
68 +
69 +Only pushes from repositories somebody on your instance follows are stored;
70 +everything else is dropped on arrival.
71 +
72 +---
73 +
74 +## Unfollowing
75 +
76 +Hit **Unfollow** next to the entry. Your instance sends an `Undo(Follow)` to the
77 +remote so it stops delivering, and the entry disappears from your list.
78 +
79 +---
80 +
81 +## Your federated identity
82 +
83 +When federation is enabled, you exist to other servers as an ActivityPub `Person`:
84 +
85 +- Actor: `https://<your-host>/ap/users/<username>`
86 +- WebFinger: `acct:<username>@<your-host>`
87 +
88 +Each public repository is a ForgeFed `Repository` actor
89 +(`https://<your-host>/ap/repos/<owner>/<name>`, WebFinger
90 +`acct:<owner>/<name>@<your-host>`). Remote users can follow your public
91 +repositories the same way you follow theirs; their servers receive your pushes
92 +automatically. Followers of a repository are public at
93 +`…/ap/repos/<owner>/<name>/followers`.
94 +
95 +Signing keys for your actors are generated and managed by the server β€” there is
96 +nothing for you to configure.

Keyboard shortcuts

?Show this help
g hGo home
EscClose dialog