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.

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