v3.5.0 8345e76

app_capability_policy.go RawHistoryFolder

1package domain
2
3import (
4	"fmt"
5	"slices"
6	"strings"
7)
8
9// allowedAppCapabilities is the closed set of Linux capabilities an
10// administrative capability policy may add to an app service. It is the
11// Docker/Podman default container set plus the few extra capabilities an
12// unprivileged network or VM workload legitimately needs:
13//
14//	NET_ADMIN                 configure interfaces, TAP devices, nftables
15//	                          inside the container's own network namespace
16//	IPC_LOCK                  lock guest memory (mlock)
17//	SYS_NICE, SYS_RESOURCE    scheduling priority and rlimit overrides
18//
19// Everything else is refused. In particular ALL, SYS_ADMIN, SYS_PTRACE,
20// SYS_MODULE, SYS_RAWIO, SYS_BOOT, SYS_TIME, DAC_READ_SEARCH, BPF,
21// PERFMON, MAC_ADMIN, MAC_OVERRIDE, AUDIT_CONTROL, and SYSLOG can reach
22// the host or defeat container confinement and have no policy form: a
23// workload that truly needs one is not an unprivileged service.
24var allowedAppCapabilities = []string{
25	"AUDIT_WRITE",
26	"CHOWN",
27	"DAC_OVERRIDE",
28	"FOWNER",
29	"FSETID",
30	"IPC_LOCK",
31	"KILL",
32	"MKNOD",
33	"NET_ADMIN",
34	"NET_BIND_SERVICE",
35	"NET_RAW",
36	"SETFCAP",
37	"SETGID",
38	"SETPCAP",
39	"SETUID",
40	"SYS_CHROOT",
41	"SYS_NICE",
42	"SYS_RESOURCE",
43}
44
45// defaultAppCapAdd is Gordon's base capability add set for app containers
46// (on top of cap_drop = ALL). Granted capabilities are added to it.
47var defaultAppCapAdd = []string{"CHOWN", "DAC_OVERRIDE", "FOWNER", "SETUID", "SETGID", "NET_BIND_SERVICE"}
48
49// DefaultAppCapAdd returns Gordon's base capability add set. The slice is a
50// copy.
51func DefaultAppCapAdd() []string {
52	return slices.Clone(defaultAppCapAdd)
53}
54
55// NormalizeCapability returns the canonical form of a capability name as
56// reported by an engine: upper case without the CAP_ prefix.
57func NormalizeCapability(name string) string {
58	return strings.TrimPrefix(strings.ToUpper(strings.TrimSpace(name)), "CAP_")
59}
60
61// ExcessCapabilities returns the sorted, normalized capabilities in actual
62// that are not in authorized. Both sides are normalized first, so engines
63// that report CAP_ prefixed names compare equal. A container holding fewer
64// capabilities than authorized is not excess.
65func ExcessCapabilities(actual, authorized []string) []string {
66	allowed := make(map[string]struct{}, len(authorized))
67	for _, name := range authorized {
68		allowed[NormalizeCapability(name)] = struct{}{}
69	}
70	var excess []string
71	for _, name := range actual {
72		n := NormalizeCapability(name)
73		if _, ok := allowed[n]; !ok {
74			excess = append(excess, n)
75		}
76	}
77	slices.Sort(excess)
78	return slices.Compact(excess)
79}
80
81// AllowedAppCapabilities returns the sorted capability names a policy may
82// add. The slice is a copy.
83func AllowedAppCapabilities() []string {
84	return slices.Clone(allowedAppCapabilities)
85}
86
87// AppCapabilityPolicy is one named administrative capability grant
88// declared under [app_capabilities.<name>]. The installation maps a
89// logical name to explicit Linux capabilities and exact app/service
90// allowlists. App manifests request capabilities by logical name; Gordon
91// resolves them at activation time and adds them to the container on top
92// of its default capability set. Resolution is never persisted: revisions
93// keep the logical names.
94type AppCapabilityPolicy struct {
95	Name string
96	// Add holds the explicit capability names granted (for example
97	// "NET_ADMIN"), restricted to the allowed set.
98	Add []string
99	// AllowedApps and AllowedServices are exact, non-empty allowlists.
100	AllowedApps     []string
101	AllowedServices []string
102}
103
104// ValidateCapabilityPolicyRef checks a logical capability-policy name used
105// from a manifest (service charset plus -- ban).
106func ValidateCapabilityPolicyRef(name string) error {
107	if !serviceNamePattern.MatchString(name) {
108		return fmt.Errorf("%w: capability name %q must match [a-z0-9_.-], max 63", ErrInvalidAppSpec, name)
109	}
110	if strings.Contains(name, "--") {
111		return fmt.Errorf("%w: capability name %q must not contain -- (reserved separator)", ErrInvalidAppSpec, name)
112	}
113	return nil
114}
115
116// ValidateCapabilityPolicyName checks a stable policy name (service
117// charset plus -- ban).
118func ValidateCapabilityPolicyName(name string) error {
119	if !serviceNamePattern.MatchString(name) {
120		return fmt.Errorf("%w: capability policy name %q must match [a-z0-9_.-], max 63", ErrCapabilityPolicy, name)
121	}
122	if strings.Contains(name, "--") {
123		return fmt.Errorf("%w: capability policy name %q must not contain -- (reserved separator)", ErrCapabilityPolicy, name)
124	}
125	return nil
126}
127
128// Validate checks the policy shape without touching the runtime.
129func (p AppCapabilityPolicy) Validate() error {
130	if err := ValidateCapabilityPolicyName(p.Name); err != nil {
131		return err
132	}
133	if err := validateCapabilityNames(p.Add); err != nil {
134		return fmt.Errorf("%w: capability policy %q: %v", ErrCapabilityPolicy, p.Name, err)
135	}
136	if err := validateAllowlist("app", p.AllowedApps, ValidateAppName); err != nil {
137		return fmt.Errorf("%w: capability policy %q: %w", ErrCapabilityPolicy, p.Name, err)
138	}
139	if err := validateAllowlist("service", p.AllowedServices, ValidateServiceName); err != nil {
140		return fmt.Errorf("%w: capability policy %q: %w", ErrCapabilityPolicy, p.Name, err)
141	}
142	return nil
143}
144
145// validateCapabilityNames requires a non-empty list of unique names from
146// the allowed set.
147func validateCapabilityNames(names []string) error {
148	if len(names) == 0 {
149		return fmt.Errorf("add must not be empty")
150	}
151	return ValidateAddableCapabilities(names)
152}
153
154// ValidateAddableCapabilities checks that every name is in the allowed set
155// and unique. An empty list is valid. It is also the runtime guard's check
156// on resolved capabilities, whatever produced them.
157func ValidateAddableCapabilities(names []string) error {
158	seen := map[string]struct{}{}
159	for _, name := range names {
160		if name == "" {
161			return fmt.Errorf("capability must not be empty")
162		}
163		if !slices.Contains(allowedAppCapabilities, name) {
164			return fmt.Errorf("capability %q is not allowed (allowed: %s)", name, strings.Join(allowedAppCapabilities, ", "))
165		}
166		if _, ok := seen[name]; ok {
167			return fmt.Errorf("duplicate capability %q", name)
168		}
169		seen[name] = struct{}{}
170	}
171	return nil
172}
173
174// ResolveAppCapabilities resolves the logical grant under this policy for
175// the given app and service. It validates the policy shape, enforces the
176// exact app/service allowlists, and returns the explicit capabilities.
177func (p AppCapabilityPolicy) ResolveAppCapabilities(app, service string) ([]string, error) {
178	if err := p.Validate(); err != nil {
179		return nil, err
180	}
181	if !allowlistContains(p.AllowedApps, app) {
182		return nil, fmt.Errorf("%w: capability policy %q does not allow app %q", ErrCapabilityPolicy, p.Name, app)
183	}
184	if !allowlistContains(p.AllowedServices, service) {
185		return nil, fmt.Errorf("%w: capability policy %q does not allow service %q", ErrCapabilityPolicy, p.Name, service)
186	}
187	return slices.Clone(p.Add), nil
188}
189
190// ResolveAppCapabilities resolves every logical capability policy in names
191// for one app service. It returns the deterministic, de-duplicated union
192// (sorted): two policies granting the same capability is harmless. Errors
193// name only the app, service, and logical policy.
194func ResolveAppCapabilities(app, service string, names []string, policies map[string]AppCapabilityPolicy) ([]string, error) {
195	if len(names) == 0 {
196		return nil, nil
197	}
198	var resolved []string
199	for _, name := range names {
200		policy, ok := policies[name]
201		if !ok {
202			return nil, fmt.Errorf("%w: app %q service %q capability %q: no administrative capability policy configured", ErrCapabilityPolicy, app, service, name)
203		}
204		caps, err := policy.ResolveAppCapabilities(app, service)
205		if err != nil {
206			return nil, fmt.Errorf("%w: app %q service %q capability %q: refused by administrative capability policy", ErrCapabilityPolicy, app, service, name)
207		}
208		resolved = append(resolved, caps...)
209	}
210	slices.Sort(resolved)
211	return slices.Compact(resolved), nil
212}

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