vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Validate the configuration and harden what a deployment creates
ed1812123cd9e9df0f34fdaf0d2511a340c2aaa6 · Keenan Nemetz · 13d ago · parent 13cff33f
Validate the configuration and harden what a deployment creates Two ways an instance used to run in a state it should have refused. The configuration was never checked. `dev_login` was guarded only by a loopback listener, so a proxy that rewrites Host to 127.0.0.1 inherited a credential-free session; open registration could be enabled on a plain-HTTP origin WebAuthn would never accept; a public_url could carry a path or credentials; a size could be negative. Config.Validate now runs before anything is opened and reports every problem at once, and dev_login requires an explicit dev_only opt-in next to a loopback listener with no public address. The per-request check no longer trusts Host at all: it must name the configured listener, and a request carrying a forwarding header gets no session. The files were never checked either. directories were created 0755 and the audit log 0644, and a file keeps the mode it was created with, so an upgraded instance stayed exposed. internal/secure creates secrets at 0600 in directories at 0700, tightens what an existing install left, keeps group access for a shared backup group, and refuses to start when a mode cannot be changed. Every audit writer and the account backup go through it. The service unit drops all capabilities, sets UMask=0027, disables core dumps, hides other processes and devices, and restricts address families; docs/operations.md now states the production baseline.
docs/operations.md +45
| @@ -93,6 +93,14 @@ repository's `vole` branch. | |||
| 93 | 93 | - The SSH host key is the only long-term secret Vole stores. Keep it readable | |
| 94 | 94 | only by the Vole user (`0600`). Browser session and enrollment tokens are | |
| 95 | 95 | stored as hashes. | |
| 96 | - Vole creates its own secrets at `0600` and its data directory at `0700`: the | ||
| 97 | roster (`users.db`), the audit log, the SSH host key and the ACME account | ||
| 98 | directory. It also tightens an existing instance on startup, and logs the paths | ||
| 99 | whose mode it changed, so an upgrade from an earlier version cannot leave a | ||
| 100 | private key world-readable. If a mode cannot be changed, startup fails rather | ||
| 101 | than serving keys it cannot protect. | ||
| 102 | A site directory that is deliberately shared with a backup group is fine at | ||
| 103 | `0750`: group access is left alone, world access is not. | ||
| 96 | 104 | - Nothing Vole writes is signed: not pushes, not approvals, not CI results. Vole | |
| 97 | 105 | relies on the SSH key at the connection, the checks before a push is accepted, | |
| 98 | 106 | and the audit log. | |
| @@ -117,3 +125,40 @@ repository's `vole` branch. | |||
| 117 | 125 | otherwise put it behind a proxy that terminates TLS and keeps the `Host` | |
| 118 | 126 | header, so site routing still works. See | |
| 119 | 127 | [Configuration § Automatic HTTPS with ACME](configuration.md#automatic-https-with-acme). | |
| 128 | |||
| 129 | ## Production baseline | ||
| 130 | |||
| 131 | A public instance is a web server, a Git server and a policy engine in one | ||
| 132 | process. The settings below are the ones that matter on the day it is exposed; | ||
| 133 | the linked sections explain each in full. | ||
| 134 | |||
| 135 | - **Web behind TLS, on loopback.** Leave `https_addr` empty, bind | ||
| 136 | `http_addr = "127.0.0.1:8080"`, and put a proxy in front that terminates TLS | ||
| 137 | and passes the `Host` header through unchanged. Alternatively set `https_addr` | ||
| 138 | with `acme_email` and `acme_domains` and let Vole do it. Set `public_url` to | ||
| 139 | the canonical `https://` address so links and passkeys agree with it. See | ||
| 140 | [Configuration](configuration.md). | ||
| 141 | - **SSH firewalled.** The SSH listener is the only write transport and the only | ||
| 142 | place an account is authenticated. Expose it to the people who push, not to the | ||
| 143 | internet at large, and leave it on a port other than 22 unless you also run | ||
| 144 | `sshd` elsewhere. | ||
| 145 | - **No development shortcuts.** `dev_only` and `dev_login` are refused together | ||
| 146 | with a public address, and an instance that needs neither should set neither. | ||
| 147 | The same goes for `open_registration`, which is off by default. | ||
| 148 | - **Conservative limits.** The defaults are chosen for a small instance: | ||
| 149 | `git_http_max_concurrent = 16`, `git_http_max_per_client = 8`, | ||
| 150 | `max_blob_bytes = 100 MiB`, `receive_max_bytes = 1 GiB`, and a 600 s budget for | ||
| 151 | one clone. Raise them with measurements, not with optimism; the read path is | ||
| 152 | anonymous, so a limit that is too low is a slow clone and a limit that is | ||
| 153 | missing is an outage. | ||
| 154 | - **Audited administration.** Remote administration uses the same SSH listener | ||
| 155 | and is restricted to instance admins. Ship the audit log off the host: local | ||
| 156 | append-only is not tamper-evident. See | ||
| 157 | [Architecture § Trust model](architecture.md). | ||
| 158 | - **Backups you have restored once.** `users.db` is not derived and not | ||
| 159 | rebuildable; everything else either is, or can be re-pushed. Test the restore, | ||
| 160 | not the backup. See [Back up an instance](#back-up-an-instance). | ||
| 161 | - **The service unit.** The packaged unit drops all capabilities, sets | ||
| 162 | `UMask=0027`, and hides other processes and devices. If you serve HTTPS or SSH | ||
| 163 | on a port below 1024, grant `CAP_NET_BIND_SERVICE` with a drop-in rather than | ||
| 164 | running the service as root. See [Packaging](packaging.md). | ||