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.
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.
forge/identity/identity.go +11−3
| @@ -36,6 +36,7 @@ import ( | |||
| 36 | 36 | "encoding/json" | |
| 37 | 37 | "errors" | |
| 38 | 38 | "fmt" | |
| 39 | "github.com/rodent-software/vole/internal/secure" | ||
| 39 | 40 | "os" | |
| 40 | 41 | "path/filepath" | |
| 41 | 42 | "regexp" | |
| @@ -181,7 +182,9 @@ func openDB(path string) (*sql.DB, error) { | |||
| 181 | 182 | // Init creates the roster database if it is missing. Running it on an existing | |
| 182 | 183 | // roster only re-applies the schema, which is how an upgrade adds a table. | |
| 183 | 184 | func Init(path string) error { | |
| 184 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { | ||
| 185 | // The roster's directory is created the way the roster itself is created: | ||
| 186 | // reachable by the service account and nobody else. | ||
| 187 | if err := secure.Mkdir(filepath.Dir(path)); err != nil { | ||
| 185 | 188 | return err | |
| 186 | 189 | } | |
| 187 | 190 | db, err := openDB(path) | |
| @@ -602,7 +605,12 @@ func (s *Store) Backup(dst string) error { | |||
| 602 | 605 | if err := os.Remove(dst); err != nil && !os.IsNotExist(err) { | |
| 603 | 606 | return err | |
| 604 | 607 | } | |
| 605 | _, err := s.db.Exec(`VACUUM INTO ?`, dst) | ||
| 608 | if _, err := s.db.Exec(`VACUUM INTO ?`, dst); err != nil { | ||
| 609 | return err | ||
| 610 | } | ||
| 611 | // VACUUM INTO creates the file with the process umask, and this copy holds | ||
| 612 | // every account, SSH key and passkey in the roster. | ||
| 613 | _, err := secure.HardenFile(dst) | ||
| 606 | 614 | return err | |
| 607 | 615 | } | |
| 608 | 616 | ||
| @@ -1104,7 +1112,7 @@ func (s *Store) audit(action, actor, account, detail string) { | |||
| 1104 | 1112 | if actor == "" { | |
| 1105 | 1113 | actor = "unknown" | |
| 1106 | 1114 | } | |
| 1107 | f, err := os.OpenFile(s.AuditPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) | ||
| 1115 | f, err := secure.OpenAppend(s.AuditPath) | ||
| 1108 | 1116 | if err != nil { | |
| 1109 | 1117 | return | |
| 1110 | 1118 | } | |