v3.2.0 4fc01fa

runtime_guard.go RawHistoryFolder

1package app
2
3import (
4	"os"
5	"path/filepath"
6	"slices"
7	"strings"
8
9	"github.com/bnema/gordon/internal/adapters/out/secretfiles"
10	"github.com/bnema/gordon/internal/boundaries/out"
11	"github.com/bnema/gordon/internal/domain"
12)
13
14// protectedRuntimePaths lists the host paths no app bind may equal, lie
15// under, or contain: Gordon's data, config, and configured files, runtime
16// sockets and storage, Gordon's runtime credentials, and key stores.
17// Empty entries are skipped; each entry is made absolute and its
18// symlink-resolved form is added when it differs.
19func protectedRuntimePaths(dataDir string, gordonPaths []string, getenv func(string) string, home string) []string {
20	runtimeDir := getenv("XDG_RUNTIME_DIR")
21	configHome := getenv("XDG_CONFIG_HOME")
22	if configHome == "" {
23		configHome = subPath(home, ".config")
24	}
25	dockerHost, _ := strings.CutPrefix(getenv("DOCKER_HOST"), "unix://")
26	if strings.Contains(dockerHost, "://") {
27		dockerHost = ""
28	}
29	raw := append([]string{
30		dataDir,
31		"/var/run/docker.sock",
32		"/run/docker.sock",
33		"/run/podman/podman.sock",
34		subPath(runtimeDir, "docker.sock"),
35		subPath(runtimeDir, "podman", "podman.sock"),
36		subPath(runtimeDir, "gordon"),
37		subPath(home, ".gordon", "run"),
38		dockerHost,
39		subPath(home, ".local", "share", "containers"),
40		subPath(home, ".local", "share", "docker"),
41		subPath(configHome, "containers"),
42		subPath(home, ".docker"),
43		subPath(configHome, "systemd", "user"),
44		getenv("GORDON_CLOUDFLARE_API_TOKEN_FILE"),
45		getenv("GNUPGHOME"),
46		subPath(home, ".gnupg"),
47		getenv("PASSWORD_STORE_DIR"),
48		subPath(home, ".password-store"),
49		getenv("SOPS_AGE_KEY_FILE"),
50		subPath(configHome, "sops", "age"),
51	}, gordonPaths...)
52	return absWithResolved(raw)
53}
54
55// absWithResolved makes each non-empty path absolute, adds its
56// symlink-resolved form when it differs, and returns the sorted set.
57func absWithResolved(raw []string) []string {
58	var paths []string
59	for _, p := range raw {
60		if p == "" {
61			continue
62		}
63		// A relative data dir or config path is resolved against the
64		// working directory, as the daemon itself resolves it.
65		clean, err := filepath.Abs(p)
66		if err != nil {
67			continue
68		}
69		paths = append(paths, clean)
70		if resolved := resolveExisting(clean); resolved != clean {
71			paths = append(paths, resolved)
72		}
73	}
74	slices.Sort(paths)
75	return slices.Compact(paths)
76}
77
78// resolveExisting resolves symlinks in the longest existing ancestor of p
79// and re-appends the missing tail, so a path created after startup under
80// a symlinked parent (for example /home -> /var/home) is still protected
81// in its resolved form.
82func resolveExisting(p string) string {
83	tail := ""
84	for dir := p; ; dir = filepath.Dir(dir) {
85		if resolved, err := filepath.EvalSymlinks(dir); err == nil {
86			return filepath.Join(resolved, tail)
87		}
88		parent := filepath.Dir(dir)
89		if parent == dir {
90			return p
91		}
92		tail = filepath.Join(filepath.Base(dir), tail)
93	}
94}
95
96// subPath joins elems under base, or returns "" when base is empty.
97func subPath(base string, elems ...string) string {
98	if base == "" {
99		return ""
100	}
101	return filepath.Join(append([]string{base}, elems...)...)
102}
103
104// configuredGordonPaths returns the config file, the runtime socket in use,
105// and every private file or directory the config points Gordon at. Log
106// paths are left to gordonLogLocations. The public TLS certificate is left out
107// so its directory (often a shared CA bundle) stays bindable.
108func configuredGordonPaths(si *serviceInit) []string {
109	cfg := si.cfg
110	paths := []string{
111		cfg.Server.TLSKeyFile,
112		cfg.Backups.StorageDir,
113		cfg.Backups.Databases.StorageDir,
114	}
115	if si.v != nil {
116		paths = append(paths, si.v.ConfigFileUsed(), resolveRuntimeConfig(si.v.GetString("server.runtime")))
117	}
118	return paths
119}
120
121// logLocation is one enabled Gordon log: the directory a read-only bind
122// may expose and the exact file or directory Gordon writes.
123type logLocation struct {
124	dir  string
125	path string
126}
127
128// readableLogDirs splits the log locations into dirs that may be bound
129// read-only and exact log paths that must stay protected instead. A dir
130// under the data dir is readable only at or under <data_dir>/logs; a dir
131// outside it only when it neither is, contains, nor lies under another
132// protected path. So a misconfigured log path never opens a read-only
133// exception on secrets, key stores, or runtime dirs, and a dropped log
134// stays write-protected.
135func readableLogDirs(locations []logLocation, protected []string, dataDir string) (readable, keepProtected []string) {
136	dataDirs := absWithResolved([]string{dataDir})
137	dataLogs := absWithResolved([]string{filepath.Join(dataDir, "logs")})
138	for _, loc := range locations {
139		if readableLogDir(loc.dir, protected, dataDirs, dataLogs) {
140			readable = append(readable, loc.dir)
141		} else {
142			keepProtected = append(keepProtected, loc.path)
143		}
144	}
145	return readable, keepProtected
146}
147
148func readableLogDir(dir string, protected, dataDirs, dataLogs []string) bool {
149	underData := slices.ContainsFunc(dataDirs, func(d string) bool { return pathWithin(d, dir) })
150	if underData && !slices.ContainsFunc(dataLogs, func(l string) bool { return pathWithin(l, dir) }) {
151		return false
152	}
153	for _, p := range protected {
154		if underData && slices.Contains(dataDirs, p) {
155			continue
156		}
157		if pathWithin(p, dir) || pathWithin(dir, p) {
158			return false
159		}
160	}
161	return true
162}
163
164// pathWithin reports whether target is root or lies under it.
165func pathWithin(root, target string) bool {
166	rel, err := filepath.Rel(root, target)
167	return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
168}
169
170// gordonLogLocations returns the enabled file logs: Gordon's process log,
171// the access log, and the container logs dir, with paths made absolute.
172func gordonLogLocations(cfg Config, dataDir string) []logLocation {
173	var locs []logLocation
174	add := func(dir, path string) {
175		dir, errDir := filepath.Abs(dir)
176		path, errPath := filepath.Abs(path)
177		if errDir == nil && errPath == nil {
178			locs = append(locs, logLocation{dir: dir, path: path})
179		}
180	}
181	if cfg.Logging.File.Enabled {
182		p := cfg.Logging.File.Path
183		if p == "" {
184			p = filepath.Join(dataDir, "logs", "gordon.log")
185		}
186		add(filepath.Dir(p), p)
187	}
188	if cfg.Logging.AccessLog.Enabled && cfg.Logging.AccessLog.Output == "file" {
189		p := cfg.Logging.AccessLog.FilePath
190		if p == "" {
191			p = filepath.Join(dataDir, "logs", "access.log")
192		}
193		add(filepath.Dir(p), p)
194	}
195	if cfg.Logging.ContainerLogs.Enabled {
196		p := cfg.Logging.ContainerLogs.Dir
197		if p == "" {
198			p = filepath.Join(dataDir, "logs", "containers")
199		}
200		add(p, p)
201	}
202	return locs
203}
204
205// buildAppRuntimeSafety opens the host secret file store and builds the
206// runtime guard applied to every app container create. The store keeps one
207// directory handle open for the daemon's lifetime.
208func buildAppRuntimeSafety(si *serviceInit, dataDir string) (out.SecretFileStore, domain.RuntimeGuard, error) {
209	store, err := secretfiles.NewStore(dataDir)
210	if err != nil {
211		return nil, domain.RuntimeGuard{}, err
212	}
213	home, _ := os.UserHomeDir()
214	paths := protectedRuntimePaths(dataDir, configuredGordonPaths(si), os.Getenv, home)
215	logDirs, logPaths := readableLogDirs(gordonLogLocations(si.cfg, dataDir), paths, dataDir)
216	paths = absWithResolved(append(paths, logPaths...))
217	return store, domain.RuntimeGuard{
218		ProtectedPaths: paths,
219		SecretFilesDir: store.Dir(),
220		LogDirs:        absWithResolved(logDirs),
221	}, nil
222}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close