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.
|
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 | } |