π (docs): Restructure guides by audience and add ForgeFed maintainer notes
Changes
9 files changed, +875 -622
MODIFY
README.md
+3 -2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
-
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
@@ -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
@@ -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
@@ -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
@@ -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.