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.

Blame · internal/secure/secure.go

Line attribution on main · 150 line(s)

ed181212
Keenan Nemetz
13d ago
1 // Package secure creates and checks the files that hold secrets: the instance
2 // directories, the account database, the SSH host key, the ACME account key and
3 // the audit log.
4 //
5 // File modes are a security control here, not a convention. A world-readable
6 // users.db is every SSH key in the roster and every passkey; a world-readable
7 // host key is a forge anybody can impersonate; a world-readable audit log can be
8 // read by the person it records. Creating them at 0600 is not enough on its own,
9 // because a file that already exists keeps whatever mode it was created with —
10 // which is how an instance upgraded from an earlier version stays exposed. So
11 // creation and tightening are the same operation, and a mode that cannot be
12 // tightened is an error rather than a warning: an instance that cannot protect
13 // its keys must not start serving them.
14 package secure
15
16 import (
17 "fmt"
18 "os"
19 )
20
21 const (
22 // DirMode is the mode of a directory that holds secrets. It gives the service
23 // account everything and nobody else anything.
24 DirMode os.FileMode = 0o700
25 // FileMode is the mode of a secret file: the owner reads and writes, nobody
26 // else can do either.
27 FileMode os.FileMode = 0o600
28 )
29
30 // Mkdir creates path and any missing parents at DirMode, then removes world
31 // access from a directory that already exists. Group access is left alone: a
32 // packaged instance may deliberately use 0750 with a dedicated group.
33 func Mkdir(path string) error {
34 if err := os.MkdirAll(path, DirMode); err != nil {
35 return err
36 }
37 _, err := HardenDir(path)
38 return err
39 }
40
41 // HardenDir removes world access from an existing directory and reports whether
42 // it had to change anything.
43 func HardenDir(path string) (bool, error) {
44 st, err := os.Stat(path)
45 if err != nil {
46 return false, err
47 }
48 if !st.IsDir() {
49 return false, fmt.Errorf("%s is not a directory", path)
50 }
51 mode := st.Mode().Perm()
52 if mode&0o007 == 0 {
53 return false, nil
54 }
55 want := mode &^ 0o007
56 if err := os.Chmod(path, want); err != nil {
57 return false, fmt.Errorf("%s is %04o and cannot be tightened to %04o: %w", path, mode, want, err)
58 }
59 return true, nil
60 }
61
62 // HardenFile forces an existing secret file to FileMode and reports whether it
63 // had to change anything.
64 func HardenFile(path string) (bool, error) {
65 st, err := os.Stat(path)
66 if err != nil {
67 return false, err
68 }
69 if st.IsDir() {
70 return false, fmt.Errorf("%s is a directory, expected a file", path)
71 }
72 if st.Mode().Perm() == FileMode {
73 return false, nil
74 }
75 had := st.Mode().Perm()
76 if err := os.Chmod(path, FileMode); err != nil {
77 return false, fmt.Errorf("%s is %04o and cannot be tightened to %04o: %w", path, had, FileMode, err)
78 }
79 return true, nil
80 }
81
82 // HardenFileIfExists is HardenFile for a file that may not exist yet.
83 func HardenFileIfExists(path string) (bool, error) {
84 changed, err := HardenFile(path)
85 if os.IsNotExist(err) {
86 return false, nil
87 }
88 return changed, err
89 }
90
91 // OpenAppend opens a secret file for appending, creating it at FileMode. An
92 // existing file is tightened first, so a log inherited from an older install
93 // stops being readable by everyone the next time it is written.
94 func OpenAppend(path string) (*os.File, error) {
95 f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, FileMode)
96 if err != nil {
97 return nil, err
98 }
99 if _, err := HardenFile(path); err != nil {
100 f.Close()
101 return nil, err
102 }
103 return f, nil
104 }
105
106 // WriteFile writes a secret file at FileMode, replacing whatever mode an existing
107 // file had.
108 func WriteFile(path string, data []byte) error {
109 if err := os.WriteFile(path, data, FileMode); err != nil {
110 return err
111 }
112 _, err := HardenFile(path)
113 return err
114 }
115
116 // Result is one path's outcome, so a caller can report what it changed.
117 type Result struct {
118 Path string
119 Changed bool
120 }
121
122 // Harden applies the right mode to each path: directories lose world access,
123 // files become 0600. Missing paths are skipped, because a store that has not been
124 // created yet will be created at the right mode. The first failure is returned
125 // with everything that did change, so the caller can still report progress.
126 func Harden(paths ...string) ([]Result, error) {
127 var results []Result
128 for _, path := range paths {
129 st, err := os.Stat(path)
130 if os.IsNotExist(err) {
131 continue
132 }
133 if err != nil {
134 return results, err
135 }
136 var changed bool
137 if st.IsDir() {
138 changed, err = HardenDir(path)
139 } else {
140 changed, err = HardenFile(path)
141 }
142 if err != nil {
143 return results, err
144 }
145 if changed {
146 results = append(results, Result{Path: path, Changed: true})
147 }
148 }
149 return results, nil
150 }