Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
Already on GitHub? Sign in to your account
apparmor,release: add better apparmor detection/mocking code #3808
ef40971
66e29b2
401fe35
d0b527c
ba9974b
3e7da29
c382b22
ee03ff0
f1f03bb
0f10b7d
534e497
6fc7559
| @@ -0,0 +1,151 @@ | ||
| +// -*- Mode: Go; indent-tabs-mode: t -*- | ||
| + | ||
| +/* | ||
| + * Copyright (C) 2017 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 ( | ||
| + "fmt" | ||
| + "io/ioutil" | ||
| + "os" | ||
| + "path/filepath" | ||
| + "sort" | ||
| + "strings" | ||
| +) | ||
| + | ||
| +// FeatureLevel encodes the kind of support for apparmor found on this system. | ||
| +type FeatureLevel int | ||
| + | ||
| +const ( | ||
| + // None indicates that apparmor is not enabled. | ||
| + None FeatureLevel = iota | ||
| + // Partial indicates that apparmor is enabled but some features are missing. | ||
| + Partial | ||
| + // Full indicates that all features are supported. | ||
| + Full | ||
| +) | ||
| + | ||
| +var ( | ||
| + // featureSysPath points to the sysfs directory where apparmor features are listed. | ||
| + featuresSysPath = "/sys/kernel/security/apparmor/features" | ||
|
|
||
| + // requiredFeatures are the apparmor features needed for strict confinement. | ||
| + requiredFeatures = []string{ | ||
| + "caps", | ||
| + "dbus", | ||
| + "domain", | ||
| + "file", | ||
| + "mount", | ||
| + "namespaces", | ||
| + "network", | ||
| + "ptrace", | ||
| + "rlimit", | ||
| + "signal", | ||
| + } | ||
| +) | ||
| + | ||
| +// KernelSupport describes apparmor features supported by the kernel. | ||
| +type KernelSupport struct { | ||
| + enabled bool | ||
| + features map[string]bool | ||
| +} | ||
| + | ||
| +// ProbeKernel checks which apparmor features are available. | ||
| +func ProbeKernel() *KernelSupport { | ||
mvo5
Collaborator
|
||
| + entries, err := ioutil.ReadDir(featuresSysPath) | ||
| + if err != nil { | ||
| + return nil | ||
| + } | ||
| + ks := &KernelSupport{ | ||
| + enabled: err == nil, | ||
| + features: make(map[string]bool, len(entries)), | ||
| + } | ||
| + for _, entry := range entries { | ||
| + // Each sub-directory represents a speicfic feature. Some have more | ||
| + // details as additional sub-directories or files therein but we are | ||
| + // not inspecting that at the moment. | ||
| + if entry.IsDir() { | ||
| + ks.features[entry.Name()] = true | ||
| + } | ||
| + } | ||
| + return ks | ||
| +} | ||
| + | ||
| +// IsEnabled returns true if apparmor is enabled. | ||
| +func (ks *KernelSupport) IsEnabled() bool { | ||
| + return ks != nil && ks.enabled | ||
mvo5
Collaborator
|
||
| +} | ||
| + | ||
| +// SupportsFeature returns true if a given apparmor feature is supported. | ||
| +func (ks *KernelSupport) SupportsFeature(feature string) bool { | ||
| + return ks != nil && ks.features[feature] | ||
|
|
||
| +} | ||
| + | ||
| +// Evaluate checks if the apparmor module is enabled and if all the required features are available. | ||
| +func (ks *KernelSupport) Evaluate() (level FeatureLevel, summary string) { | ||
| + if !ks.IsEnabled() { | ||
| + return None, fmt.Sprintf("apparmor is not enabled") | ||
| + } | ||
| + var missing []string | ||
| + for _, feature := range requiredFeatures { | ||
| + if !ks.SupportsFeature(feature) { | ||
| + missing = append(missing, feature) | ||
| + } | ||
| + } | ||
| + if len(missing) > 0 { | ||
| + sort.Strings(missing) | ||
| + return Partial, fmt.Sprintf("apparmor is enabled but some features are missing: %s", strings.Join(missing, ", ")) | ||
| + } | ||
| + return Full, "apparmor is enabled and all features are available" | ||
| +} | ||
| + | ||
| +// MockFeatureLevel fakes the desired apparmor feature level. | ||
| +func MockFeatureLevel(level FeatureLevel) (restore func()) { | ||
| + oldFeaturesSysPath := featuresSysPath | ||
| + | ||
| + temp, err := ioutil.TempDir("", "mock-apparmor-feature-level") | ||
| + if err != nil { | ||
| + panic(err) | ||
| + } | ||
| + featuresSysPath = filepath.Join(temp, "features") | ||
| + | ||
| + switch level { | ||
| + case None: | ||
| + // create no directory at all (apparmor not available). | ||
| + case Partial: | ||
| + // create several feature directories, matching vanilla 4.12 kernel. | ||
| + for _, feature := range []string{"caps", "domain", "file", "network", "policy", "rlimit"} { | ||
| + if err := os.MkdirAll(filepath.Join(featuresSysPath, feature), 0755); err != nil { | ||
| + panic(err) | ||
| + } | ||
| + } | ||
| + case Full: | ||
| + // create all the feature directories, matching Ubuntu kernels. | ||
| + for _, feature := range requiredFeatures { | ||
| + if err := os.MkdirAll(filepath.Join(featuresSysPath, feature), 0755); err != nil { | ||
| + panic(err) | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + return func() { | ||
| + if err := os.RemoveAll(temp); err != nil { | ||
| + panic(err) | ||
| + } | ||
| + featuresSysPath = oldFeaturesSysPath | ||
| + } | ||
| +} | ||
| @@ -0,0 +1,77 @@ | ||
| +// -*- Mode: Go; indent-tabs-mode: t -*- | ||
| + | ||
| +/* | ||
| + * Copyright (C) 2017 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_test | ||
| + | ||
| +import ( | ||
| + . "gopkg.in/check.v1" | ||
| + "testing" | ||
| + | ||
| + "github.com/snapcore/snapd/apparmor" | ||
| +) | ||
| + | ||
| +func Test(t *testing.T) { | ||
| + TestingT(t) | ||
| +} | ||
| + | ||
| +type probeSuite struct{} | ||
| + | ||
| +var _ = Suite(&probeSuite{}) | ||
| + | ||
| +func (s *probeSuite) TestMockProbeNone(c *C) { | ||
| + restore := apparmor.MockFeatureLevel(apparmor.None) | ||
| + defer restore() | ||
| + | ||
| + ks := apparmor.ProbeKernel() | ||
| + c.Assert(ks.IsEnabled(), Equals, false) | ||
| + c.Assert(ks.SupportsFeature("dbus"), Equals, false) | ||
| + c.Assert(ks.SupportsFeature("file"), Equals, false) | ||
| + | ||
| + level, summary := ks.Evaluate() | ||
| + c.Assert(level, Equals, apparmor.None) | ||
| + c.Assert(summary, Equals, "apparmor is not enabled") | ||
| +} | ||
| + | ||
| +func (s *probeSuite) TestMockProbePartial(c *C) { | ||
| + restore := apparmor.MockFeatureLevel(apparmor.Partial) | ||
| + defer restore() | ||
| + | ||
| + ks := apparmor.ProbeKernel() | ||
| + c.Assert(ks.IsEnabled(), Equals, true) | ||
| + c.Assert(ks.SupportsFeature("dbus"), Equals, false) | ||
| + c.Assert(ks.SupportsFeature("file"), Equals, true) | ||
| + | ||
| + level, summary := ks.Evaluate() | ||
| + c.Assert(level, Equals, apparmor.Partial) | ||
| + c.Assert(summary, Equals, "apparmor is enabled but some features are missing: dbus, mount, namespaces, ptrace, signal") | ||
| +} | ||
| + | ||
| +func (s *probeSuite) TestMockProbeFull(c *C) { | ||
| + restore := apparmor.MockFeatureLevel(apparmor.Full) | ||
| + defer restore() | ||
| + | ||
| + ks := apparmor.ProbeKernel() | ||
| + c.Assert(ks.IsEnabled(), Equals, true) | ||
| + c.Assert(ks.SupportsFeature("dbus"), Equals, true) | ||
| + c.Assert(ks.SupportsFeature("file"), Equals, true) | ||
| + | ||
| + level, summary := ks.Evaluate() | ||
| + c.Assert(level, Equals, apparmor.Full) | ||
| + c.Assert(summary, Equals, "apparmor is enabled and all features are available") | ||
| +} |
| @@ -21,11 +21,11 @@ package release | ||
| import ( | ||
| "bufio" | ||
| - "io/ioutil" | ||
| "os" | ||
| - "path/filepath" | ||
| "strings" | ||
| "unicode" | ||
| + | ||
| + "github.com/snapcore/snapd/apparmor" | ||
| ) | ||
| // Series holds the Ubuntu Core series for snapd to use. | ||
| @@ -55,16 +55,8 @@ var ( | ||
| // ForceDevMode returns true if the distribution doesn't implement required | ||
| // security features for confinement and devmode is forced. | ||
| func (o *OS) ForceDevMode() bool { | ||
| - for _, req := range requiredApparmorFeatures { | ||
| - // Also ensure appamor is enabled (cannot use | ||
| - // osutil.FileExists() here because of cyclic imports) | ||
| - p := filepath.Join(apparmorFeaturesSysPath, req) | ||
| - if _, err := os.Stat(p); err != nil { | ||
| - return true | ||
| - } | ||
| - } | ||
| - | ||
| - return false | ||
| + level, _ := apparmor.ProbeKernel().Evaluate() | ||
mvo5
Collaborator
|
||
| + return level != apparmor.Full | ||
| } | ||
| var ( | ||
| @@ -151,26 +143,9 @@ func MockReleaseInfo(osRelease *OS) (restore func()) { | ||
| // MockForcedDevmode fake the system to believe its in a distro | ||
| // that is in ForcedDevmode | ||
| func MockForcedDevmode(isDevmode bool) (restore func()) { | ||
| - oldApparmorFeaturesSysPath := apparmorFeaturesSysPath | ||
| - | ||
| - temp, err := ioutil.TempDir("", "mock-forced-devmode") | ||
| - if err != nil { | ||
| - panic(err) | ||
| - } | ||
| - fakeApparmorFeaturesSysPath := filepath.Join(temp, "apparmor") | ||
| - if !isDevmode { | ||
| - for _, req := range requiredApparmorFeatures { | ||
| - if err := os.MkdirAll(filepath.Join(fakeApparmorFeaturesSysPath, req), 0755); err != nil { | ||
| - panic(err) | ||
| - } | ||
| - } | ||
| - } | ||
| - apparmorFeaturesSysPath = fakeApparmorFeaturesSysPath | ||
| - | ||
| - return func() { | ||
| - if err := os.RemoveAll(temp); err != nil { | ||
| - panic(err) | ||
| - } | ||
| - apparmorFeaturesSysPath = oldApparmorFeaturesSysPath | ||
| + level := apparmor.Full | ||
| + if isDevmode { | ||
| + level = apparmor.None | ||
| } | ||
| + return apparmor.MockFeatureLevel(level) | ||
| } | ||
Can you comment (not necessarily in the code, but in the PR) why you weren't able to use dirs/dirs.go?