main bac21c6

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	if err := ValidateAddableCapabilities(config.AddedCapabilities); err != nil {
45		return fmt.Errorf("%w: %v", ErrRuntimeGuard, err)
46	}
47	for _, bind := range config.Binds {
48		if g.isOwnSecretBind(config, bind) {
49			continue
50		}
51		if g.overlapsProtected(bind) {
52			return fmt.Errorf("%w: bind %q overlaps a protected Gordon path", ErrRuntimeGuard, bind.Name)
53		}
54	}
55	return nil
56}
57
58func checkGuardNetworkMode(mode string) error {
59	if mode == "host" || strings.HasPrefix(mode, "container:") || strings.HasPrefix(mode, "ns:") {
60		return fmt.Errorf("%w: network mode %q is not allowed", ErrRuntimeGuard, guardNetworkLabel(mode))
61	}
62	return nil
63}
64
65// guardNetworkLabel keeps the mode kind but drops a namespace path.
66func guardNetworkLabel(mode string) string {
67	if kind, _, ok := strings.Cut(mode, ":"); ok {
68		return kind + ":"
69	}
70	return mode
71}
72
73// checkGuardVolumes refuses volume names the runtime would read as host
74// paths and turn into binds.
75func checkGuardVolumes(volumes map[string]string) error {
76	for dest, name := range volumes {
77		if strings.ContainsAny(name, `/\`) {
78			return fmt.Errorf("%w: volume for %q is not a named volume", ErrRuntimeGuard, dest)
79		}
80	}
81	return nil
82}
83
84// isOwnSecretBind reports whether bind is exactly the read-only secret file
85// mount Gordon creates: SecretFilesDir/<app>/<container>/<KEY> mounted at
86// /run/secrets/<KEY>.
87func (g RuntimeGuard) isOwnSecretBind(config *ContainerConfig, bind ContainerBind) bool {
88	if g.SecretFilesDir == "" || !bind.ReadOnly {
89		return false
90	}
91	app := config.Labels[LabelApp]
92	if app == "" || config.Name == "" {
93		return false
94	}
95	key := filepath.Base(bind.Source)
96	if err := ValidateEnvKey(key); err != nil {
97		return false
98	}
99	return bind.Source == filepath.Join(g.SecretFilesDir, app, config.Name, key) &&
100		bind.Destination == SecretFilePath(key)
101}
102
103// overlapsProtected reports whether the bind source, raw or
104// symlink-resolved, is equal to, under, or an ancestor of a protected
105// path, or of a log dir when the bind is writable. A read-only source at
106// or under a usable log dir only fails when it contains a protected path.
107func (g RuntimeGuard) overlapsProtected(bind ContainerBind) bool {
108	for _, candidate := range guardCandidates(bind.Source) {
109		readableLog := bind.ReadOnly && g.underUsableLogDir(candidate)
110		for _, protected := range g.ProtectedPaths {
111			if resolvedUnderRoot(candidate, protected) {
112				return true
113			}
114			if !readableLog && resolvedUnderRoot(protected, candidate) {
115				return true
116			}
117		}
118		if bind.ReadOnly {
119			continue
120		}
121		for _, logDir := range g.LogDirs {
122			if resolvedUnderRoot(logDir, candidate) || resolvedUnderRoot(candidate, logDir) {
123				return true
124			}
125		}
126	}
127	return false
128}
129
130// underUsableLogDir reports whether path is at or under a log dir that
131// neither is nor contains a protected path.
132func (g RuntimeGuard) underUsableLogDir(path string) bool {
133	for _, logDir := range g.LogDirs {
134		if resolvedUnderRoot(logDir, path) && !g.containsProtected(logDir) {
135			return true
136		}
137	}
138	return false
139}
140
141func (g RuntimeGuard) containsProtected(dir string) bool {
142	for _, protected := range g.ProtectedPaths {
143		if resolvedUnderRoot(dir, protected) {
144			return true
145		}
146	}
147	return false
148}
149
150func guardCandidates(source string) []string {
151	clean := filepath.Clean(source)
152	candidates := []string{clean}
153	if resolved, err := filepath.EvalSymlinks(clean); err == nil && resolved != clean {
154		candidates = append(candidates, resolved)
155	}
156	return candidates
157}

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