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.
forge/instance.go +50−3
| @@ -23,6 +23,7 @@ import ( | |||
| 23 | 23 | "github.com/rodent-software/vole/forge/pageview" | |
| 24 | 24 | "github.com/rodent-software/vole/forge/repo" | |
| 25 | 25 | "github.com/rodent-software/vole/forge/session" | |
| 26 | "github.com/rodent-software/vole/internal/secure" | ||
| 26 | 27 | ) | |
| 27 | 28 | ||
| 28 | 29 | // Instance is the state a running or administered Vole instance owns. | |
| @@ -46,8 +47,13 @@ type Instance struct { | |||
| 46 | 47 | // Open opens every store an instance needs, in the order their dependencies | |
| 47 | 48 | // require, and releases what it opened if one of them fails. | |
| 48 | 49 | func Open(cfg *config.Config, binPath string) (*Instance, error) { | |
| 49 | if err := os.MkdirAll(cfg.ReposDir(), 0o755); err != nil { | ||
| 50 | return nil, err | ||
| 50 | // Directories that hold secrets are created at 0700. The repositories tree is | ||
| 51 | // content, not secrets, but it lives inside the data directory and there is no | ||
| 52 | // reason for it to be wider than the directory that contains it. | ||
| 53 | for _, dir := range []string{cfg.DataDir, cfg.ReposDir(), cfg.SpoolDir(), cfg.LFSDir()} { | ||
| 54 | if err := secure.Mkdir(dir); err != nil { | ||
| 55 | return nil, fmt.Errorf("data directory %s: %w", dir, err) | ||
| 56 | } | ||
| 51 | 57 | } | |
| 52 | 58 | ident, err := identity.Open(cfg.UsersDB()) | |
| 53 | 59 | if err != nil { | |
| @@ -137,6 +143,39 @@ func Open(cfg *config.Config, binPath string) (*Instance, error) { | |||
| 137 | 143 | }, nil | |
| 138 | 144 | } | |
| 139 | 145 | ||
| 146 | // TightenSecrets brings the modes of everything that holds a credential to what | ||
| 147 | // they must be: the account database and its write-ahead files, the SSH host key, | ||
| 148 | // the ACME account directory and the audit log. It runs after the stores are open | ||
| 149 | // because opening a store may create its file, and it reports what it changed so | ||
| 150 | // the caller can say so — an upgraded instance silently changing modes is better | ||
| 151 | // than one that leaves a private key readable, but it should not be silent. | ||
| 152 | // | ||
| 153 | // A path that cannot be tightened is an error: the instance is about to serve | ||
| 154 | // credentials it cannot protect. | ||
| 155 | func TightenSecrets(cfg *config.Config) ([]string, error) { | ||
| 156 | paths := []string{ | ||
| 157 | cfg.DataDir, | ||
| 158 | cfg.ReposDir(), | ||
| 159 | cfg.SpoolDir(), | ||
| 160 | cfg.LFSDir(), | ||
| 161 | cfg.UsersDB(), cfg.UsersDB() + "-wal", cfg.UsersDB() + "-shm", | ||
| 162 | cfg.IndexDB(), cfg.IndexDB() + "-wal", cfg.IndexDB() + "-shm", | ||
| 163 | cfg.RunsDB(), cfg.RunsDB() + "-wal", cfg.RunsDB() + "-shm", | ||
| 164 | cfg.AuditLog(), | ||
| 165 | cfg.SSHHostKey(), | ||
| 166 | cfg.ACMECachePath(), | ||
| 167 | } | ||
| 168 | results, err := secure.Harden(paths...) | ||
| 169 | var changed []string | ||
| 170 | for _, r := range results { | ||
| 171 | changed = append(changed, r.Path) | ||
| 172 | } | ||
| 173 | if err != nil { | ||
| 174 | return changed, err | ||
| 175 | } | ||
| 176 | return changed, nil | ||
| 177 | } | ||
| 178 | |||
| 140 | 179 | // OpenPath loads the configuration at cfgPath and opens the instance it names. | |
| 141 | 180 | func OpenPath(cfgPath string) (*Instance, error) { | |
| 142 | 181 | cfg, err := config.Load(config.ResolvePath(cfgPath)) | |
| @@ -221,12 +260,20 @@ func InitAt(cfgPath string) (string, error) { | |||
| 221 | 260 | if err != nil { | |
| 222 | 261 | return "", err | |
| 223 | 262 | } | |
| 224 | if err := os.MkdirAll(cfg.ReposDir(), 0o755); err != nil { | ||
| 263 | if err := secure.Mkdir(cfg.DataDir); err != nil { | ||
| 264 | return "", err | ||
| 265 | } | ||
| 266 | if err := secure.Mkdir(cfg.ReposDir()); err != nil { | ||
| 225 | 267 | return "", err | |
| 226 | 268 | } | |
| 227 | 269 | if err := identity.Init(cfg.UsersDB()); err != nil { | |
| 228 | 270 | return "", err | |
| 229 | 271 | } | |
| 272 | // A roster created by an older install keeps whatever mode it was created | ||
| 273 | // with; init is also an upgrade path, so tighten it here too. | ||
| 274 | if _, err := secure.HardenFileIfExists(cfg.UsersDB()); err != nil { | ||
| 275 | return "", err | ||
| 276 | } | ||
| 230 | 277 | return abs, nil | |
| 231 | 278 | } | |
| 232 | 279 | ||