forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
19.7 KiB
1# forge
2
3forge is a self-hosted git forge written in Rust. It is a working name, and the project is not yet released.
4
5It runs as a single process (`forge`) that serves the web UI, git over HTTPS (smart HTTP), and git over SSH. Repositories are plain bare git repositories on disk. Account and repo metadata live in a SQLite/Turso database.
6
7## Status
8
9forge is at **Milestone 2** ("repo browsing and raw files"). It currently provides:
10
11- local accounts (argon2id passwords), with the first user created as admin
12- sessions, CSRF protection on every POST, and a `__Host-` session cookie
13- SSH public keys and personal access tokens (`forge_...`)
14- creating repos, with public or private visibility, collaborators, and an archive flag
15- clone and push over HTTPS and SSH
16- basic branch protection: the default branch cannot be deleted or force-pushed, and archived repos reject pushes
17- repo browsing: the repo home page with the rendered README, directory listings with the last commit per entry, file views with syntax highlighting and `#L12` line anchors, Markdown rendering with a source toggle, commit history (paginated), commit views with diffs, branch and tag lists, and `.tar.gz` / `.zip` archives
18- raw files served from a separate user-content origin (see [Raw user content]#raw-user-content)
19
20Not yet: issues, pull requests, webhooks, mirrors, CI. See [Roadmap]#roadmap.
21
22## Requirements
23
24- Rust stable, edition 2024
25- git 2.38 or newer (boot refuses to start with an older git)
26
27## Quick start
28
291. Copy the example config. forge reads `config/forge.toml` by default.
30
31 ```sh
32 cp config/forge.example.toml config/forge.toml
33 ```
34
352. Start the server.
36
37 ```sh
38 cargo run -p forge-web
39 ```
40
413. Create the admin account. Either open http://127.0.0.1:7660/setup (it works only while there are zero users), or use the CLI:
42
43 ```sh
44 cargo run -p forge-web -- admin create-user <username> --admin
45 ```
46
47 The CLI reads the password from `FORGE_PASSWORD` if set, or prompts for it twice. Passwords must be at least 12 characters. The first user is always an admin, even without `--admin`.
48
494. Add an SSH key at `/settings/keys`. Use an ed25519 key:
50
51 ```sh
52 ssh-keygen -t ed25519
53 ```
54
55 Paste the contents of the `.pub` file. RSA keys are rejected with a message asking for ed25519. The reason is that `ssh-rsa` keys sign with SHA-1, and forge disables SHA-1 signatures. ECDSA and security-key (`sk-`) variants are also accepted by the form.
56
575. To change only the capitalization of your username (for example `tahinli` to `Tahinli`), open `/settings/profile`. Any other change is refused. The instance operator can do the same from a shell with `cargo run -p forge-web -- admin set-username <current> <NewCase>`.
58
596. Create an access token at `/settings/tokens`. The token starts with `forge_` and is shown once. Store it somewhere safe.
60
617. Account security lives at `/settings/security`:
62 - **Password:** change it with the current password. The new one needs 10 to 1024 characters. Your other sessions are signed out; the one you are using stays signed in. Wrong current passwords count toward the same per-user login limit as failed logins.
63 - **Email:** change it with the current password. The address must contain one `@`, have no spaces, and be at most 254 characters.
64 - **Two-factor authentication (TOTP):** click "Enable 2FA", add the secret or `otpauth://` link to an authenticator app, and confirm with a 6-digit code. Ten single-use recovery codes are then shown once, so save them. Turning 2FA off needs the password and a code. Regenerating recovery codes needs a code. Codes are TOTP (RFC 6238, SHA-1, 6 digits, 30-second steps), and a code from one step either side of the current time is accepted. Each code works only once. With 2FA on, a correct password leads to `/login/2fa`, which asks for a code or a recovery code. The pending login lasts 5 minutes. Each user gets 10 second-factor attempts per 15 minutes.
65 - **Git over HTTPS is unaffected by 2FA.** It authenticates with access tokens (step 6), not with passwords or TOTP codes. Revoke a token at `/settings/tokens` if it leaks.
66
677. Create a repo at `/new`, then push over either transport:
68
69 ```sh
70 # SSH
71 git remote add origin ssh://git@127.0.0.1:2222/<owner>/<repo>.git
72 git push -u origin main
73
74 # HTTPS: username and token as the password
75 git remote add origin http://127.0.0.1:7660/<owner>/<repo>.git
76 git push -u origin main
77 ```
78
79## Configuration
80
81The file is TOML. Every key in `config/forge.example.toml` is listed below. Paths are relative to the working directory unless absolute.
82
83| Key | Example value | Meaning |
84|---|---|---|
85| `database` | `"./data/forge.db"` | SQLite/Turso database file. `admin create-user` creates its parent directory. |
86| `data_dir` | `"./data"` | Root for repos, instance key, gitconfig, hooks, and sockets. Created with mode 0700. |
87| `listen` | `"127.0.0.1:7660"` | HTTP listen address for the web UI and smart HTTP. |
88| `base_url` | `"http://127.0.0.1:7660"` | Public base URL used in links and clone URLs. Set this to your public `https://` URL in production. |
89| `user_content_base_url` | `"http://127.0.0.1:7661"` | Separate origin for raw user content. Set it to a different registrable domain in production (see [Raw user content]#raw-user-content). |
90| `user_content_listen` | `"127.0.0.1:7661"` | Address of the listener that serves raw user content. Your reverse proxy forwards the user-content hostname to it. |
91| `ssh_listen` | `"0.0.0.0:2222"` | SSH listen address for git over SSH. |
92| `ssh_public_host` | `"127.0.0.1"` | Host shown in SSH clone URLs. |
93| `ssh_public_port` | `2222` | Port shown in SSH clone URLs. |
94| `registration` | `"closed"` | Self-registration policy: `closed`, `invite`, or `open`. Default is `closed`. M1 has no sign-up page, so accounts come from `/setup`, the CLI, or `/admin/users`. |
95| `trusted_proxies` | `[]` | Addresses or CIDR networks (for example `["127.0.0.1"]`) whose `X-Forwarded-For` header is trusted. Set it to the address your reverse proxy connects from. Empty means the TCP peer is the client, so a spoofed header cannot change the client IP. Used for login and git throttling. |
96
97Production config normally lives at `/etc/forge/forge.toml` (see the systemd unit below).
98
99### Command line and environment
100
101```text
102forge [--config <path>] [serve | hook <pre-receive|post-receive> | runner <register|run|check> ... | admin create-user <username> [--admin] | admin set-username <current> <NewCase> | admin reset-password <username> | admin trash-list | admin trash-purge --older-than-days <N> | admin audit-purge --older-than-days <N> | admin delete-user <username> [--yes] | admin backup --out <DIR> [--include-key] [--repos-only | --db-only] | admin backup-verify <PATH> | admin fsck [--full] | admin lfs-gc | admin restore --from <SET> --data-dir <NEW_DIR>]
103```
104
105- `--config <path>` sets the config file. Precedence: `--config`, then `FORGE_CONFIG`, then `config/forge.toml`.
106- `serve` (the default when no subcommand is given) starts the server.
107- `hook <stage>` is called by git hook shims. You do not run it by hand.
108- `admin create-user <username> [--admin]` creates a user from the CLI.
109- `admin set-username <current> <NewCase>` changes only the capitalization of a user's username (`current` matches any case). Example: `forge admin set-username tahinli Tahinli`.
110- `admin reset-password <username>` sets a random 20-character temporary password, prints it once, and signs the user out of every session. Give the password to the user, who should change it at `/settings/security`. Nothing else changes. Use it when a user is locked out of their account. Two-factor settings are not changed, so the user still needs a code.
111- `admin trash-list` lists deleted repos in `data_dir/trash`, oldest first, as `<deleted_at>\t<repo id>\t<path>`.
112- `admin trash-purge --older-than-days N` permanently removes trash entries deleted at least N days ago. Nothing else is removed, and this cannot be undone.
113- `FORGE_PASSWORD` supplies the password for `admin create-user` without a prompt.
114- `RUST_LOG` sets the log filter (default `info`). Logs go to stderr.
115- `GET /healthz` returns `ok <build sha>`.
116
117## Data layout
118
119Everything under `data_dir`:
120
121| Path | Purpose |
122|---|---|
123| `forge.db`, `forge.db-wal`, `forge.db-shm` | Metadata database (users, sessions, keys, tokens, repos). Sits next to `database` in the example config. |
124| `forge.key` | Instance key. Encrypts sealed values. Created on first boot. |
125| `ssh_host_ed25519_key` | SSH host key. Created on first boot. |
126| `gitconfig` | Managed gitconfig written at boot. Do not edit by hand, it is rewritten at boot. |
127| `hooks/` | `pre-receive` and `post-receive` shims that run `forge hook <name>`. Rewritten at boot. |
128| `repos/ab/cd/<id>.git` | Bare repositories. `<id>` is the lowercased ULID; the path does not depend on the repo name. |
129| `internal.sock` | Unix socket used by git hooks to reach the server. Mode 0600. Removed on shutdown. |
130| `trash/<id>-<unix_ts>.git` | Repos deleted from the settings page (`/{owner}/{repo}/settings`). Each is a plain bare repository, moved here by a rename. The database rows are gone, so the repo no longer appears in forge. |
131
132### Recovering a deleted repo
133
134A deleted repo is not erased at once. Its bare directory is moved to `data_dir/trash/`, and only `admin trash-purge` removes it. To recover it, list the trash with `forge admin trash-list` and clone from the directory, which works without the server:
135
136```sh
137git clone --mirror /var/lib/forge/trash/<id>-<unix_ts>.git recovered.git
138```
139
140The clone restores all branches and tags. The owner, visibility, description and collaborators are not restored, so re-create the repo under the same name and push the mirror to it with `git push --mirror`.
141
142### Backups
143
144See [`docs/backup.md`]docs/backup.md for what a backup set contains, how to ship it off-site, how to restore, and the quarterly restore drill.
145
146- **`forge.key` must be backed up separately and stored safely, offline.** Without it, sealed values cannot be decrypted, and they are lost. Keep it out of the same backup set as the database if you can.
147- **Never copy a live `forge.db`** (or its `-wal`/`-shm` files) with `cp` or `rsync`. Take a consistent snapshot with `VACUUM INTO`, for example from the `sqlite3` shell while forge is running:
148
149 ```sql
150 VACUUM INTO '/var/backups/forge/forge-2026-10-15.db';
151 ```
152
153- **Repositories**: do not copy bare repos while pushes are in progress. Use `git bundle create --all` or `git push --mirror` to a copy.
154- **Unix socket path length**: `data_dir/internal.sock` must be shorter than 108 bytes, the unix socket path limit. Boot fails with an error otherwise. Choose a short `data_dir` (for example `/var/lib/forge`).
155
156## Deployment
157
158### systemd
159
160`deploy/forge.service` runs `/usr/local/bin/forge serve` as a dedicated `forge` user, with `FORGE_CONFIG=/etc/forge/forge.toml`, working directory `/var/lib/forge`, and a hardened sandbox (`ProtectSystem=strict`, `/var/lib/forge` and, if present, `/var/backups/forge` writable, no capabilities, `UMask=0077`).
161
162Typical setup:
163
164```sh
165sudo useradd --system --home /var/lib/forge --shell /usr/sbin/nologin forge
166sudo install -d -o forge -g forge /var/lib/forge /etc/forge
167sudo install -m 0640 -g forge config/forge.example.toml /etc/forge/forge.toml
168sudo install -m 0755 target/release/forge /usr/local/bin/forge
169sudo install -m 0644 deploy/forge.service /etc/systemd/system/forge.service
170sudo systemctl daemon-reload && sudo systemctl enable forge
171```
172
173Edit `/etc/forge/forge.toml` before starting:
174
175- `base_url`: public `https://` URL.
176- `user_content_base_url`: a separate registrable domain, not a subdomain of `base_url`.
177- `ssh_public_host` and `ssh_public_port`: public hostname and port.
178- `database` and `data_dir`: absolute paths under `/var/lib/forge`.
179- `trusted_proxies = ["127.0.0.1"]` when the reverse proxy runs on this host. Otherwise every client shares the proxy's IP for login and git throttling.
180- `[backup] dir`: under `/var/lib/forge` or `/var/backups/forge`. The unit makes only those writable. Create the latter with `sudo install -d -o forge -g forge -m 0700 /var/backups/forge`.
181
182Create the admin before starting. `/setup` lets the first visitor claim the admin account while there are no users, so create the admin before the reverse proxy exposes the site.
183
184```sh
185sudo -u forge FORGE_CONFIG=/etc/forge/forge.toml /usr/local/bin/forge admin create-user <name> --admin
186sudo systemctl start forge
187```
188
189Open the SSH port (2222 by default) in the firewall.
190
191### Reverse proxy and TLS
192
193Run forge behind a TLS reverse proxy for any non-localhost use. The session cookie is named `__Host-forge_session` and is marked `Secure`, so browsers send it only over HTTPS. Logging in over plain HTTP outside localhost will not work. Set `base_url` to the public HTTPS URL.
194
195Git routes need a proxy that does not buffer or time out early. The example below targets [Ferron]https://ferron.sh 3.0.0-rc.7, the newest release found (a pre-release; the latest stable, 2.8.1, was not checked against this config). Ferron uses its own `ferron.conf` format, not KDL.
196
197- Buffering: `buffer_request` and `buffer_response` are off by default. The example sets both to `0` so they stay off, because git smart HTTP streams packs in both directions.
198- Timeouts: `connection_timeout` (default 2s) and `idle_timeout` (default 60s) are per upstream. The example raises `idle_timeout` to `3600s`, because `git upload-pack` can stay silent while it builds a pack. Ferron's proxy docs list no total-request timeout, so there is no separate read/send timeout to raise.
199- Body size: Ferron's docs describe no request body size limit, so there is nothing to disable. Do not add one on git paths (`/<owner>/<repo>/git-receive-pack`, `git-upload-pack`).
200- Forwarded headers: Ferron sets `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Real-IP` and `Forwarded` itself and keeps the incoming `Host`, so the example adds no header lines.
201- SSH: port 2222 is not proxied. Ferron handles HTTP(S) only, so forge serves SSH directly on `ssh_listen`. Open that port on the host.
202
203### Brute-force throttling
204
205Login and git HTTP authentication are throttled in memory, per process, over a 15 minute sliding window:
206
207- Login: 10 failed attempts per username, and 30 per client IP. A throttled attempt gets HTTP 429 with the message "Too many attempts. Try again later.", and the password is not checked. A successful login clears that username's counter.
208- Git HTTP (Basic auth with a token): 30 failed attempts per client IP. The next request gets HTTP 429 with `Retry-After`. Anonymous requests and successful logins are not counted.
209- The client IP is the TCP peer, unless the peer is in `trusted_proxies`. Then it is the right-most `X-Forwarded-For` address that is not a trusted proxy. IPv6 addresses are counted per /64.
210- Throttling events are logged at `warn` with the IP and the folded username. Counters are lost on restart and are not shared between instances.
211
212### Raw user content
213
214forge runs a third listener, `user_content_listen` (default `127.0.0.1:7661`). It serves only the raw files of repos, on the origin `user_content_base_url`. It has its own explicit routes and does not share the app's router.
215
216Put it on its own registrable domain in production, for example `usercontent.example.net` next to `forge.example.com`. Browsers treat that as a separate site, so a file a user uploads cannot run script against the app origin. The raw origin is also built to be inert:
217
218- No cookies are read or set on it, so app sessions never reach it.
219- Every response has `Content-Security-Policy: default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox`, `X-Content-Type-Options: nosniff`, and `Referrer-Policy: no-referrer`.
220- SVG, HTML, XML and other non-image binaries are sent with `Content-Disposition: attachment`.
221
222Private repos need a signed link. The app puts a `?token=` on raw links for private repos, and that token is valid for 10 minutes. Public repos need no token. A missing, hidden, or bad token gets 404.
223
224Example Ferron 3.0.0-rc.7 config (`ferron.conf`) with two hostnames. A bare host block already gets automatic ACME TLS; the `tls` block only sets the account contact:
225
226```
227forge.example.com {
228 tls {
229 provider acme
230 challenge http-01
231 contact admin@example.com
232 }
233 buffer_request 0
234 buffer_response 0
235 proxy {
236 upstream http://127.0.0.1:7660 {
237 idle_timeout "3600s"
238 }
239 }
240}
241
242usercontent.example.net {
243 tls {
244 provider acme
245 challenge http-01
246 contact admin@example.com
247 }
248 buffer_request 0
249 buffer_response 0
250 proxy {
251 upstream http://127.0.0.1:7661
252 }
253}
254```
255
256The two hostnames are different registrable domains, so the raw origin is a separate site with no app cookies.
257
258With that setup, set `base_url = "https://forge.example.com"` and `user_content_base_url = "https://usercontent.example.net"`.
259
260### Limits
261
262- Blob view: the first 1 MiB of a file is shown. Larger files are truncated, with a link to the raw file.
263- Syntax highlighting: skipped for files over 512 KiB or 20,000 lines. Those are shown without highlighting.
264- Raw files: up to 8 MiB are read into memory and sent. Larger files are streamed.
265- Commit diffs: at most 300 files, 2,000 lines per file, and 20,000 lines in total. Anything past that is cut off with a notice.
266
267Compression is currently **off for every route**. Git smart-HTTP responses must not be compressed, and the server cannot yet switch compression off per route, so it is disabled globally (`TODO(M1)` in `boot.rs`). Let the proxy do no compression on git paths either.
268
269## Security notes
270
271From `docs/architecture.md`. These are the properties forge is designed for. Not all are fully implemented in M1.
272
273- Authorization goes through a single function, `authorize()`. A private repo that the actor cannot see returns 404, so its existence is not revealed.
274- Passwords use argon2id. Tokens and session tokens are stored as SHA-256 hashes and compared in constant time. Tokens are shown once and kept out of URLs and logs.
275- SSH keys must match exactly. `ssh-rsa` with SHA-1 is disabled, and the SSH server accepts only `GIT_PROTOCOL` from env requests. Handshake and inactivity timeouts apply.
276- No shell is used. SSH exec commands are parsed against an allow-list, and git is started with explicit arguments.
277- Git runs with `receive.fsckObjects=true`, and hooks are fail-closed: if the internal socket is unreachable, the push is rejected.
278- Users cannot write hooks or repo-local config. Submodules are not recursed.
279- Run as a dedicated service user, and keep the git binary patched.
280- Keep `forge.key` and `data_dir` private. Back up `forge.key` separately.
281
282## Roadmap
283
284From `docs/architecture.md` (section 10).
285
286- **M1: clone and push work.** Accounts, SSH keys, tokens, repo create, smart HTTP and SSH, pre-receive policy, minimal UI.
287- **M2 (current): repo browsing and raw domain.** Tree, blob, commit and diff views; README rendering; syntax highlighting; raw files on `user_content_base_url`.
288- **M3: issues.** Issues, comments, labels, milestones, with permissions through `authorize()`.
289- **M4: pull requests.** PR refs, mergeability via `merge-tree`, reviews, merge strategies.
290- **M5: webhooks, push mirrors, replication, backups, export.** Including `VACUUM INTO` snapshots, integrity checks, and restore drills.
291- **M6: custom CI runner and commit-status API.** Required checks enforced on push as well as PR merge.
292
293## License
294
295forge is licensed under the GNU Affero General Public License, version 3 or later (AGPL-3.0-or-later).
296
297### Fonts
298
299The web UI bundles two fonts, served from the forge origin under `/assets/fonts/`: Figtree (UI) and IBM Plex Mono (code). Both are licensed under the SIL Open Font License 1.1. The license texts are in `style/fonts/` and are also served at `/assets/fonts/Figtree-OFL.txt` and `/assets/fonts/IBMPlexMono-OFL.txt`. Sources, pinned versions and checksums are listed in `style/fonts/README.md`.