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/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