runtime_guard.go RawHistoryFolder

1package domain
2
3import (
4	"fmt"
5	"path/filepath"
6	"strings"
7)
8
9// RuntimeGuard is the last check on every app container config before it
10// reaches the runtime. It refuses configs that would give a workload
11// access to the host or to Gordon itself, whatever produced them.
12//
13// The zero value still refuses host/shared network namespaces and host
14// paths passed as volume names; ProtectedPaths adds the bind checks.
15type RuntimeGuard struct {
16	// ProtectedPaths are absolute host paths no bind may equal, lie under,
17	// or contain (data dir, config, runtime sockets, key stores).
18	ProtectedPaths []string
19	// SecretFilesDir is the host base directory of file-delivered secrets
20	// (<data_dir>/secrets-run). Gordon's own read-only secret binds under
21	// it are the only allowed exception.
22	SecretFilesDir string
23	// LogDirs are Gordon's log directories. Binds overlapping them are
24	// refused unless read-only; a read-only bind at or under one is
25	// allowed even when a protected path (the data dir) contains it, so
26	// a log shipper can read them. A log dir that is or contains a
27	// protected path grants no exception.
28	LogDirs []string
29}
30
31// Check refuses a config that breaks the guard. Errors wrap
32// ErrRuntimeGuard and name the bind, volume destination, or network mode,
33// never a host path.
34func (g RuntimeGuard) Check(config *ContainerConfig) error {
35	if err := checkGuardNetworkMode(config.NetworkMode); err != nil {
36		return err
37	}
38	if err := checkGuardVolumes(config.Volumes); err != nil {
39		return err
40	}
41	if err := checkGuardVolumes(config.ReadOnlyVolumes); err != nil {
42		return err
43	}
44	for _, bind := range config.Binds {
45		if g.isOwnSecretBind(config, bind) {
46			continue
47		}
48		if g.overlapsProtected(bind) {
49			return fmt.Errorf("%w: bind %q overlaps a protected Gordon path", ErrRuntimeGuard, bind.Name)
50		}
51	}
52	return nil
53}
54
55func checkGuardNetworkMode(mode string) error {
56	if mode == "host" || strings.HasPrefix(mode, "container:") || strings.HasPrefix(mode, "ns:") {
57		return fmt.Errorf("%w: network mode %q is not allowed", ErrRuntimeGuard, guardNetworkLabel(mode))
58	}
59	return nil
60}
61
62// guardNetworkLabel keeps the mode kind but drops a namespace path.
63func guardNetworkLabel(mode string) string {
64	if kind, _, ok := strings.Cut(mode, ":"); ok {
65		return kind + ":"
66	}
67	return mode
68}
69
70// checkGuardVolumes refuses volume names the runtime would read as host
71// paths and turn into binds.
72func checkGuardVolumes(volumes map[string]string) error {
73	for dest, name := range volumes {
74		if strings.ContainsAny(name, `/\`) {
75			return fmt.Errorf("%w: volume for %q is not a named volume", ErrRuntimeGuard, dest)
76		}
77	}
78	return nil
79}
80
81// isOwnSecretBind reports whether bind is exactly the read-only secret file
82// mount Gordon creates: SecretFilesDir/<app>/<container>/<KEY> mounted at
83// /run/secrets/<KEY>.
84func (g RuntimeGuard) isOwnSecretBind(config *ContainerConfig, bind ContainerBind) bool {
85	if g.SecretFilesDir == "" || !bind.ReadOnly {
86		return false
87	}
88	app := config.Labels[LabelApp]
89	if app == "" || config.Name == "" {
90		return false
91	}
92	key := filepath.Base(bind.Source)
93	if err := ValidateEnvKey(key); err != nil {
94		return false
95	}
96	return bind.Source == filepath.Join(g.SecretFilesDir, app, config.Name, key) &&
97		bind.Destination == SecretFilePath(key)
98}
99
100// overlapsProtected reports whether the bind source, raw or
101// symlink-resolved, is equal to, under, or an ancestor of a protected
102// path, or of a log dir when the bind is writable. A read-only source at
103// or under a usable log dir only fails when it contains a protected path.
104func (g RuntimeGuard) overlapsProtected(bind ContainerBind) bool {
105	for _, candidate := range guardCandidates(bind.Source) {
106		readableLog := bind.ReadOnly && g.underUsableLogDir(candidate)
107		for _, protected := range g.ProtectedPaths {
108			if resolvedUnderRoot(candidate, protected) {
109				return true
110			}
111			if !readableLog && resolvedUnderRoot(protected, candidate) {
112				return true
113			}
114		}
115		if bind.ReadOnly {
116			continue
117		}
118		for _, logDir := range g.LogDirs {
119			if resolvedUnderRoot(logDir, candidate) || resolvedUnderRoot(candidate, logDir) {
120				return true
121			}
122		}
123	}
124	return false
125}
126
127// underUsableLogDir reports whether path is at or under a log dir that
128// neither is nor contains a protected path.
129func (g RuntimeGuard) underUsableLogDir(path string) bool {
130	for _, logDir := range g.LogDirs {
131		if resolvedUnderRoot(logDir, path) && !g.containsProtected(logDir) {
132			return true
133		}
134	}
135	return false
136}
137
138func (g RuntimeGuard) containsProtected(dir string) bool {
139	for _, protected := range g.ProtectedPaths {
140		if resolvedUnderRoot(dir, protected) {
141			return true
142		}
143	}
144	return false
145}
146
147func guardCandidates(source string) []string {
148	clean := filepath.Clean(source)
149	candidates := []string{clean}
150	if resolved, err := filepath.EvalSymlinks(clean); err == nil && resolved != clean {
151		candidates = append(candidates, resolved)
152	}
153	return candidates
154}

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