Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Blame · docs/configuration.md

Line attribution on main · 486 line(s)

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.