forked from snapcore/snapd
/
apparmor.go
414 lines (368 loc) · 12.4 KB
/
apparmor.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2018 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package apparmor
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/strutil"
)
// LevelType encodes the kind of support for apparmor
// found on this system.
type LevelType int
const (
// Unknown indicates that apparmor was not probed yet.
Unknown LevelType = iota
// Unsupported indicates that apparmor is not enabled.
Unsupported
// Unusable indicates that apparmor is enabled but cannot be used.
Unusable
// Partial indicates that apparmor is enabled but some
// features are missing.
Partial
// Full indicates that all features are supported.
Full
)
func setupConfCacheDirs(newrootdir string) {
ConfDir = filepath.Join(newrootdir, "/etc/apparmor.d")
CacheDir = filepath.Join(newrootdir, "/var/cache/apparmor")
SystemCacheDir = filepath.Join(ConfDir, "cache")
exists, isDir, _ := osutil.DirExists(SystemCacheDir)
if !exists || !isDir {
// some systems use a single cache dir instead of splitting
// out the system cache
// TODO: it seems Solus has a different setup too, investigate this
SystemCacheDir = CacheDir
}
}
func init() {
dirs.AddRootDirCallback(setupConfCacheDirs)
setupConfCacheDirs(dirs.GlobalRootDir)
}
var (
ConfDir string
CacheDir string
SystemCacheDir string
)
func (level LevelType) String() string {
switch level {
case Unknown:
return "unknown"
case Unsupported:
return "none"
case Unusable:
return "unusable"
case Partial:
return "partial"
case Full:
return "full"
}
return fmt.Sprintf("AppArmorLevelType:%d", level)
}
// appArmorAssessment represents what is supported AppArmor-wise by the system.
var appArmorAssessment = &appArmorAssess{appArmorProber: &appArmorProbe{}}
// ProbedLevel quantifies how well apparmor is supported on the current
// kernel. The computation is costly to perform. The result is cached internally.
func ProbedLevel() LevelType {
appArmorAssessment.assess()
return appArmorAssessment.level
}
// Summary describes how well apparmor is supported on the current
// kernel. The computation is costly to perform. The result is cached
// internally.
func Summary() string {
appArmorAssessment.assess()
return appArmorAssessment.summary
}
// KernelFeatures returns a sorted list of apparmor features like
// []string{"dbus", "network"}. The result is cached internally.
func KernelFeatures() ([]string, error) {
return appArmorAssessment.KernelFeatures()
}
// ParserFeatures returns a sorted list of apparmor parser features
// like []string{"unsafe", ...}. The computation is costly to perform. The
// result is cached internally.
func ParserFeatures() ([]string, error) {
return appArmorAssessment.ParserFeatures()
}
// ParserMtime returns the mtime of the AppArmor parser, else 0.
func ParserMtime() int64 {
var mtime int64
mtime = 0
if path, err := findAppArmorParser(); err == nil {
if fi, err := os.Stat(path); err == nil {
mtime = fi.ModTime().Unix()
}
}
return mtime
}
// probe related code
var (
// requiredParserFeatures denotes the features that must be present in the parser.
// Absence of any of those features results in the effective level be at most UnusableAppArmor.
requiredParserFeatures = []string{
"unsafe",
}
// preferredParserFeatures denotes the features that should be present in the parser.
// Absence of any of those features results in the effective level be at most PartialAppArmor.
preferredParserFeatures = []string{
"unsafe",
}
// requiredKernelFeatures denotes the features that must be present in the kernel.
// Absence of any of those features results in the effective level be at most UnusableAppArmor.
requiredKernelFeatures = []string{
// For now, require at least file and simply prefer the rest.
"file",
}
// preferredKernelFeatures denotes the features that should be present in the kernel.
// Absence of any of those features results in the effective level be at most PartialAppArmor.
preferredKernelFeatures = []string{
"caps",
"dbus",
"domain",
"file",
"mount",
"namespaces",
"network",
"ptrace",
"signal",
}
// Since AppArmorParserMtime() will be called by generateKey() in
// system-key and that could be called by different users on the
// system, use a predictable search path for finding the parser.
parserSearchPath = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
// Filesystem root defined locally to avoid dependency on the
// 'dirs' package
rootPath = "/"
)
// Each apparmor feature is manifested as a directory entry.
const featuresSysPath = "sys/kernel/security/apparmor/features"
type appArmorProber interface {
KernelFeatures() ([]string, error)
ParserFeatures() ([]string, error)
}
type appArmorAssess struct {
appArmorProber
// level contains the assessment of the "level" of apparmor support.
level LevelType
// summary contains a human readable description of the assessment.
summary string
once sync.Once
}
func (aaa *appArmorAssess) assess() {
aaa.once.Do(func() {
aaa.level, aaa.summary = aaa.doAssess()
})
}
func (aaa *appArmorAssess) doAssess() (level LevelType, summary string) {
// First, quickly check if apparmor is available in the kernel at all.
kernelFeatures, err := aaa.KernelFeatures()
if os.IsNotExist(err) {
return Unsupported, "apparmor not enabled"
}
// Then check that the parser supports the required parser features.
// If we have any missing required features then apparmor is unusable.
parserFeatures, err := aaa.ParserFeatures()
if os.IsNotExist(err) {
return Unsupported, "apparmor_parser not found"
}
var missingParserFeatures []string
for _, feature := range requiredParserFeatures {
if !strutil.SortedListContains(parserFeatures, feature) {
missingParserFeatures = append(missingParserFeatures, feature)
}
}
if len(missingParserFeatures) > 0 {
summary := fmt.Sprintf("apparmor_parser is available but required parser features are missing: %s",
strings.Join(missingParserFeatures, ", "))
return Unusable, summary
}
// Next, check that the kernel supports the required kernel features.
var missingKernelFeatures []string
for _, feature := range requiredKernelFeatures {
if !strutil.SortedListContains(kernelFeatures, feature) {
missingKernelFeatures = append(missingKernelFeatures, feature)
}
}
if len(missingKernelFeatures) > 0 {
summary := fmt.Sprintf("apparmor is enabled but required kernel features are missing: %s",
strings.Join(missingKernelFeatures, ", "))
return Unusable, summary
}
// Next check that the parser supports preferred parser features.
// If we have any missing preferred features then apparmor is partially enabled.
for _, feature := range preferredParserFeatures {
if !strutil.SortedListContains(parserFeatures, feature) {
missingParserFeatures = append(missingParserFeatures, feature)
}
}
if len(missingParserFeatures) > 0 {
summary := fmt.Sprintf("apparmor_parser is available but some features are missing: %s",
strings.Join(missingParserFeatures, ", "))
return Partial, summary
}
// Lastly check that the kernel supports preferred kernel features.
for _, feature := range preferredKernelFeatures {
if !strutil.SortedListContains(kernelFeatures, feature) {
missingKernelFeatures = append(missingKernelFeatures, feature)
}
}
if len(missingKernelFeatures) > 0 {
summary := fmt.Sprintf("apparmor is enabled but some kernel features are missing: %s",
strings.Join(missingKernelFeatures, ", "))
return Partial, summary
}
// If we got here then all features are available and supported.
return Full, "apparmor is enabled and all features are available"
}
type appArmorProbe struct {
// kernelFeatures contains a list of kernel features that are supported.
kernelFeatures []string
// kernelError contains an error, if any, encountered when
// discovering available kernel features.
kernelError error
// parserFeatures contains a list of parser features that are supported.
parserFeatures []string
// parserError contains an error, if any, encountered when
// discovering available parser features.
parserError error
probeKernelOnce sync.Once
probeParserOnce sync.Once
}
func (aap *appArmorProbe) KernelFeatures() ([]string, error) {
aap.probeKernelOnce.Do(func() {
aap.kernelFeatures, aap.kernelError = probeKernelFeatures()
})
return aap.kernelFeatures, aap.kernelError
}
func (aap *appArmorProbe) ParserFeatures() ([]string, error) {
aap.probeParserOnce.Do(func() {
aap.parserFeatures, aap.parserError = probeParserFeatures()
})
return aap.parserFeatures, aap.parserError
}
func probeKernelFeatures() ([]string, error) {
// note that ioutil.ReadDir() is already sorted
dentries, err := ioutil.ReadDir(filepath.Join(rootPath, featuresSysPath))
if err != nil {
return []string{}, err
}
features := make([]string, 0, len(dentries))
for _, fi := range dentries {
if fi.IsDir() {
features = append(features, fi.Name())
}
}
return features, nil
}
func probeParserFeatures() ([]string, error) {
parser, err := findAppArmorParser()
if err != nil {
return []string{}, err
}
features := make([]string, 0, 1)
if tryAppArmorParserFeature(parser, "change_profile unsafe /**,") {
features = append(features, "unsafe")
}
sort.Strings(features)
return features, nil
}
// findAppArmorParser returns the path of the apparmor_parser binary if one is found.
func findAppArmorParser() (string, error) {
for _, dir := range filepath.SplitList(parserSearchPath) {
path := filepath.Join(dir, "apparmor_parser")
if _, err := os.Stat(path); err == nil {
return path, nil
}
}
return "", os.ErrNotExist
}
// tryAppArmorParserFeature attempts to pre-process a bit of apparmor syntax with a given parser.
func tryAppArmorParserFeature(parser, rule string) bool {
cmd := exec.Command(parser, "--preprocess")
cmd.Stdin = bytes.NewBufferString(fmt.Sprintf("profile snap-test {\n %s\n}", rule))
if err := cmd.Run(); err != nil {
return false
}
return true
}
// mocking
type mockAppArmorProbe struct {
kernelFeatures []string
kernelError error
parserFeatures []string
parserError error
}
func (m *mockAppArmorProbe) KernelFeatures() ([]string, error) {
return m.kernelFeatures, m.kernelError
}
func (m *mockAppArmorProbe) ParserFeatures() ([]string, error) {
return m.parserFeatures, m.parserError
}
// MockAppArmorLevel makes the system believe it has certain level of apparmor
// support.
//
// AppArmor kernel and parser features are set to unrealistic values that do
// not match the requested level. Use this function to observe behavior that
// relies solely on the apparmor level value.
func MockLevel(level LevelType) (restore func()) {
oldAppArmorAssessment := appArmorAssessment
mockProbe := &mockAppArmorProbe{
kernelFeatures: []string{"mocked-kernel-feature"},
parserFeatures: []string{"mocked-parser-feature"},
}
appArmorAssessment = &appArmorAssess{
appArmorProber: mockProbe,
level: level,
summary: fmt.Sprintf("mocked apparmor level: %s", level),
}
appArmorAssessment.once.Do(func() {})
return func() {
appArmorAssessment = oldAppArmorAssessment
}
}
// MockAppArmorFeatures makes the system believe it has certain kernel and
// parser features.
//
// AppArmor level and summary are automatically re-assessed as needed
// on both the change and the restore process. Use this function to
// observe real assessment of arbitrary features.
func MockFeatures(kernelFeatures []string, kernelError error, parserFeatures []string, parserError error) (restore func()) {
oldAppArmorAssessment := appArmorAssessment
mockProbe := &mockAppArmorProbe{
kernelFeatures: kernelFeatures,
kernelError: kernelError,
parserFeatures: parserFeatures,
parserError: parserError,
}
appArmorAssessment = &appArmorAssess{
appArmorProber: mockProbe,
}
appArmorAssessment.assess()
return func() {
appArmorAssessment = oldAppArmorAssessment
}
}