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.
Bound the anonymous Git HTTP read path
86991e7020a418a08a5e3a36ce8f296561392e3b · Keenan Nemetz · 13d ago · parent e05f1c48
Bound the anonymous Git HTTP read path The transport that needs no account had no bound on anything: the request body was capped only at "not negative", the backend ran without a request context or deadline, the path check accepted any suffix after `git-upload-pack` and handed it to the CGI as `PATH_INFO`, and the server set no `ReadTimeout` and no `MaxHeaderBytes`. Bound what the read side can spend, which is every dimension available to a fetch and none of the ones a push would need: a 2 MiB body cap with a required `Content-Length`, exactly two endpoints with one method and one service each so the CGI never sees an unvalidated path, `exec.CommandContext` plus `WaitDelay` plus an explicit kill so a disconnect or an expired budget stops git instead of leaving it running, and a concurrency semaphore with a per-client bound that answers 503 with Retry-After rather than queueing. The client address is the connection's own unless the operator names a proxy header, because behind a proxy the connection address is the proxy. Both limits and the header name are configuration, `git_http` turns the transport off entirely, and `status` lists them so an operator can see what is in force. `WriteTimeout` is deliberately not set: clone, archives, LFS objects and site files are streamed, so those flows are bounded where they are produced.
docs/configuration.md +44
| @@ -60,6 +60,11 @@ vole admin account list | |||
| 60 | 60 | | `receive_max_bytes` | `1073741824` (1 GiB) | Largest whole push Vole will accept | | |
| 61 | 61 | | `lfs_object_max_bytes` | `2147483648` (2 GiB) | Largest single LFS object | | |
| 62 | 62 | | `lfs_warn_bytes` | `5242880` (5 MiB) | Warn when a large binary is committed outside LFS; `0` turns the warning off | | |
| 63 | | `git_http` | `true` | Serve anonymous clone, fetch and LFS downloads over smart HTTP; off leaves SSH as the only transport | | ||
| 64 | | `git_http_max_concurrent` | `16` | Simultaneous anonymous reads on that transport; past it a clone is refused with `503` and `Retry-After` | | ||
| 65 | | `git_http_max_per_client` | `8` | Simultaneous reads from one address; `0` leaves only the concurrency bound | | ||
| 66 | | `git_http_client_ip_header` | empty | Header a trusted proxy sets with the client address, e.g. `"X-Forwarded-For"` | | ||
| 67 | | `git_http_timeout_seconds` | `600` | How long one clone may run, Git process included | | ||
| 63 | 68 | | `lease_seconds` | `60` | How long a runner may hold a job without checking in before it is given to someone else | | |
| 64 | 69 | | `max_jobs_per_repo` | `0` | How many jobs one repository may run at once; `0` means no limit | | |
| 65 | 70 | | `pages_domain` | empty | Domain suffix for websites, as in `<repo>.<pages_domain>`; empty disables sites | | |
| @@ -71,6 +76,45 @@ The two push limits work at different levels. `max_blob_bytes` checks each file | |||
| 71 | 76 | a push. `receive_max_bytes` caps the size of the whole push and is enforced by Git | |
| 72 | 77 | while it receives the data, before any branch is updated. | |
| 73 | 78 | ||
| 79 | ## Anonymous read limits | ||
| 80 | |||
| 81 | Smart HTTP is the transport an unauthenticated visitor uses: `git clone`, | ||
| 82 | fetch, and the LFS downloads that share the same URLs. Pushes never arrive there; | ||
| 83 | they are SSH only. Because the reader has no account, the transport is bounded in | ||
| 84 | every dimension that costs the instance something. | ||
| 85 | |||
| 86 | ```toml | ||
| 87 | git_http = true | ||
| 88 | git_http_max_concurrent = 16 | ||
| 89 | git_http_max_per_client = 8 | ||
| 90 | git_http_client_ip_header = "X-Forwarded-For" | ||
| 91 | git_http_timeout_seconds = 600 | ||
| 92 | ``` | ||
| 93 | |||
| 94 | `git_http_max_concurrent` bounds simultaneous reads, and a clone past it is | ||
| 95 | refused with `503` and `Retry-After` rather than queued: a waiting queue is | ||
| 96 | another thing that grows with load. `git_http_max_per_client` keeps one address | ||
| 97 | from taking every slot, and `0` turns that rule off. | ||
| 98 | |||
| 99 | Behind a proxy every connection appears to come from the proxy, so the | ||
| 100 | per-client rule needs the address the proxy saw. Name the header it sets: | ||
| 101 | |||
| 102 | ```toml | ||
| 103 | git_http_client_ip_header = "X-Forwarded-For" | ||
| 104 | ``` | ||
| 105 | |||
| 106 | Vole takes the last entry of that header, which is the one the nearest proxy | ||
| 107 | appended. Leave it empty when nothing sits in front of Vole: the connection's own | ||
| 108 | address cannot be forged, and a header no proxy sets can. | ||
| 109 | |||
| 110 | `git_http = false` removes the endpoints entirely, which is the setting for an | ||
| 111 | instance whose repositories are all internal and none of which are cloned | ||
| 112 | anonymously. SSH pushes are unaffected. LFS downloads share the HTTP URLs, so they | ||
| 113 | go with it; `git lfs` will report a missing object rather than fetch from SSH. | ||
| 114 | |||
| 115 | A release archive is served under the same limit: `git_http_timeout_seconds` | ||
| 116 | does not apply to the release page's tarball, which has its own concurrency bound. | ||
| 117 | |||
| 74 | 118 | ## Instance name and tagline | |
| 75 | 119 | ||
| 76 | 120 | The landing page names the forge above its repository list. Set these when the | |