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.

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 }