vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
|
520470e1
Keenan Nemetz
16d ago
|
1 | # Configuration |
| 2 | ||
| 3 | Vole is designed to work out of the box. The defaults are enough for a local | |
| 4 | instance. Change them when you want Vole to listen on another address, limit what | |
| 5 | people can push, or serve websites. | |
| 6 | ||
|
6ce635a8
Keenan Nemetz
1d ago
|
7 | There is no general control panel. One TOML file is the whole configuration. An |
|
4506a4d3
Keenan Nemetz
14d ago
|
8 | instance admin can see the effective values and the file they came from on the |
|
6ce635a8
Keenan Nemetz
1d ago
|
9 | Admin page's Configuration tab, and that view is read-only. |
| 10 | ||
| 11 | The one exception is the customizable repository: `instance_repo` names a | |
| 12 | private repository whose tree supplies the landing page's introduction, a | |
| 13 | stylesheet, a favicon, and static assets. It is not written to `vole.toml`, | |
| 14 | because the packaged service runs with `ProtectSystem=strict` and cannot write | |
| 15 | its config file. See [Customizing the instance](#customizing-the-instance). | |
|
520470e1
Keenan Nemetz
16d ago
|
16 | |
| 17 | ## A minimal config | |
| 18 | ||
| 19 | ```toml | |
| 20 | data_dir = "./data" | |
| 21 | http_addr = "127.0.0.1:8080" | |
| 22 | ssh_addr = "127.0.0.1:2222" | |
| 23 | # public_url = "https://forge.example.com" | |
| 24 | ``` | |
| 25 | ||
|
c36d5073
Keenan Nemetz
16d ago
|
26 | ## Which config Vole reads |
| 27 | ||
| 28 | Local commands look for a config in this order: | |
| 29 | ||
| 30 | 1. `--config <path>` | |
| 31 | 2. the `VOLE_CONFIG` environment variable | |
| 32 | 3. `./vole.toml` | |
| 33 | 4. `~/.vole/vole.toml` | |
| 34 | ||
| 35 | If none exists, Vole uses the built-in defaults with `data_dir = "./data"`. | |
| 36 | This is why `vole admin init --dir ~/.vole` works from any directory: later | |
| 37 | commands find `~/.vole/vole.toml` without repeating the path. For an instance | |
| 38 | somewhere else, run commands from that directory, pass `--config`, or set | |
| 39 | `VOLE_CONFIG`: | |
| 40 | ||
| 41 | ```sh | |
|
2e024cc9
Keenan Nemetz
14d ago
|
42 | vole admin --config /srv/vole/vole.toml account list |
|
c36d5073
Keenan Nemetz
16d ago
|
43 | export VOLE_CONFIG=/srv/vole/vole.toml |
|
2e024cc9
Keenan Nemetz
14d ago
|
44 | vole admin account list |
|
c36d5073
Keenan Nemetz
16d ago
|
45 | ``` |
| 46 | ||
|
520470e1
Keenan Nemetz
16d ago
|
47 | ## Settings |
| 48 | ||
| 49 | | Setting | Default | What it does | | |
| 50 | |---|---:|---| | |
| 51 | | `data_dir` | `"./data"` | Where Vole keeps repositories, databases, stored files, and the audit log | | |
|
0c40a1d3
Keenan Nemetz
7d ago
|
52 | | `secrets_key` | `<data_dir>/secrets.key` | The file holding the key that seals each repository's secrets. Point it somewhere a backup of `data_dir` does not reach, so a copy of the database is a copy of ciphertext | |
|
642e5994
Keenan Nemetz
14d ago
|
53 | | `http_addr` | `"127.0.0.1:8080"` | Address for the web interface and anonymous Git clone; with `https_addr`, the ACME challenge and redirect address | |
| 54 | | `https_addr` | empty | Address for automatic HTTPS; setting it enables ACME | | |
|
268b9368
Keenan Nemetz
21h ago
|
55 | | `ssh_addr` | `"127.0.0.1:2222"` | Address for SSH, the authenticated Git transport; its port is also the one the clone link names, while the clone host comes from `public_url` | |
|
78cd3437
Keenan Nemetz
13d ago
|
56 | | `ssh_max_connections` | `64` | Simultaneous connections on that listener; past it a connection is closed before the handshake rather than queued | |
| 57 | | `ssh_max_git_processes` | `16` | Git processes across every SSH connection; past it a command waits a short while, then is told to retry | | |
| 58 | | `ssh_handshake_timeout_seconds` | `20` | How long a connection may take to finish the SSH handshake before it is dropped | | |
|
3d707ea7
Keenan Nemetz
21h ago
|
59 | | `instance_name` | `"Vole"` | What the instance calls itself: the home page heading, every page's document title, and the brand link's accessible name | |
|
6ce635a8
Keenan Nemetz
1d ago
|
60 | | `instance_repo` | `"vole-instance"` | Repository whose tree customizes the instance; empty disables it | |
|
ab326d3a
Keenan Nemetz
14d ago
|
61 | | `public_url` | empty | Canonical browser address; must be HTTPS to enable passkey sign-in | |
|
642e5994
Keenan Nemetz
14d ago
|
62 | | `acme_email` | empty | Contact address given to the certificate authority | |
| 63 | | `acme_domains` | empty | Host names to obtain certificates for; defaults to the host in `public_url` | | |
| 64 | | `acme_cache_dir` | `<data_dir>/acme` | Where the ACME account key and certificates are stored | | |
| 65 | | `acme_directory_url` | Let's Encrypt production | ACME directory; use the staging URL or a private CA while testing | | |
|
ed181212
Keenan Nemetz
13d ago
|
66 | | `dev_login` | empty | Development only: sign loopback browser requests in as this account; requires `dev_only` | |
| 67 | | `dev_only` | `false` | Acknowledge that this instance is a development instance; without it `dev_login` is refused | | |
|
f920a9f1
Keenan Nemetz
16d ago
|
68 | | `open_registration` | `false` | Let anyone create an account by registering a passkey; requires `public_url` to be HTTPS | |
|
5b814e99
Keenan Nemetz
14d ago
|
69 | | `open_registration_role` | `"none"` | Instance-wide role a self-registered account gets; applies to public repositories, `admin` is refused | |
|
520470e1
Keenan Nemetz
16d ago
|
70 | | `max_blob_bytes` | `104857600` (100 MiB) | Largest single file accepted in a push | |
| 71 | | `receive_max_bytes` | `1073741824` (1 GiB) | Largest whole push Vole will accept | | |
| 72 | | `lfs_object_max_bytes` | `2147483648` (2 GiB) | Largest single LFS object | | |
| 73 | | `lfs_warn_bytes` | `5242880` (5 MiB) | Warn when a large binary is committed outside LFS; `0` turns the warning off | | |
|
9e8cc155
Keenan Nemetz
12d ago
|
74 | | `repo_max_refs` | `500` | Branches and tags in one repository; a push that would exceed it is refused | |
|
b312e166
Keenan Nemetz
11d ago
|
75 | | `repo_max_metadata_entries` | `2000` | Documents on the metadata branch in one repository: issues, requests, comments, approvals, and release records | |
|
9e8cc155
Keenan Nemetz
12d ago
|
76 | | `repo_max_metadata_bytes` | `33554432` (32 MiB) | Total size of those files | |
| 77 | | `repo_max_bytes` | `5368709120` (5 GiB) | Git objects in one repository; a repository already over it refuses further pushes | | |
|
5ece70a5
Keenan Nemetz
11d ago
|
78 | | `repo_max_lfs_bytes` | `10737418240` (10 GiB) | LFS objects and run artifacts in one repository, checked on upload | |
|
e5542ac6
Keenan Nemetz
12d ago
|
79 | | `contributor_writes_per_hour` | `300` | Browser writes an untrusted (`contribute`) account may make in an hour: issues, comments, requests, labels, state changes. Maintainers and admins are trusted and are not budgeted | |
|
86991e70
Keenan Nemetz
13d ago
|
80 | | `git_http` | `true` | Serve anonymous clone, fetch and LFS downloads over smart HTTP; off leaves SSH as the only transport | |
| 81 | | `git_http_max_concurrent` | `16` | Simultaneous anonymous reads on that transport; past it a clone is refused with `503` and `Retry-After` | | |
| 82 | | `git_http_max_per_client` | `8` | Simultaneous reads from one address; `0` leaves only the concurrency bound | | |
|
edf9db6e
Keenan Nemetz
13d ago
|
83 | | `git_http_client_ip_header` | empty | Header a trusted proxy sets with the client address, e.g. `"X-Forwarded-For"`. The same header is what the sign-in rate limits key on | |
|
86991e70
Keenan Nemetz
13d ago
|
84 | | `git_http_timeout_seconds` | `600` | How long one clone may run, Git process included | |
|
520470e1
Keenan Nemetz
16d ago
|
85 | | `lease_seconds` | `60` | How long a runner may hold a job without checking in before it is given to someone else | |
| 86 | | `max_jobs_per_repo` | `0` | How many jobs one repository may run at once; `0` means no limit | | |
|
701232cf
Keenan Nemetz
12d ago
|
87 | | `pages_domain` | empty | Domain suffix for websites, as in `<repo>.<pages_domain>`; empty disables sites. Use a different registrable domain from the forge when sites are untrusted — see [Sites](sites.md#same-site-risk) | |
|
520470e1
Keenan Nemetz
16d ago
|
88 | | `pages_ref` | `"pages"` | Branch served as a website | |
| 89 | | `pages_procs` | `4` | Number of Git reader processes per active site | | |
| 90 | | `pages_cache_bytes` | `33554432` (32 MiB) | Memory used to cache site files | | |
| 91 | ||
| 92 | The two push limits work at different levels. `max_blob_bytes` checks each file in | |
| 93 | a push. `receive_max_bytes` caps the size of the whole push and is enforced by Git | |
| 94 | while it receives the data, before any branch is updated. | |
| 95 | ||
|
86991e70
Keenan Nemetz
13d ago
|
96 | ## Anonymous read limits |
| 97 | ||
| 98 | Smart HTTP is the transport an unauthenticated visitor uses: `git clone`, | |
| 99 | fetch, and the LFS downloads that share the same URLs. Pushes never arrive there; | |
| 100 | they are SSH only. Because the reader has no account, the transport is bounded in | |
| 101 | every dimension that costs the instance something. | |
| 102 | ||
| 103 | ```toml | |
| 104 | git_http = true | |
| 105 | git_http_max_concurrent = 16 | |
| 106 | git_http_max_per_client = 8 | |
| 107 | git_http_client_ip_header = "X-Forwarded-For" | |
| 108 | git_http_timeout_seconds = 600 | |
| 109 | ``` | |
| 110 | ||
| 111 | `git_http_max_concurrent` bounds simultaneous reads, and a clone past it is | |
| 112 | refused with `503` and `Retry-After` rather than queued: a waiting queue is | |
| 113 | another thing that grows with load. `git_http_max_per_client` keeps one address | |
| 114 | from taking every slot, and `0` turns that rule off. | |
| 115 | ||
| 116 | Behind a proxy every connection appears to come from the proxy, so the | |
| 117 | per-client rule needs the address the proxy saw. Name the header it sets: | |
| 118 | ||
| 119 | ```toml | |
| 120 | git_http_client_ip_header = "X-Forwarded-For" | |
| 121 | ``` | |
| 122 | ||
| 123 | Vole takes the last entry of that header, which is the one the nearest proxy | |
| 124 | appended. Leave it empty when nothing sits in front of Vole: the connection's own | |
| 125 | address cannot be forged, and a header no proxy sets can. | |
| 126 | ||
| 127 | `git_http = false` removes the endpoints entirely, which is the setting for an | |
| 128 | instance whose repositories are all internal and none of which are cloned | |
| 129 | anonymously. SSH pushes are unaffected. LFS downloads share the HTTP URLs, so they | |
| 130 | go with it; `git lfs` will report a missing object rather than fetch from SSH. | |
| 131 | ||
| 132 | A release archive is served under the same limit: `git_http_timeout_seconds` | |
| 133 | does not apply to the release page's tarball, which has its own concurrency bound. | |
| 134 | ||
|
6ce635a8
Keenan Nemetz
1d ago
|
135 | ## Customizing the instance |
| 136 | ||
| 137 | `instance_repo` names a repository whose tree customizes the instance. The idea is | |
| 138 | GitHub's `.github` repository: fixed paths read as data, not templates. | |
| 139 | ||
| 140 | ```toml | |
| 141 | instance_repo = "vole-instance" # the default; empty disables customization | |
| 142 | ``` | |
|
074ae160
Keenan Nemetz
14d ago
|
143 | |
|
6ce635a8
Keenan Nemetz
1d ago
|
144 | An instance admin can scaffold it from **Admin → Configuration** (the Instance |
| 145 | repository panel), or create a repository with that name normally and push the | |
|
3e7c5e67
Keenan Nemetz
1d ago
|
146 | files. `vole admin init` creates it with the starter files when it is missing, |
| 147 | so a new instance starts with it. It is an ordinary repository: it appears in the | |
| 148 | repository list, has the usual pages, history, issues, and change requests, and | |
| 149 | its visibility is set in Settings. It is private by default, so only instance | |
| 150 | admins see it until one of them publishes it. The files below are served to | |
| 151 | visitors either way. | |
|
6ce635a8
Keenan Nemetz
1d ago
|
152 | |
| 153 | | Path | What it does | | |
| 154 | |---|---| | |
| 155 | | `profile/README.md` | Landing page introduction; markdown, sanitized like an issue body | | |
| 156 | | `profile/private/README.md` | Replaces it for signed-in viewers when present | | |
|
f7a34d87
Keenan Nemetz
1d ago
|
157 | | `profile/custom.css` | Instance-wide styles, loaded last | |
| 158 | | `profile/themes/<id>.css` | One stylesheet per selectable theme | | |
|
6ce635a8
Keenan Nemetz
1d ago
|
159 | | `profile/favicon.svg` | Replaces the favicon links | |
|
b7396a11
Keenan Nemetz
6h ago
|
160 | | `assets/` | Served at `/-/static/custom/assets/`; CSS, images and fonts only | |
|
6ce635a8
Keenan Nemetz
1d ago
|
161 | |
| 162 | Other files are ignored, and assets are limited to CSS, images and fonts: a | |
|
ff9870f1
Keenan Nemetz
1d ago
|
163 | same-origin script is not a theme. `profile/README.md` and every other file are |
| 164 | edited by pushing; the home page has no separate editor. A push is live within a | |
| 165 | couple of seconds, and a load failure keeps the previous theme. | |
|
6ce635a8
Keenan Nemetz
1d ago
|
166 | |
| 167 | The introduction is rendered and sanitized the same way an issue body is, and it | |
| 168 | appears above the repository list. An empty `profile/README.md` removes the | |
| 169 | card's body. The heading is `instance_name`, which is what the forge calls itself: | |
|
074ae160
Keenan Nemetz
14d ago
|
170 | |
| 171 | ```toml | |
| 172 | instance_name = "Acme Forge" | |
| 173 | ``` | |
| 174 | ||
|
3d707ea7
Keenan Nemetz
21h ago
|
175 | The name is also the document title on every page (`Issues · Acme Forge`) and |
| 176 | the accessible name of the brand link ("Acme Forge home"), so a renamed instance | |
| 177 | is renamed in the tab as well as in the heading. The brand mark in the top bar | |
| 178 | stays Vole's emblem, and prose about the software itself, such as the status | |
| 179 | page's "restart Vole", keeps the product name: the instance name describes the | |
| 180 | organisation's forge, not the program serving it. | |
|
074ae160
Keenan Nemetz
14d ago
|
181 | |
|
bc95767f
Keenan Nemetz
1d ago
|
182 | ### Theming |
| 183 | ||
|
0584bb70
Keenan Nemetz
1d ago
|
184 | `profile/custom.css` is loaded after the built-in `vole.css`, `everforest.css`, |
| 185 | and `palettes.css`, and after any theme pack, so it can override every theme. | |
| 186 | Override the design tokens rather than copying rules; the layout, contrast, and | |
| 187 | responsive behaviour then stay intact. | |
| 188 | ||
|
97b3b022
Keenan Nemetz
1d ago
|
189 | Vole ships the Everforest variants, Catppuccin Mocha and Latte, Nord, Solarized |
| 190 | Light, Rosé Pine Moon and Dawn, Kanagawa Wave, Dracula, Tokyo Night, and Ayu | |
| 191 | Light. The Appearance picker lists them before the instance's own theme packs. | |
|
f7a34d87
Keenan Nemetz
1d ago
|
192 | |
| 193 | #### Theme packs | |
| 194 | ||
|
d222b0cf
Keenan Nemetz
1d ago
|
195 | Account → Appearance lists the built-in Everforest variants plus every CSS file |
| 196 | in `profile/themes/`. A theme is self-describing: the file name is the id and | |
| 197 | the label, and the rules are scoped to that id. `acme-ocean.css` is the | |
| 198 | “Acme Ocean” theme; `acme-paper.css` is “Acme Paper”. | |
|
f7a34d87
Keenan Nemetz
1d ago
|
199 | |
| 200 | ```css | |
| 201 | /* profile/themes/acme-ocean.css */ | |
| 202 | :root[data-theme="acme-ocean"] { | |
| 203 | color-scheme: dark; | |
| 204 | --bg: #0b1720; | |
| 205 | --bg-panel: #102433; | |
| 206 | --bg-raised: #163041; | |
| 207 | --fg: #d7e8f0; | |
| 208 | --fg-muted: #8fb0c0; | |
| 209 | --fg-faint: #6f8f9f; | |
| 210 | --accent: #38bdf8; | |
| 211 | --accent-fg: #04121a; | |
| 212 | --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent); | |
| 213 | } | |
| 214 | ``` | |
| 215 | ||
| 216 | ```css | |
| 217 | /* profile/themes/acme-paper.css */ | |
| 218 | :root[data-theme="acme-paper"] { | |
| 219 | color-scheme: light; | |
| 220 | --bg: #fffdf7; | |
| 221 | --bg-panel: #f8f3e8; | |
| 222 | --bg-raised: #f1eadc; | |
| 223 | --fg: #2b2a26; | |
| 224 | --fg-muted: #6b675d; | |
| 225 | --fg-faint: #8d887c; | |
| 226 | --accent: #9a5b13; | |
| 227 | --accent-fg: #fffdf7; | |
| 228 | --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent); | |
| 229 | } | |
| 230 | ``` | |
| 231 | ||
|
d222b0cf
Keenan Nemetz
1d ago
|
232 | The file name must use lowercase letters, digits, and dashes (at most 41 |
|
97b3b022
Keenan Nemetz
1d ago
|
233 | characters), and cannot collide with a built-in value: `system`, `nord`, |
| 234 | `dracula`, `tokyo-night`, or a name starting with `everforest-`, `catppuccin-`, | |
| 235 | `solarized-`, `rose-pine-`, `kanagawa-`, or `ayu-`. A file whose name breaks that | |
| 236 | rule fails the snapshot load with a message on the Admin page; the previous theme | |
| 237 | stays in use. A light theme must set `color-scheme: light` | |
|
d222b0cf
Keenan Nemetz
1d ago
|
238 | and light surface and text tokens, or form controls and inherited colours stay |
| 239 | dark. | |
|
f7a34d87
Keenan Nemetz
1d ago
|
240 | |
|
d222b0cf
Keenan Nemetz
1d ago
|
241 | Every theme file is linked on every page, so keep them small. The selection is a |
| 242 | browser preference, not an account setting: it lives in `localStorage`, and a | |
| 243 | theme whose file is deleted falls back to System. | |
|
f7a34d87
Keenan Nemetz
1d ago
|
244 | |
| 245 | #### Tokens | |
|
bc95767f
Keenan Nemetz
1d ago
|
246 | |
| 247 | Use `:root[data-theme]`, not plain `:root`. Everforest sets its tokens on | |
| 248 | `:root[data-theme^="everforest-"]`, which is more specific than `:root`, so a | |
| 249 | plain `:root` override can lose even though the custom file loads later. | |
| 250 | ||
| 251 | ```css | |
| 252 | :root[data-theme] { | |
| 253 | --accent: #c678dd; | |
| 254 | --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent); | |
| 255 | --radius: 4px; | |
| 256 | --radius-lg: 6px; | |
| 257 | --page-max: 1200px; | |
| 258 | } | |
| 259 | ``` | |
| 260 | ||
| 261 | A value that has to differ between dark and light modes belongs in one of these | |
| 262 | blocks. They match every Everforest variant: | |
| 263 | ||
| 264 | ```css | |
| 265 | :root[data-theme^="everforest-dark"] { --bg: #101418; } | |
| 266 | :root[data-theme^="everforest-light"] { --bg: #fffdf7; } | |
| 267 | ``` | |
| 268 | ||
| 269 | The tokens, grouped by what they change: | |
| 270 | ||
| 271 | | Group | Tokens | | |
| 272 | |---|---| | |
| 273 | | surfaces | `--bg`, `--bg-panel`, `--bg-raised`, `--bg-hover`, `--bg-active`, `--border`, `--border-strong` | | |
| 274 | | text | `--fg`, `--fg-muted`, `--fg-faint` | | |
| 275 | | identity | `--accent`, `--accent-fg`, `--accent-soft` | | |
| 276 | | status | `--good`, `--good-soft`, `--bad`, `--bad-soft`, `--warn`, `--warn-soft`, `--running`, `--merged`, `--merged-soft`, `--status-fg` | | |
| 277 | | shape | `--radius`, `--radius-lg`, `--page-max`, `--page-gutter`, `--search-w` | | |
| 278 | | type | `--font`, `--mono`, `--fs`, `--lh`. Leave `--font-scale` alone: it is the reader's text-size preset | | |
| 279 | | syntax | `--syn-comment`, `--syn-punctuation`, `--syn-operator`, `--syn-number`, `--syn-string`, `--syn-keyword`, `--syn-function`, `--syn-type`, `--syn-variable`, `--syn-added`, `--syn-deleted` | | |
| 280 | ||
| 281 | If a token cannot express the change, these page-level classes cover most | |
| 282 | re-skins: `.panel`, `.readme`, `.md`, `.repo-card`, `.repo-grid`, `.home-grid`, | |
| 283 | `.home-rail`, `.repohead`, `.tabs`, `.chip`, `.label`, `.status`, | |
| 284 | `.config-table`, `.diff`, `.blob`. They are not a public API: an upgrade may move | |
| 285 | markup, so prefer a token when one exists. | |
| 286 | ||
|
520470e1
Keenan Nemetz
16d ago
|
287 | ## Public URL |
| 288 | ||
| 289 | Set `public_url` when people reach Vole through a different address than | |
| 290 | `http_addr`, for example through a proxy: | |
| 291 | ||
| 292 | ```toml | |
| 293 | public_url = "https://forge.example.com" | |
| 294 | ``` | |
| 295 | ||
| 296 | The CLI uses this to print a link to an issue, comment, or release it just | |
|
ab326d3a
Keenan Nemetz
14d ago
|
297 | created. It is also the WebAuthn relying-party origin: browser sign-in is enabled |
|
6c84d387
Keenan Nemetz
16d ago
|
298 | only when `public_url` is an `https://` URL. Vole does not change how it routes |
| 299 | requests based on this setting. | |
|
520470e1
Keenan Nemetz
16d ago
|
300 | |
|
d37d301f
Keenan Nemetz
19h ago
|
301 | The host name in `public_url` is also the import prefix a Go module path uses: |
| 302 | `public_url = "https://forge.example.com"` publishes a public repository as | |
| 303 | `forge.example.com/<repository>`. See [Go modules](go-modules.md). | |
| 304 | ||
|
701232cf
Keenan Nemetz
12d ago
|
305 | It is also where generated clone and LFS download links point. That is a security |
| 306 | property, not a convenience: without it those links follow the request's `Host` | |
| 307 | header, which is a value the browser's request carries and an attacker can choose. | |
| 308 | Copying a clone URL out of a page and into a terminal is normal use, so the URL a | |
| 309 | page shows should be the instance's, not the caller's. Set `public_url` on any | |
| 310 | instance other than a loopback preview. | |
| 311 | ||
|
268b9368
Keenan Nemetz
21h ago
|
312 | The same host is what the SSH clone link shows, with the port from `ssh_addr`: |
| 313 | an instance listening on `0.0.0.0:2222` with `public_url = | |
| 314 | "https://forge.example.com"` offers `ssh://git@forge.example.com:2222/demo.git`. | |
| 315 | A bind address is not something a person can connect to, so it is never used as | |
| 316 | the clone host when a name is configured. | |
| 317 | ||
|
701232cf
Keenan Nemetz
12d ago
|
318 | Forwarded headers (`X-Forwarded-Proto`, `X-Forwarded-For`) are believed only when |
| 319 | `git_http_client_ip_header` is set, because that is the instance's statement that | |
| 320 | a proxy it trusts is rewriting them. Without it, a client that sets a forwarding | |
| 321 | header is describing itself, and Vole ignores it. | |
| 322 | ||
|
642e5994
Keenan Nemetz
14d ago
|
323 | ## Automatic HTTPS with ACME |
| 324 | ||
| 325 | Vole can obtain and renew its own TLS certificates, so a small public instance | |
| 326 | does not need a proxy in front of it. Set `https_addr`, point the host name in | |
| 327 | `public_url` at the server, and make both ports reachable: | |
| 328 | ||
| 329 | ```toml | |
| 330 | http_addr = ":80" | |
| 331 | https_addr = ":443" | |
| 332 | public_url = "https://forge.example.com" | |
| 333 | acme_email = "admin@example.com" | |
| 334 | ``` | |
| 335 | ||
| 336 | `https_addr` turns it on, and `public_url` must then be an `https://` URL. Vole | |
| 337 | serves the forge over TLS on that address. `http_addr` stops serving pages: it | |
| 338 | answers ACME HTTP-01 challenges and redirects everything else to HTTPS. The | |
| 339 | certificate host comes from `public_url`; to serve more than one name, list each | |
| 340 | one in `acme_domains` (each host gets its own certificate): | |
| 341 | ||
| 342 | ```toml | |
| 343 | acme_domains = ["forge.example.com", "git.example.com"] | |
| 344 | ``` | |
| 345 | ||
| 346 | Certificates are obtained on first use and renewed in the background. Vole tries | |
| 347 | tls-alpn-01 on port 443 and http-01 on port 80, so a network that blocks one can | |
| 348 | still validate through the other. DNS must already point at the instance; ACME | |
| 349 | cannot issue a name that does not resolve here. There is no wildcard support: | |
| 350 | list concrete names, or keep a proxy for a wildcard certificate. | |
| 351 | ||
| 352 | State lives in `data/acme/` by default. It contains the ACME account key, so the | |
| 353 | directory is created private; back it up with the other instance data, though | |
| 354 | losing it only means registering again and asking for new certificates. | |
| 355 | ||
| 356 | While setting this up, use the staging directory so a mistake does not consume a | |
| 357 | production rate limit: | |
| 358 | ||
| 359 | ```toml | |
| 360 | acme_directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory" | |
| 361 | ``` | |
| 362 | ||
| 363 | Ports below 1024 need `CAP_NET_BIND_SERVICE`. The packaged systemd unit can grant | |
| 364 | it; see [Packages](packaging.md). An instance with `https_addr` empty behaves as | |
| 365 | before and leaves TLS to a proxy. | |
| 366 | ||
| 367 | When `pages_domain` is also set, Vole obtains a certificate for a site host the | |
| 368 | first time that host is requested, provided the repository exists and is | |
| 369 | publicly readable. Site certificates therefore work with a wildcard DNS record | |
| 370 | pointing at the instance, but they count against the certificate authority's | |
| 371 | per-domain rate limit. | |
| 372 | ||
|
f920a9f1
Keenan Nemetz
16d ago
|
373 | ## Open registration |
| 374 | ||
|
ab326d3a
Keenan Nemetz
14d ago
|
375 | By default only an instance admin can create an account. Set |
|
f920a9f1
Keenan Nemetz
16d ago
|
376 | `open_registration = true` to let anyone create one by registering a passkey: |
| 377 | ||
| 378 | ```toml | |
| 379 | public_url = "https://forge.example.com" | |
| 380 | open_registration = true | |
|
5b814e99
Keenan Nemetz
14d ago
|
381 | open_registration_role = "contribute" |
|
f920a9f1
Keenan Nemetz
16d ago
|
382 | ``` |
| 383 | ||
|
5b814e99
Keenan Nemetz
14d ago
|
384 | The setting has no effect unless `public_url` is HTTPS. New accounts have no |
| 385 | repository-specific access, but `open_registration_role` gives them an | |
| 386 | instance-wide role that applies to every public repository. `contribute` lets | |
| 387 | them read, discuss, and push to their own `refs/heads/contrib/<name>/` namespace | |
| 388 | without being added to each repository. It does not open private repositories; | |
| 389 | `admin` is refused for self-registration. A registered account can add its own | |
| 390 | SSH key from **Account**, so it can push. There is no email verification; | |
| 391 | registration is a deliberate policy choice, not a safe default. | |
| 392 | ||
| 393 | A repository can still be published for anonymous readers with: | |
|
f920a9f1
Keenan Nemetz
16d ago
|
394 | |
| 395 | ```sh | |
|
2e024cc9
Keenan Nemetz
14d ago
|
396 | vole admin acl set-default-role demo read |
|
f920a9f1
Keenan Nemetz
16d ago
|
397 | ``` |
| 398 | ||
|
ab326d3a
Keenan Nemetz
14d ago
|
399 | ## Development sign-in |
|
f09ef39e
Keenan Nemetz
15d ago
|
400 | |
| 401 | Browsers require HTTPS for passkeys, which makes a local `http://127.0.0.1:8080` | |
|
ed181212
Keenan Nemetz
13d ago
|
402 | preview awkward to use for write testing. `dev_login` is an explicit escape hatch, |
| 403 | and `dev_only` is what makes it explicit: | |
|
f09ef39e
Keenan Nemetz
15d ago
|
404 | |
| 405 | ```toml | |
| 406 | http_addr = "127.0.0.1:8080" | |
|
ed181212
Keenan Nemetz
13d ago
|
407 | dev_only = true |
|
f09ef39e
Keenan Nemetz
15d ago
|
408 | dev_login = "you" |
| 409 | ``` | |
| 410 | ||
| 411 | When set, a request from loopback is signed in as that account using a normal | |
|
ed181212
Keenan Nemetz
13d ago
|
412 | server-side session. Vole refuses to start unless all of these hold: `dev_only` |
| 413 | is set, `http_addr` is a loopback address, there is no `https_addr`, and | |
| 414 | `public_url` is unset or itself loopback. It logs a warning when the setting is | |
| 415 | active. The session cookie is deliberately not named `__Host-` so a browser will | |
| 416 | store it over plain HTTP. Never enable this on an instance other people can reach. | |
| 417 | ||
| 418 | The request check does not trust the `Host` header alone. A loopback listener | |
| 419 | behind a proxy sees loopback as the peer address, so a development request must | |
| 420 | also name the configured `http_addr` (same port, and either the same host or | |
| 421 | another loopback name), and any request carrying `Forwarded`, | |
| 422 | `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Proto` or `X-Real-IP` is | |
| 423 | refused. A proxy that rewrites `Host` to `127.0.0.1` therefore gets no session. | |
|
f09ef39e
Keenan Nemetz
15d ago
|
424 | |
|
b7396a11
Keenan Nemetz
6h ago
|
425 | Signing out opts the browser out of automatic dev sign-in; visit `/-/dev/login` to |
|
f09ef39e
Keenan Nemetz
15d ago
|
426 | sign back in. Remove `dev_login` and restart to turn it off entirely. |
| 427 | ||
|
ed181212
Keenan Nemetz
13d ago
|
428 | ## Refused configurations |
| 429 | ||
| 430 | `vole serve` validates the configuration before it opens anything and refuses to | |
| 431 | start on a combination that cannot be safe. It reports every problem at once, so | |
| 432 | one restart is enough to fix the file. | |
| 433 | ||
| 434 | | Refused when | Why | | |
| 435 | |---|---| | |
| 436 | | `dev_login` without `dev_only` | A credential-free session must be a decision, not a copied line | | |
| 437 | | `dev_login` with `https_addr` | That listener is reachable from the network | | |
| 438 | | `dev_login` with a non-loopback `public_url` | A published instance is not a development instance | | |
| 439 | | `dev_login` with a non-loopback `http_addr` | The session is only for the machine it runs on | | |
| 440 | | `open_registration` without an HTTPS `public_url` | WebAuthn credentials are bound to an HTTPS origin | | |
| 441 | | `open_registration_role` = `admin` | Self-registration cannot grant instance administration | | |
| 442 | | `public_url` with a user, path, query or fragment | It is an origin that links are appended to | | |
| 443 | | `pages_domain` that is not a bare DNS suffix | Site routing matches a visitor's `Host` against it | | |
| 444 | | `receive_max_bytes` below `max_blob_bytes` | No push could ever contain a permitted file | | |
| 445 | | `https_addr` with no `acme_domains` or `public_url` | There is no name to obtain a certificate for | | |
| 446 | | `acme_email` that is not one address | The certificate authority has nobody to contact | | |
| 447 | | `acme_directory_url` that is not HTTPS | Renewal would fail after the typo was forgotten | | |
| 448 | | a negative size, lease or limit | It is never what the operator meant | | |
| 449 | ||
|
520470e1
Keenan Nemetz
16d ago
|
450 | ## Running behind a proxy |
| 451 | ||
|
642e5994
Keenan Nemetz
14d ago
|
452 | If a proxy already terminates TLS, leave `https_addr` empty. Vole serves plain |
| 453 | HTTP on `http_addr`; keep the `Host` header intact, because Vole uses it to | |
| 454 | decide which site a request belongs to. | |
|
520470e1
Keenan Nemetz
16d ago
|
455 | |
| 456 | ```nginx | |
| 457 | server { | |
| 458 | listen 443 ssl; | |
| 459 | server_name *.forge.example.com; | |
| 460 | location / { | |
| 461 | proxy_set_header Host $host; | |
| 462 | proxy_set_header X-Forwarded-Proto $scheme; | |
| 463 | proxy_pass http://127.0.0.1:8080; | |
| 464 | } | |
| 465 | } | |
| 466 | ``` | |
| 467 | ||
| 468 | SSH is separate. Expose `ssh_addr` directly, or through a TCP proxy that keeps the | |
| 469 | connection intact. | |
| 470 | ||
| 471 | ## What Vole stores | |
| 472 | ||
| 473 | | Path | What it is | | |
| 474 | |---|---| | |
|
4506a4d3
Keenan Nemetz
14d ago
|
475 | | `data/users.db` | Accounts, names, instance roles, SSH keys, passkeys, enrollment links, browser sessions, access rules, locks, and daily page-view counters. This is the main account database. Session and enrollment-link tokens are stored only as hashes. | |
|
520470e1
Keenan Nemetz
16d ago
|
476 | | `data/repos/` | Your Git repositories. | |
| 477 | | `data/index.db` | A rebuildable search index. | | |
|
b312e166
Keenan Nemetz
11d ago
|
478 | | `data/runs.db` | The CI queue, finished-run records and steps, artifact pointers, and contributor-branch CI approvals. It is a source of truth: back it up. Deleting it loses CI history and pending work. | |
|
5ece70a5
Keenan Nemetz
11d ago
|
479 | | `data/lfs/` | Stored large files and build artifacts. | |
| 480 | | `data/logs/` | One file per CI attempt, streamed from the runner while the job runs. | | |
|
b1499c72
Keenan Nemetz
13d ago
|
481 | | `data/audit.log` | A record of privileged actions: one tab-separated line per action, `timestamp`, `action`, `actor`, `subject`, `detail`. Created and kept at mode `0600`. See [Audit log](operations.md#audit-log). | |
|
0c40a1d3
Keenan Nemetz
7d ago
|
482 | | `data/secrets.db`, `data/secrets.key` | Each repository's secrets, sealed with AES-256-GCM under the key. Losing the key loses every stored value, and restoring the database beside a newly generated key reads nothing. | |
|
642e5994
Keenan Nemetz
14d ago
|
483 | | `data/ssh_host_ed25519_key` | The SSH host key. It is the instance's long-term secret; browser credentials are public keys and session tokens are hashed. | |
| 484 | | `data/acme/` | The ACME account key and issued TLS certificates, when automatic HTTPS is enabled. | | |
|
520470e1
Keenan Nemetz
16d ago
|
485 | |
| 486 | See [Operations](operations.md) for backup and recovery. |